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
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:
co-authored by
Claude Sonnet 5
parent
e1a36ce227
commit
567b4ef068
@@ -0,0 +1,31 @@
|
||||
"""Data access for Keyword."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.keyword import Keyword
|
||||
|
||||
|
||||
async def get_keyword_by_id(session: AsyncSession, keyword_id: uuid.UUID) -> Keyword | None:
|
||||
result = await session.execute(select(Keyword).where(Keyword.id == keyword_id))
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def list_keywords(session: AsyncSession, *, limit: int, offset: int) -> tuple[list[Keyword], int]:
|
||||
"""Browse keywords -- see genre_repository.list_genres, which this
|
||||
mirrors exactly (no filter, no sort_by, alphabetical by name)."""
|
||||
|
||||
stmt = (
|
||||
select(Keyword, func.count().over().label("total_count"))
|
||||
.order_by(Keyword.name.asc(), Keyword.id.asc())
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
)
|
||||
rows = (await session.execute(stmt)).all()
|
||||
if not rows:
|
||||
return [], 0
|
||||
return [row[0] for row in rows], rows[0].total_count
|
||||
@@ -12,7 +12,7 @@ from app.models.company import Company, MovieCompany
|
||||
from app.models.enums import MovieStatus
|
||||
from app.models.genre import Genre, MovieGenre
|
||||
from app.models.keyword import Keyword, MovieKeyword
|
||||
from app.models.movie import Movie, MovieRecommendation
|
||||
from app.models.movie import Movie, MovieAlternateTitle, MovieRecommendation
|
||||
|
||||
_MOVIE_SORT_COLUMNS = {
|
||||
"popularity": Movie.popularity,
|
||||
@@ -69,6 +69,18 @@ async def get_movie_companies(
|
||||
return [(company, role) for company, role in result.all()]
|
||||
|
||||
|
||||
async def get_movie_alternate_titles(
|
||||
session: AsyncSession, movie_id: uuid.UUID
|
||||
) -> list[MovieAlternateTitle]:
|
||||
stmt = (
|
||||
select(MovieAlternateTitle)
|
||||
.where(MovieAlternateTitle.movie_id == movie_id)
|
||||
.order_by(MovieAlternateTitle.country_code.asc().nulls_last(), MovieAlternateTitle.title.asc())
|
||||
)
|
||||
result = await session.execute(stmt)
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
async def get_movie_recommendation_ids(
|
||||
session: AsyncSession, movie_id: uuid.UUID
|
||||
) -> list[uuid.UUID]:
|
||||
@@ -90,6 +102,7 @@ async def list_movies(
|
||||
session: AsyncSession,
|
||||
*,
|
||||
genre_id: uuid.UUID | None,
|
||||
keyword_id: uuid.UUID | None,
|
||||
year: int | None,
|
||||
status: MovieStatus | None,
|
||||
sort_by: str,
|
||||
@@ -111,6 +124,10 @@ async def list_movies(
|
||||
stmt = stmt.join(MovieGenre, MovieGenre.movie_id == Movie.id).where(
|
||||
MovieGenre.genre_id == genre_id
|
||||
)
|
||||
if keyword_id is not None:
|
||||
stmt = stmt.join(MovieKeyword, MovieKeyword.movie_id == Movie.id).where(
|
||||
MovieKeyword.keyword_id == keyword_id
|
||||
)
|
||||
if year is not None:
|
||||
stmt = stmt.where(func.extract("year", Movie.primary_release_date) == year)
|
||||
if status is not None:
|
||||
|
||||
@@ -11,7 +11,7 @@ from app.models.company import Company, SeriesCompany
|
||||
from app.models.enums import CompanyType, SeriesStatus
|
||||
from app.models.genre import Genre, SeriesGenre
|
||||
from app.models.keyword import Keyword, SeriesKeyword
|
||||
from app.models.tv import Episode, Season, Series
|
||||
from app.models.tv import Episode, Season, Series, SeriesAlternateTitle
|
||||
|
||||
_SERIES_SORT_COLUMNS = {
|
||||
"popularity": Series.popularity,
|
||||
@@ -158,10 +158,25 @@ async def get_series_companies(
|
||||
return [(company, role) for company, role in result.all()]
|
||||
|
||||
|
||||
async def get_series_alternate_titles(
|
||||
session: AsyncSession, series_id: uuid.UUID
|
||||
) -> list[SeriesAlternateTitle]:
|
||||
stmt = (
|
||||
select(SeriesAlternateTitle)
|
||||
.where(SeriesAlternateTitle.series_id == series_id)
|
||||
.order_by(
|
||||
SeriesAlternateTitle.country_code.asc().nulls_last(), SeriesAlternateTitle.title.asc()
|
||||
)
|
||||
)
|
||||
result = await session.execute(stmt)
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
async def list_series(
|
||||
session: AsyncSession,
|
||||
*,
|
||||
genre_id: uuid.UUID | None,
|
||||
keyword_id: uuid.UUID | None,
|
||||
year: int | None,
|
||||
status: SeriesStatus | None,
|
||||
sort_by: str,
|
||||
@@ -180,6 +195,10 @@ async def list_series(
|
||||
stmt = stmt.join(SeriesGenre, SeriesGenre.series_id == Series.id).where(
|
||||
SeriesGenre.genre_id == genre_id
|
||||
)
|
||||
if keyword_id is not None:
|
||||
stmt = stmt.join(SeriesKeyword, SeriesKeyword.series_id == Series.id).where(
|
||||
SeriesKeyword.keyword_id == keyword_id
|
||||
)
|
||||
if year is not None:
|
||||
stmt = stmt.where(func.extract("year", Series.first_air_date) == year)
|
||||
if status is not None:
|
||||
|
||||
Reference in New Issue
Block a user