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)
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).
+12
View File
@@ -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):
+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.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
],
)
)
+1 -6
View File
@@ -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.
+22 -2
View File
@@ -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))