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
@@ -13,6 +13,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.ids import aurora_id
|
||||
from app.models.genre import Genre
|
||||
from app.models.keyword import Keyword
|
||||
from app.services import movie_service
|
||||
from app.services.ingestion.movie_ingestion import ingest_movie
|
||||
from tests.integration.test_movie_ingestion import FakeTMDbProvider
|
||||
@@ -41,6 +42,7 @@ async def _seed_movies(db_session: AsyncSession) -> None:
|
||||
popularity=90.0,
|
||||
vote_average=8.0,
|
||||
release_date="2021-01-01",
|
||||
keywords={"keywords": [{"id": 1, "name": "heist"}]},
|
||||
),
|
||||
_movie_payload(
|
||||
id=9003,
|
||||
@@ -70,6 +72,13 @@ async def _genre_aurora_id(db_session: AsyncSession, name: str) -> str:
|
||||
return aurora_id("genre", str(genre.id))
|
||||
|
||||
|
||||
async def _keyword_aurora_id(db_session: AsyncSession, name: str) -> str:
|
||||
keyword = (
|
||||
await db_session.execute(select(Keyword).where(Keyword.name == name))
|
||||
).scalar_one()
|
||||
return aurora_id("keyword", str(keyword.id))
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_movies_defaults_to_popularity_descending(db_session: AsyncSession):
|
||||
await _seed_movies(db_session)
|
||||
@@ -77,6 +86,7 @@ async def test_list_movies_defaults_to_popularity_descending(db_session: AsyncSe
|
||||
out = await movie_service.list_movies(
|
||||
db_session,
|
||||
genre_aurora_id=None,
|
||||
keyword_aurora_id=None,
|
||||
year=None,
|
||||
status=None,
|
||||
sort_by="popularity",
|
||||
@@ -103,6 +113,7 @@ async def test_list_movies_filters_by_genre(db_session: AsyncSession):
|
||||
out = await movie_service.list_movies(
|
||||
db_session,
|
||||
genre_aurora_id=action_id,
|
||||
keyword_aurora_id=None,
|
||||
year=None,
|
||||
status=None,
|
||||
sort_by="popularity",
|
||||
@@ -119,6 +130,42 @@ async def test_list_movies_filters_by_genre(db_session: AsyncSession):
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_movies_filters_by_keyword(db_session: AsyncSession):
|
||||
await _seed_movies(db_session)
|
||||
heist_id = await _keyword_aurora_id(db_session, "heist")
|
||||
|
||||
out = await movie_service.list_movies(
|
||||
db_session,
|
||||
genre_aurora_id=None,
|
||||
keyword_aurora_id=heist_id,
|
||||
year=None,
|
||||
status=None,
|
||||
sort_by="popularity",
|
||||
sort_dir="desc",
|
||||
page=1,
|
||||
page_size=20,
|
||||
)
|
||||
|
||||
assert [m.title for m in out.results] == ["B Action Movie"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_movies_rejects_malformed_keyword_filter(db_session: AsyncSession):
|
||||
with pytest.raises(movie_service.InvalidMovieFilterError):
|
||||
await movie_service.list_movies(
|
||||
db_session,
|
||||
genre_aurora_id=None,
|
||||
keyword_aurora_id="not-an-aurora-id",
|
||||
year=None,
|
||||
status=None,
|
||||
sort_by="popularity",
|
||||
sort_dir="desc",
|
||||
page=1,
|
||||
page_size=20,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_movies_filters_by_year(db_session: AsyncSession):
|
||||
await _seed_movies(db_session)
|
||||
@@ -126,6 +173,7 @@ async def test_list_movies_filters_by_year(db_session: AsyncSession):
|
||||
out = await movie_service.list_movies(
|
||||
db_session,
|
||||
genre_aurora_id=None,
|
||||
keyword_aurora_id=None,
|
||||
year=2020,
|
||||
status=None,
|
||||
sort_by="popularity",
|
||||
@@ -144,6 +192,7 @@ async def test_list_movies_filters_by_status(db_session: AsyncSession):
|
||||
out = await movie_service.list_movies(
|
||||
db_session,
|
||||
genre_aurora_id=None,
|
||||
keyword_aurora_id=None,
|
||||
year=None,
|
||||
status="planned",
|
||||
sort_by="popularity",
|
||||
@@ -162,6 +211,7 @@ async def test_list_movies_sorts_by_title_ascending(db_session: AsyncSession):
|
||||
out = await movie_service.list_movies(
|
||||
db_session,
|
||||
genre_aurora_id=None,
|
||||
keyword_aurora_id=None,
|
||||
year=None,
|
||||
status=None,
|
||||
sort_by="title",
|
||||
@@ -185,6 +235,7 @@ async def test_list_movies_paginates(db_session: AsyncSession):
|
||||
page1 = await movie_service.list_movies(
|
||||
db_session,
|
||||
genre_aurora_id=None,
|
||||
keyword_aurora_id=None,
|
||||
year=None,
|
||||
status=None,
|
||||
sort_by="popularity",
|
||||
@@ -195,6 +246,7 @@ async def test_list_movies_paginates(db_session: AsyncSession):
|
||||
page2 = await movie_service.list_movies(
|
||||
db_session,
|
||||
genre_aurora_id=None,
|
||||
keyword_aurora_id=None,
|
||||
year=None,
|
||||
status=None,
|
||||
sort_by="popularity",
|
||||
@@ -215,6 +267,7 @@ async def test_list_movies_rejects_malformed_genre_filter(db_session: AsyncSessi
|
||||
await movie_service.list_movies(
|
||||
db_session,
|
||||
genre_aurora_id="not-an-aurora-id",
|
||||
keyword_aurora_id=None,
|
||||
year=None,
|
||||
status=None,
|
||||
sort_by="popularity",
|
||||
@@ -230,6 +283,7 @@ async def test_list_movies_rejects_unknown_status_filter(db_session: AsyncSessio
|
||||
await movie_service.list_movies(
|
||||
db_session,
|
||||
genre_aurora_id=None,
|
||||
keyword_aurora_id=None,
|
||||
year=None,
|
||||
status="not_a_real_status",
|
||||
sort_by="popularity",
|
||||
@@ -246,6 +300,7 @@ async def test_list_movies_returns_empty_page_when_nothing_matches(db_session: A
|
||||
out = await movie_service.list_movies(
|
||||
db_session,
|
||||
genre_aurora_id=None,
|
||||
keyword_aurora_id=None,
|
||||
year=1899,
|
||||
status=None,
|
||||
sort_by="popularity",
|
||||
|
||||
Reference in New Issue
Block a user