diff --git a/VERSION b/VERSION
index 78756de..68e76af 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-0.57.0
\ No newline at end of file
+0.57.1
\ No newline at end of file
diff --git a/backend/app/downloads/og.py b/backend/app/downloads/og.py
index 23331fd..09f13f1 100644
--- a/backend/app/downloads/og.py
+++ b/backend/app/downloads/og.py
@@ -95,15 +95,22 @@ def _watch_tags(token: str, db: Session) -> str | None:
return None
title = (job.display_name or asset.title or "Video").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
- # signed CDN URL especially) expires and isn't reliably fetchable cross-origin by the crawler.
- # (This block only runs for non-password links, so the poster needs no grant.) Fall back to the
- # remote thumbnail only if we somehow have no poster.
- image = (
- f"{settings.app_base}/api/public/watch/{token}/poster.jpg"
- if asset.poster_path
- else asset.thumbnail_url
- )
+ # Pick the link-preview image. Facebook's scraper REJECTS our own poster ("Corrupted Image" in the
+ # Sharing Debugger): the poster is an ffmpeg-produced JPEG whose non-standard marker order FB's
+ # decoder won't accept, even though browsers/iOS/Twitter render it fine. So for a YouTube source
+ # prefer its STABLE hqdefault thumbnail — a Google-encoded JPEG (480×360) at a non-expiring URL
+ # (unlike the stored `sqp`-signed variant) that FB decodes reliably. Non-YouTube sources fall back
+ # to the poster (fine everywhere but FB) then the raw remote thumbnail. This block only runs for
+ # non-password links, so the poster needs no grant.
+ 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 = [
f"
{html.escape(title)}",
_tag("og:title", title),
@@ -118,20 +125,20 @@ def _watch_tags(token: str, db: Session) -> str | None:
if image:
# A real thumbnail → a large image card; without one, a plain (text) card is served.
tags.append(_tag("og:image", image))
- # Facebook's scraper is pickier than Apple's on-device unfurl: spell out that the image is a
- # public HTTPS JPEG, and declare its dimensions so the card includes the image on the very
- # first (async) scrape instead of unfurling title-only — see _jpeg_size.
+ # Declare the image is a public HTTPS JPEG with known dimensions, so FB (which fetches
+ # og:image asynchronously on first scrape) includes it instead of unfurling title-only. For a
+ # poster we read the real dimensions from its JPEG header; the ytimg hqdefault is a fixed 480×360.
if image.startswith("https://"):
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"))
+ 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 —
# 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)
- dims = _jpeg_size(poster_abs) if poster_abs else None
- if dims:
- tags.append(_tag("og:image:width", str(dims[0])))
- tags.append(_tag("og:image:height", str(dims[1])))
+ image_dims = _jpeg_size(poster_abs) if poster_abs else None
+ if image_dims:
+ tags.append(_tag("og:image:width", str(image_dims[0])))
+ tags.append(_tag("og:image:height", str(image_dims[1])))
tags.append(_tag("twitter:card", "summary_large_image", attr="name"))
tags.append(_tag("twitter:image", image, attr="name"))
else:
diff --git a/frontend/src/lib/releaseNotes.ts b/frontend/src/lib/releaseNotes.ts
index 95609a4..710d97b 100644
--- a/frontend/src/lib/releaseNotes.ts
+++ b/frontend/src/lib/releaseNotes.ts
@@ -14,6 +14,13 @@ export interface 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",
date: "2026-07-28",