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
+12
View File
@@ -29,6 +29,7 @@ from app.services.common import (
pick_certification,
primary_artwork_path,
split_credits,
to_alternate_title_out,
to_images_out,
to_release_date_out,
to_video_out,
@@ -98,6 +99,7 @@ async def get_series_detail(session: AsyncSession, series_aurora_id: str) -> Ser
genres = await tv_repository.get_series_genres(session, series_id)
keywords = await tv_repository.get_series_keywords(session, series_id)
alternate_titles = await tv_repository.get_series_alternate_titles(session, series_id)
networks = await tv_repository.get_series_companies(session, series_id, CompanyType.NETWORK)
companies = await tv_repository.get_series_companies(
session, series_id, CompanyType.PRODUCTION_COMPANY
@@ -133,6 +135,7 @@ async def get_series_detail(session: AsyncSession, series_aurora_id: str) -> Ser
certification=pick_certification(release_dates),
genres=[GenreOut(id=aurora_id("genre", str(g.id)), name=g.name) for g in genres],
keywords=[KeywordOut(id=aurora_id("keyword", str(k.id)), name=k.name) for k in keywords],
alternate_titles=[to_alternate_title_out(t) for t in alternate_titles],
creators=creators,
cast=cast,
crew=crew,
@@ -300,6 +303,7 @@ async def list_series(
session: AsyncSession,
*,
genre_aurora_id: str | None,
keyword_aurora_id: str | None,
year: int | None,
status: str | None,
sort_by: str,
@@ -314,6 +318,13 @@ async def list_series(
except ValueError as exc:
raise InvalidSeriesFilterError(f"invalid genre id: {genre_aurora_id!r}") from exc
keyword_id: uuid.UUID | None = None
if keyword_aurora_id is not None:
try:
keyword_id = uuid.UUID(parse_aurora_id(keyword_aurora_id, expected_type="keyword"))
except ValueError as exc:
raise InvalidSeriesFilterError(f"invalid keyword id: {keyword_aurora_id!r}") from exc
status_enum: SeriesStatus | None = None
if status is not None:
try:
@@ -324,6 +335,7 @@ async def list_series(
series_rows, total = await tv_repository.list_series(
session,
genre_id=genre_id,
keyword_id=keyword_id,
year=year,
status=status_enum,
sort_by=sort_by,