diff --git a/README.md b/README.md index 323e0a2..0c28687 100644 --- a/README.md +++ b/README.md @@ -185,9 +185,9 @@ transaction-mode pooling), horizontally-scaled API and worker replicas. See See [`docs/database.md#known-limitations`](docs/database.md#known-limitations) and [`docs/api.md#known-limitations`](docs/api.md#known-limitations) for the current, tracked list (no curated language/country seed data, no fuzzy -entity matching, no field-level provenance, no pagination beyond search, -person credits' episode-level guest appearances rolled up to a per-series -count rather than broken out individually). None of these block the Core -0.1 milestone (section 34): search → movie/series/person detail works -end-to-end against real TMDb data, verified on a real PostgreSQL + Redis + +entity matching, no field-level provenance, list-shaped responses beyond +search and browse/discover return everything with no pagination). None of +these block the Core 0.1 milestone (section 34): search → movie/series/person +detail works end-to-end against real TMDb data, verified on a real +PostgreSQL + Redis + Docker deployment (not just this repo's own dev machine). diff --git a/app/schemas/person.py b/app/schemas/person.py index b8c6090..25d7d8b 100644 --- a/app/schemas/person.py +++ b/app/schemas/person.py @@ -18,6 +18,14 @@ class PersonSummaryOut(BaseModel): popularity: float | None = None +class GuestEpisodeOut(BaseModel): + id: str + season_number: int + episode_number: int + title: str | None = None + air_date: date | None = None + + class FilmographyCreditOut(BaseModel): id: str media_id: str @@ -33,6 +41,10 @@ class FilmographyCreditOut(BaseModel): # those are aggregated to one row per series rather than one per # episode. None for movie credits and regular (non-guest) series roles. episode_count: int | None = None + # The individual episodes behind episode_count, sorted by (season, + # episode) -- empty for movie credits and regular (non-guest) series + # roles, same as episode_count. + episodes: list[GuestEpisodeOut] = [] class PersonDetailOut(BaseModel): diff --git a/app/services/person_service.py b/app/services/person_service.py index 3f38524..2bc3f10 100644 --- a/app/services/person_service.py +++ b/app/services/person_service.py @@ -14,6 +14,7 @@ from app.models.tv import Series from app.repositories import movie_repository, person_repository, polymorphic, tv_repository from app.schemas.person import ( FilmographyCreditOut, + GuestEpisodeOut, PersonCreditsOut, PersonDetailOut, PersonSummaryOut, @@ -156,6 +157,10 @@ async def get_person_credits(session: AsyncSession, person_aurora_id: str) -> Pe if series is None: continue representative = group[0] + guest_episodes = sorted( + (episodes[c.entity_id] for c in group if c.entity_id in episodes), + key=lambda ep: (ep.season_number, ep.episode_number), + ) _bucket(department).append( FilmographyCreditOut( id=aurora_id("credit", str(representative.id)), @@ -168,6 +173,16 @@ async def get_person_credits(session: AsyncSession, person_aurora_id: str) -> Pe release_date=series.first_air_date, poster_path=await _series_poster_path(session, series.id), episode_count=len(group), + episodes=[ + GuestEpisodeOut( + id=aurora_id("episode", str(ep.id)), + season_number=ep.season_number, + episode_number=ep.episode_number, + title=ep.title, + air_date=ep.air_date, + ) + for ep in guest_episodes + ], ) ) diff --git a/docs/api.md b/docs/api.md index ca95c5b..4633716 100644 --- a/docs/api.md +++ b/docs/api.md @@ -145,7 +145,7 @@ network and a production company are both `Company` rows distinguished by | Method | Path | |---|---| | GET | `/api/v1/people/{aurora_id}` | -| GET | `/api/v1/people/{aurora_id}/credits` (movie and series filmography; guest-star episode credits are aggregated to one row per series with `episode_count`, not one row per episode) | +| GET | `/api/v1/people/{aurora_id}/credits` (movie and series filmography; guest-star episode credits are aggregated to one row per series rather than one per episode, but carry both `episode_count` and the individual `episodes` breakdown, sorted by season/episode number) | ## Collections / Companies @@ -195,8 +195,3 @@ you want network-level isolation too. person's `credits`) still return everything with no pagination at all — fine at today's scale, but the natural next thing to revisit if any of them grow large. -- `/people/{id}/credits` covers movies and series (episode guest-star - credits rolled up per series). It does not yet expose a per-episode - breakdown for those guest appearances (just the aggregate - `episode_count`) — the individual episode `Credit` rows exist in the - database if that's ever needed. diff --git a/tests/integration/test_person_credits.py b/tests/integration/test_person_credits.py index 8ed7930..7d34891 100644 --- a/tests/integration/test_person_credits.py +++ b/tests/integration/test_person_credits.py @@ -57,11 +57,24 @@ async def test_person_credits_include_movies_series_and_aggregated_guest_spots( db_session.add(season) await db_session.flush() + # Inserted out of episode-number order deliberately, to prove the + # aggregated breakdown sorts by (season, episode) rather than insertion + # or credit-creation order. episode1 = Episode( - series_id=series.id, season_id=season.id, season_number=1, episode_number=1 + series_id=series.id, + season_id=season.id, + season_number=1, + episode_number=2, + title="Second Appearance", + air_date=date(2019, 5, 15), ) episode2 = Episode( - series_id=series.id, season_id=season.id, season_number=1, episode_number=2 + series_id=series.id, + season_id=season.id, + season_number=1, + episode_number=1, + title="First Appearance", + air_date=date(2019, 5, 8), ) db_session.add_all([episode1, episode2]) await db_session.flush() @@ -99,3 +112,10 @@ async def test_person_credits_include_movies_series_and_aggregated_guest_spots( guest = next(c for c in series_entries if c.character == "Guest Character") assert guest.episode_count == 2 + assert len(guest.episodes) == 2 + assert [e.episode_number for e in guest.episodes] == [1, 2] + assert [e.title for e in guest.episodes] == ["First Appearance", "Second Appearance"] + assert guest.episodes[0].season_number == 1 + assert guest.episodes[0].air_date == date(2019, 5, 8) + assert guest.episodes[0].id == aurora_id("episode", str(episode2.id)) + assert guest.episodes[1].id == aurora_id("episode", str(episode1.id))