Add keyword browsing/filtering and expose alternate titles
CI / lint (push) Canceled after 0s
CI / typecheck (push) Canceled after 0s
CI / unit-tests (push) Canceled after 0s
CI / integration-tests (push) Canceled after 0s
CI / container-build (push) Canceled after 0s
CI / dependency-audit (push) Canceled after 0s

Keywords were ingested and shown read-only on detail pages, but had no
directory (GET /keywords) or filter param -- the same gap genres had
before today. Mirrors the genre browse pattern exactly: repository,
service, route, plus a `keyword` filter on GET /movies and GET /series.

Alternate/translated titles ("Also Known As") were normalized down to
bare title strings and never written to MovieAlternateTitle/
SeriesAlternateTitle at all. Normalizer now captures country_code + type
per TMDb's actual shape; ingestion writes them; movie/series detail now
expose an alternate_titles list.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
NixReaper
2026-09-06 04:40:32 -05:00
co-authored by Claude Sonnet 5
parent e1a36ce227
commit 567b4ef068
23 changed files with 548 additions and 17 deletions
+21 -4
View File
@@ -96,6 +96,23 @@ def company_logo_artwork(logo_path: str | None) -> NormalizedArtwork | None:
)
@dataclass
class NormalizedAlternateTitle:
title: str
country_code: str | None
type: str | None = None
def _alternate_titles(entries: list[dict]) -> list[NormalizedAlternateTitle]:
return [
NormalizedAlternateTitle(
title=t["title"], country_code=t.get("iso_3166_1") or None, type=t.get("type") or None
)
for t in entries
if t.get("title")
]
# TMDb's per-country rating body isn't returned alongside a release_dates
# certification or a content_ratings rating -- only the country and the
# rating string itself are. This maps the handful of markets Aurora can
@@ -216,7 +233,7 @@ class NormalizedMovie:
adult: bool
genres: list[str] = field(default_factory=list)
keywords: list[str] = field(default_factory=list)
alternate_titles: list[str] = field(default_factory=list)
alternate_titles: list[NormalizedAlternateTitle] = field(default_factory=list)
companies: list[NormalizedCompanyRef] = field(default_factory=list)
cast: list[NormalizedPersonRef] = field(default_factory=list)
crew: list[NormalizedPersonRef] = field(default_factory=list)
@@ -419,7 +436,7 @@ def normalize_movie(raw: dict) -> NormalizedMovie:
adult=bool(raw.get("adult", False)),
genres=[g["name"] for g in raw.get("genres", [])],
keywords=[k["name"] for k in keywords],
alternate_titles=[t["title"] for t in alt_titles if t.get("title")],
alternate_titles=_alternate_titles(alt_titles),
companies=companies,
cast=cast,
crew=crew,
@@ -567,7 +584,7 @@ class NormalizedSeries:
vote_count: int | None
genres: list[str] = field(default_factory=list)
keywords: list[str] = field(default_factory=list)
alternate_titles: list[str] = field(default_factory=list)
alternate_titles: list[NormalizedAlternateTitle] = field(default_factory=list)
networks: list[NormalizedCompanyRef] = field(default_factory=list)
companies: list[NormalizedCompanyRef] = field(default_factory=list)
creators: list[NormalizedPersonRef] = field(default_factory=list)
@@ -748,7 +765,7 @@ def normalize_series(raw: dict) -> NormalizedSeries:
vote_count=raw.get("vote_count"),
genres=[g["name"] for g in raw.get("genres", [])],
keywords=[k["name"] for k in keywords],
alternate_titles=[t["title"] for t in alt_titles if t.get("title")],
alternate_titles=_alternate_titles(alt_titles),
networks=networks,
companies=companies,
creators=creators,