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
+20 -1
View File
@@ -25,7 +25,7 @@ from app.models.credit import Credit
from app.models.enums import MatchConfidence, MovieStatus, VideoType
from app.models.genre import MovieGenre
from app.models.keyword import MovieKeyword
from app.models.movie import Movie, MovieRecommendation
from app.models.movie import Movie, MovieAlternateTitle, MovieRecommendation
from app.models.video import Video
from app.providers.base import MetadataProvider
from app.repositories import polymorphic
@@ -51,6 +51,24 @@ async def _replace_movie_keywords(session: AsyncSession, movie_id: uuid.UUID, na
session.add(MovieKeyword(movie_id=movie_id, keyword_id=keyword.id))
async def _replace_movie_alternate_titles(
session: AsyncSession, movie_id: uuid.UUID, alt_titles
) -> None:
await session.execute(
delete(MovieAlternateTitle).where(MovieAlternateTitle.movie_id == movie_id)
)
for alt in alt_titles:
country_code = None
if alt.country_code:
country = await refs.get_or_create_country(session, alt.country_code, alt.country_code)
country_code = country.code
session.add(
MovieAlternateTitle(
movie_id=movie_id, title=alt.title, country_code=country_code, type=alt.type
)
)
async def _replace_movie_companies(
session: AsyncSession, movie_id: uuid.UUID, provider_id: uuid.UUID, companies
) -> None:
@@ -239,6 +257,7 @@ async def ingest_movie(
await _replace_movie_genres(session, movie.id, normalized.genres)
await _replace_movie_keywords(session, movie.id, normalized.keywords)
await _replace_movie_alternate_titles(session, movie.id, normalized.alternate_titles)
await _replace_movie_companies(session, movie.id, tmdb_provider.id, normalized.companies)
await _replace_movie_credits(session, movie.id, tmdb_provider.id, normalized.cast, normalized.crew)
await polymorphic.sync_artwork(