Add per-episode breakdown to guest-star filmography credits
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

FilmographyCreditOut already aggregated a guest star's episode
appearances into one row per series with an episode_count, but never
exposed which episodes those were even though the individual episode
Credit rows exist in the database. Adds an `episodes` list (season/
episode number, title, air date, sorted chronologically) alongside the
existing count -- closes a known limitation tracked in docs/api.md.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
NixReaper
2026-09-05 09:19:52 -05:00
co-authored by Claude Sonnet 5
parent 20f5e82d1b
commit 606d2532c4
5 changed files with 55 additions and 13 deletions
+5 -5
View File
@@ -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) See [`docs/database.md#known-limitations`](docs/database.md#known-limitations)
and [`docs/api.md#known-limitations`](docs/api.md#known-limitations) for the and [`docs/api.md#known-limitations`](docs/api.md#known-limitations) for the
current, tracked list (no curated language/country seed data, no fuzzy current, tracked list (no curated language/country seed data, no fuzzy
entity matching, no field-level provenance, no pagination beyond search, entity matching, no field-level provenance, list-shaped responses beyond
person credits' episode-level guest appearances rolled up to a per-series search and browse/discover return everything with no pagination). None of
count rather than broken out individually). None of these block the Core these block the Core 0.1 milestone (section 34): search → movie/series/person
0.1 milestone (section 34): search → movie/series/person detail works detail works end-to-end against real TMDb data, verified on a real
end-to-end against real TMDb data, verified on a real PostgreSQL + Redis + PostgreSQL + Redis +
Docker deployment (not just this repo's own dev machine). Docker deployment (not just this repo's own dev machine).
+12
View File
@@ -18,6 +18,14 @@ class PersonSummaryOut(BaseModel):
popularity: float | None = None 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): class FilmographyCreditOut(BaseModel):
id: str id: str
media_id: str media_id: str
@@ -33,6 +41,10 @@ class FilmographyCreditOut(BaseModel):
# those are aggregated to one row per series rather than one per # those are aggregated to one row per series rather than one per
# episode. None for movie credits and regular (non-guest) series roles. # episode. None for movie credits and regular (non-guest) series roles.
episode_count: int | None = None 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): class PersonDetailOut(BaseModel):
+15
View File
@@ -14,6 +14,7 @@ from app.models.tv import Series
from app.repositories import movie_repository, person_repository, polymorphic, tv_repository from app.repositories import movie_repository, person_repository, polymorphic, tv_repository
from app.schemas.person import ( from app.schemas.person import (
FilmographyCreditOut, FilmographyCreditOut,
GuestEpisodeOut,
PersonCreditsOut, PersonCreditsOut,
PersonDetailOut, PersonDetailOut,
PersonSummaryOut, PersonSummaryOut,
@@ -156,6 +157,10 @@ async def get_person_credits(session: AsyncSession, person_aurora_id: str) -> Pe
if series is None: if series is None:
continue continue
representative = group[0] 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( _bucket(department).append(
FilmographyCreditOut( FilmographyCreditOut(
id=aurora_id("credit", str(representative.id)), 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, release_date=series.first_air_date,
poster_path=await _series_poster_path(session, series.id), poster_path=await _series_poster_path(session, series.id),
episode_count=len(group), 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
],
) )
) )
+1 -6
View File
@@ -145,7 +145,7 @@ network and a production company are both `Company` rows distinguished by
| Method | Path | | Method | Path |
|---|---| |---|---|
| GET | `/api/v1/people/{aurora_id}` | | 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 ## Collections / Companies
@@ -195,8 +195,3 @@ you want network-level isolation too.
person's `credits`) still return everything with no pagination at all — 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 fine at today's scale, but the natural next thing to revisit if any of
them grow large. 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.
+22 -2
View File
@@ -57,11 +57,24 @@ async def test_person_credits_include_movies_series_and_aggregated_guest_spots(
db_session.add(season) db_session.add(season)
await db_session.flush() 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( 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( 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]) db_session.add_all([episode1, episode2])
await db_session.flush() 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") guest = next(c for c in series_entries if c.character == "Guest Character")
assert guest.episode_count == 2 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))