diff --git a/backend/app/downloads/backfill.py b/backend/app/downloads/backfill.py index 5e3aa54..de99c41 100644 --- a/backend/app/downloads/backfill.py +++ b/backend/app/downloads/backfill.py @@ -78,7 +78,9 @@ def run() -> dict: a.uploader_url = uu filled_url += 1 log.info("asset %s uploader_url <- %s", a.id, uu) - if a.thumbnail_url is None and a.poster_path is None: + if a.poster_path is None: + # Record a self-hosted poster for every download (the reliable og:image source): + # ensure_poster returns the existing .jpg thumbnail sidecar, or cuts a frame. rel = editmod.ensure_poster(a, settings.download_root) if rel: a.poster_path = rel diff --git a/backend/app/downloads/og.py b/backend/app/downloads/og.py index 54613a5..a28ee99 100644 --- a/backend/app/downloads/og.py +++ b/backend/app/downloads/og.py @@ -50,11 +50,14 @@ 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 the source thumbnail; fall back to our generated poster (absolute, token-gated URL) so - # even a thumbnail-less video (e.g. a reddit clip) unfurls with an image. (This block only runs - # for non-password links, so the poster needs no grant.) - image = asset.thumbnail_url or ( - f"{settings.app_base}/api/public/watch/{token}/poster.jpg" if asset.poster_path else None + # 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 ) tags = [ f"{html.escape(title)}", diff --git a/backend/app/worker.py b/backend/app/worker.py index 39b8f22..ab06b3f 100644 --- a/backend/app/worker.py +++ b/backend/app/worker.py @@ -428,10 +428,11 @@ def _download(job_id: int, asset_id: int, source_kind: str, source_ref: str, spe asset.uploader_url = _uploader_url(info) asset.upload_date = meta.upload_date asset.thumbnail_url = meta.thumbnail_url - # No thumbnail from the source (e.g. a direct media URL) → cut a poster frame from - # the file so the card / watch page / link preview still shows an image. - if not meta.thumbnail_url: - asset.poster_path = editmod.ensure_poster(asset, settings.download_root) + # Record a self-hosted poster for every download: it's the reliable link-preview + # (og:image) source — a remote thumbnail, esp. Facebook's signed CDN URL, expires + # and can't be relied on cross-origin. write_sidecars already wrote .jpg from + # the thumbnail; ensure_poster returns that, or cuts a frame if there was none. + asset.poster_path = editmod.ensure_poster(asset, settings.download_root) asset.source_webpage_url = info.get("webpage_url") asset.nfo_written = nfo_ok asset.error = None diff --git a/frontend/src/lib/releaseNotes.ts b/frontend/src/lib/releaseNotes.ts index 7717eb1..6147913 100644 --- a/frontend/src/lib/releaseNotes.ts +++ b/frontend/src/lib/releaseNotes.ts @@ -20,7 +20,7 @@ export const RELEASE_NOTES: ReleaseEntry[] = [ summary: "Clickable channels, poster images for any source, and a dialog-dismiss fix.", features: [ "Downloads: the auto-detected channel under a download is now a clickable link to the real channel for YouTube and Facebook sources. Your existing downloads were backfilled, so they're clickable too.", - "Downloads: a video from a source with no thumbnail (e.g. a direct Reddit video link) now gets a poster image generated from the video itself — shown on the card, on the shared watch page, and in rich link previews. Existing thumbnail-less downloads were backfilled.", + "Downloads: a video from a source with no thumbnail (e.g. a direct Reddit video link) now gets a poster image generated from the video itself — shown on the card, on the shared watch page, and in rich link previews. Shared-link previews now use this self-hosted image, so they render reliably in chat apps and don't break over time. Existing downloads were backfilled.", ], fixes: [ "Dialogs no longer close by accident when you press the mouse inside (e.g. selecting text in a field), drag outside, and release — a popup now only closes when the click both starts and ends on the backdrop.",