Merge: promote dev to prod
This commit is contained in:
+24
-17
@@ -95,15 +95,22 @@ def _watch_tags(token: str, db: Session) -> str | None:
|
|||||||
return None
|
return None
|
||||||
title = (job.display_name or asset.title or "Video").strip()
|
title = (job.display_name or asset.title or "Video").strip()
|
||||||
channel = (job.display_uploader or asset.uploader or "").strip()
|
channel = (job.display_uploader or asset.uploader or "").strip()
|
||||||
# Prefer our self-hosted poster for the link-preview image: a remote thumbnail (Facebook's
|
# Pick the link-preview image. Facebook's scraper REJECTS our own poster ("Corrupted Image" in the
|
||||||
# signed CDN URL especially) expires and isn't reliably fetchable cross-origin by the crawler.
|
# Sharing Debugger): the poster is an ffmpeg-produced JPEG whose non-standard marker order FB's
|
||||||
# (This block only runs for non-password links, so the poster needs no grant.) Fall back to the
|
# decoder won't accept, even though browsers/iOS/Twitter render it fine. So for a YouTube source
|
||||||
# remote thumbnail only if we somehow have no poster.
|
# prefer its STABLE hqdefault thumbnail — a Google-encoded JPEG (480×360) at a non-expiring URL
|
||||||
image = (
|
# (unlike the stored `sqp`-signed variant) that FB decodes reliably. Non-YouTube sources fall back
|
||||||
f"{settings.app_base}/api/public/watch/{token}/poster.jpg"
|
# to the poster (fine everywhere but FB) then the raw remote thumbnail. This block only runs for
|
||||||
if asset.poster_path
|
# non-password links, so the poster needs no grant.
|
||||||
else asset.thumbnail_url
|
yt_id_match = re.search(r"i\.ytimg\.com/vi/([\w-]+)/", asset.thumbnail_url or "")
|
||||||
)
|
image_dims: tuple[int, int] | None = None
|
||||||
|
if yt_id_match:
|
||||||
|
image: str | None = f"https://i.ytimg.com/vi/{yt_id_match.group(1)}/hqdefault.jpg"
|
||||||
|
image_dims = (480, 360)
|
||||||
|
elif asset.poster_path:
|
||||||
|
image = f"{settings.app_base}/api/public/watch/{token}/poster.jpg"
|
||||||
|
else:
|
||||||
|
image = asset.thumbnail_url
|
||||||
tags = [
|
tags = [
|
||||||
f"<title>{html.escape(title)}</title>",
|
f"<title>{html.escape(title)}</title>",
|
||||||
_tag("og:title", title),
|
_tag("og:title", title),
|
||||||
@@ -118,20 +125,20 @@ def _watch_tags(token: str, db: Session) -> str | None:
|
|||||||
if image:
|
if image:
|
||||||
# A real thumbnail → a large image card; without one, a plain (text) card is served.
|
# A real thumbnail → a large image card; without one, a plain (text) card is served.
|
||||||
tags.append(_tag("og:image", image))
|
tags.append(_tag("og:image", image))
|
||||||
# Facebook's scraper is pickier than Apple's on-device unfurl: spell out that the image is a
|
# Declare the image is a public HTTPS JPEG with known dimensions, so FB (which fetches
|
||||||
# public HTTPS JPEG, and declare its dimensions so the card includes the image on the very
|
# og:image asynchronously on first scrape) includes it instead of unfurling title-only. For a
|
||||||
# first (async) scrape instead of unfurling title-only — see _jpeg_size.
|
# poster we read the real dimensions from its JPEG header; the ytimg hqdefault is a fixed 480×360.
|
||||||
if image.startswith("https://"):
|
if image.startswith("https://"):
|
||||||
tags.append(_tag("og:image:secure_url", image))
|
tags.append(_tag("og:image:secure_url", image))
|
||||||
if image.endswith("poster.jpg") and asset.poster_path:
|
|
||||||
tags.append(_tag("og:image:type", "image/jpeg"))
|
tags.append(_tag("og:image:type", "image/jpeg"))
|
||||||
|
if image_dims is None and image.endswith("poster.jpg") and asset.poster_path:
|
||||||
# safe_abs_path returns None if the poster file is missing (gc'd) or escapes the root —
|
# safe_abs_path returns None if the poster file is missing (gc'd) or escapes the root —
|
||||||
# guard it, else _jpeg_size(None) would raise an uncaught AttributeError on this public page.
|
# guard it, else _jpeg_size(None) would raise an uncaught AttributeError on this public page.
|
||||||
poster_abs = storage.safe_abs_path(settings.download_root, asset.poster_path)
|
poster_abs = storage.safe_abs_path(settings.download_root, asset.poster_path)
|
||||||
dims = _jpeg_size(poster_abs) if poster_abs else None
|
image_dims = _jpeg_size(poster_abs) if poster_abs else None
|
||||||
if dims:
|
if image_dims:
|
||||||
tags.append(_tag("og:image:width", str(dims[0])))
|
tags.append(_tag("og:image:width", str(image_dims[0])))
|
||||||
tags.append(_tag("og:image:height", str(dims[1])))
|
tags.append(_tag("og:image:height", str(image_dims[1])))
|
||||||
tags.append(_tag("twitter:card", "summary_large_image", attr="name"))
|
tags.append(_tag("twitter:card", "summary_large_image", attr="name"))
|
||||||
tags.append(_tag("twitter:image", image, attr="name"))
|
tags.append(_tag("twitter:image", image, attr="name"))
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -14,6 +14,13 @@ export interface ReleaseEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const RELEASE_NOTES: ReleaseEntry[] = [
|
export const RELEASE_NOTES: ReleaseEntry[] = [
|
||||||
|
{
|
||||||
|
version: "0.57.1",
|
||||||
|
date: "2026-07-28",
|
||||||
|
fixes: [
|
||||||
|
"Sharing: a YouTube video's shared-link preview now uses the original YouTube thumbnail, which Facebook Messenger reliably shows — the previously-served self-hosted image was a valid picture everywhere else but Facebook's stricter processor rejected it as “corrupted”.",
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
version: "0.57.0",
|
version: "0.57.0",
|
||||||
date: "2026-07-28",
|
date: "2026-07-28",
|
||||||
|
|||||||
Reference in New Issue
Block a user