feat(titles): normalize video titles for display (feed, search, downloads)

Noisy YouTube titles are cleaned for display + storage, raw kept in videos.original_title:
- app/titles.normalize_title: strip emoji/symbols (keep accents), remove trailing SEO hashtag
  clusters (keep numeric #3 episode markers), context-aware de-shout (mostly-ALL-CAPS titles ->
  Title Case with an acronym whitelist + function-word lowercasing; otherwise only long all-caps
  words), collapse repeated punctuation
- applied at enrichment (sync/videos.py) and in the download worker (ad-hoc yt-dlp titles);
  catalog downloads inherit the normalized title automatically
- migration 0039: add original_title, preserve raw, rewrite title (generated search_vector
  regenerates); reversible via original_title

Backfill on localdev: 122115/273417 titles normalized in ~2 min. Verified in the feed + on
real messy samples (emoji/de-shout/hashtags), accents + acronyms (PS5/AI/USA/PC) preserved.
This commit is contained in:
npeter83 2026-07-03 03:00:08 +02:00
parent b200f61642
commit d55799cbc1
5 changed files with 140 additions and 4 deletions

View file

@ -30,6 +30,7 @@ from app.config import settings
from app.db import SessionLocal
from app.downloads import formats, quota, service, storage
from app.models import DownloadJob, MediaAsset
from app.titles import normalize_title
log = logging.getLogger("siftlode.worker")
@ -190,7 +191,7 @@ def _fill_asset_meta_early(asset_id: int, info: dict) -> None:
asset = db.get(MediaAsset, asset_id)
if asset is None or asset.title:
return
asset.title = info.get("title")
asset.title = normalize_title(info.get("title"))
asset.uploader = info.get("uploader") or info.get("channel")
asset.thumbnail_url = info.get("thumbnail")
asset.duration_s = int(info["duration"]) if info.get("duration") else None
@ -297,7 +298,7 @@ def _download(job_id: int, asset_id: int, source_kind: str, source_ref: str, spe
ext = produced.suffix.lstrip(".").lower()
meta = storage.MediaMeta(
video_id=info.get("id") or source_ref,
title=info.get("title") or source_ref,
title=normalize_title(info.get("title")) or source_ref,
uploader=info.get("uploader") or info.get("channel") or "Unknown Channel",
upload_date=_parse_upload_date(info.get("upload_date")),
duration_s=int(info["duration"]) if info.get("duration") else None,