"""Server-rendered Open Graph tags for the public /watch/{token} page. The watch page is a client-side SPA, so a social crawler (facebookexternalhit, Twitterbot, …) that fetches /watch/{token} would otherwise only see the generic index.html — a blank/greyed link card. This injects per-video OG/Twitter tags into the served HTML so a shared link unfurls with the video's title, channel and thumbnail. Real browsers ignore the extra tags and hydrate the SPA as usual. Password-protected / expired / invalid links fall back to the generic card (no metadata leak). """ import html import re from pathlib import Path from sqlalchemy import select from sqlalchemy.orm import Session from app.config import settings from app.downloads import links as linksmod from app.downloads import storage from app.models import DownloadJob, DownloadLink, MediaAsset _OG_BLOCK = re.compile(r".*?", re.DOTALL) def _tag(prop: str, content: str, attr: str = "property") -> str: return f'' def _jpeg_size(path: Path) -> tuple[int, int] | None: """Read (width, height) from a JPEG's SOF marker without an image library (Pillow isn't a dep). Facebook's scraper fetches og:image ASYNCHRONOUSLY on a URL's first scrape, so a card built then has NO image unless og:image:width/height are declared up front — which is exactly why a freshly shared /watch link unfurled title-only on desktop Messenger. We can't declare dimensions we don't know, and the poster's aspect differs from the video's, so read them straight from the JPEG here (cheap — only on a crawler hit). Returns None for a non-JPEG/truncated file, in which case the dimensions are simply omitted (the prior behaviour).""" try: with path.open("rb") as f: if f.read(2) != b"\xff\xd8": # SOI — not a JPEG return None while True: b = f.read(1) if not b: return None if b != b"\xff": continue marker = f.read(1) while marker == b"\xff": # skip fill bytes marker = f.read(1) if not marker: return None m = marker[0] if m == 0x01 or 0xD0 <= m <= 0xD9: # standalone markers, no length payload continue seg = f.read(2) if len(seg) < 2: return None length = (seg[0] << 8) + seg[1] # SOF0..SOF15 carry the frame dimensions (skip DHT/DAC/RST, which share the range). if 0xC0 <= m <= 0xCF and m not in (0xC4, 0xC8, 0xCC): data = f.read(5) if len(data) < 5: return None height = (data[1] << 8) + data[2] width = (data[3] << 8) + data[4] return width, height f.seek(length - 2, 1) # skip this segment's payload except OSError: return None def _watch_tags(token: str, db: Session) -> str | None: """Build the OG/Twitter meta block for a watch token, or None to keep the generic card.""" link = db.execute( select(DownloadLink).where(DownloadLink.token == token) ).scalar_one_or_none() if link is None or linksmod.is_expired(link): return None url = f"{settings.app_base}/watch/{token}" if link.password_hash: # Password-gated: don't leak the title/thumbnail in the unfurl. return "\n ".join( [ "