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

@ -11,6 +11,7 @@ from sqlalchemy.orm import Session
from app import progress, sysconfig
from app.models import Channel, Video
from app.titles import normalize_title
from app.youtube.client import YouTubeClient, best_thumbnail
from app.youtube.rss import fetch_channel_feed
from app.youtube.shorts import make_client, probe_is_short
@ -225,7 +226,11 @@ def apply_video_details(video: Video, item: dict) -> None:
live = item.get("liveStreamingDetails")
status = item.get("status", {})
video.title = snippet.get("title") or video.title
raw_title = snippet.get("title")
if raw_title:
# Store the raw title and a normalized one for display (feed/search/downloads).
video.original_title = raw_title
video.title = normalize_title(raw_title)
video.description = snippet.get("description")
if not video.published_at:
video.published_at = parse_dt(snippet.get("publishedAt"))