E5 S2: prioritise TMDB crew, and capture the TV showrunner
A live smoke against the real TMDB API showed two crew problems the mocked tests missed: (1) a TV show's creator/showrunner lives in the payload's top-level `created_by`, not the crew list, so it was dropped entirely; and (2) TMDB's crew order is arbitrary — a TV show's per-episode Directors (dozens) flooded the key-crew list and buried the show-level roles. Fix: split the accepted crew jobs by media type — movies key on Director/Writer/…; TV keys on Creator (from created_by) + the show-level Executive Producer/Writer/Producer/Composer, deliberately excluding the per-episode Director/Editor/DP noise. Stored crew is sorted by display priority and capped, so the lead roles come first. Verified live: Breaking Bad -> [Gilligan (Creator), Johnson (EP), … , Writer]; The Godfather -> [Coppola (Director), Puzo (Screenplay), …]. test_plex_tmdb green (9).
This commit is contained in:
@@ -27,8 +27,12 @@ log = logging.getLogger("siftlode.tmdb")
|
||||
# Politeness throttle: TMDB tolerates ~50 req/s; ~33/s leaves comfortable headroom for a shared host.
|
||||
_MIN_INTERVAL = 0.03
|
||||
|
||||
# The crew jobs worth surfacing on an info page (TMDB lists dozens per title). Order-preserving pick.
|
||||
_CREW_JOBS = (
|
||||
# The crew jobs worth surfacing on an info page, in DISPLAY-priority order (the stored crew is sorted by
|
||||
# this — TMDB's own crew order is arbitrary and buries the lead roles). MOVIE and TV differ on purpose:
|
||||
# on a TV show `Director`/`Editor`/`Director of Photography` are PER-EPISODE credits (dozens of names
|
||||
# that would flood the list and bury the showrunner), so TV keys on `Creator` (from the payload's
|
||||
# top-level `created_by`, not the crew list) + the show-level producers/writers instead.
|
||||
_MOVIE_CREW_JOBS = (
|
||||
"Director",
|
||||
"Writer",
|
||||
"Screenplay",
|
||||
@@ -39,6 +43,9 @@ _CREW_JOBS = (
|
||||
"Director of Photography",
|
||||
"Original Music Composer",
|
||||
)
|
||||
_TV_CREW_JOBS = ("Creator", "Executive Producer", "Writer", "Producer", "Original Music Composer")
|
||||
_MOVIE_PRIORITY = {job: i for i, job in enumerate(_MOVIE_CREW_JOBS)}
|
||||
_TV_PRIORITY = {job: i for i, job in enumerate(_TV_CREW_JOBS)}
|
||||
|
||||
# append_to_response bundles the sub-resources into ONE request per title (keywords + credits +
|
||||
# recommendations + images + external ids). TV uses aggregate_credits (per-person aggregated jobs).
|
||||
@@ -159,16 +166,28 @@ def parse_tmdb(payload: dict, media_type: str) -> dict:
|
||||
kw_list = kw_container.get("results") if is_tv else kw_container.get("keywords")
|
||||
keywords = [k.get("name") for k in (kw_list or []) if k.get("name")][:20]
|
||||
|
||||
priority = _TV_PRIORITY if is_tv else _MOVIE_PRIORITY
|
||||
credits = payload.get("aggregate_credits" if is_tv else "credits") or {}
|
||||
crew: list[dict] = []
|
||||
# A TV show's creators live at the top level (created_by), not in the crew list — surface them.
|
||||
if is_tv:
|
||||
for c in payload.get("created_by") or []:
|
||||
if c.get("name"):
|
||||
crew.append(
|
||||
{"name": c["name"], "job": "Creator", "tmdb_id": c.get("id"), "profile_path": c.get("profile_path")}
|
||||
)
|
||||
for c in credits.get("crew") or []:
|
||||
# Movie crew entries carry `job`; TV aggregate_credits carry a `jobs` array per person.
|
||||
job = (c.get("jobs") or [{}])[0].get("job") if is_tv else c.get("job")
|
||||
if job in _CREW_JOBS and c.get("name"):
|
||||
if job in priority and c.get("name"):
|
||||
crew.append(
|
||||
{"name": c["name"], "job": job, "tmdb_id": c.get("id"), "profile_path": c.get("profile_path")}
|
||||
)
|
||||
crew = _dedup_crew(crew)[:12]
|
||||
# Dedup by (name, job) — a person can hold two distinct key roles (Coppola: Director + Writer) — then
|
||||
# order by display priority so the lead roles come first, and cap.
|
||||
crew = _dedup_crew(crew)
|
||||
crew.sort(key=lambda c: priority.get(c["job"], 99))
|
||||
crew = crew[:12]
|
||||
|
||||
# name -> tmdb person id, for the plex_people foundation (S6 fetches bios off these ids).
|
||||
people: list[dict] = []
|
||||
|
||||
@@ -44,6 +44,7 @@ TV_PAYLOAD = {
|
||||
"id": 1396,
|
||||
"vote_average": 9.0,
|
||||
"vote_count": 12000,
|
||||
"created_by": [{"id": 66633, "name": "Vince Gilligan", "profile_path": "/vg.jpg"}],
|
||||
"keywords": {"results": [{"name": "drugs"}, {"name": "new mexico"}]},
|
||||
"aggregate_credits": {
|
||||
"cast": [{"id": 17419, "name": "Bryan Cranston"}],
|
||||
@@ -102,9 +103,12 @@ def test_parse_tv_uses_aggregate_credits_and_no_money():
|
||||
d = tmdb.parse_tmdb(TV_PAYLOAD, "tv")
|
||||
assert d["budget"] is None and d["revenue"] is None # TV has neither
|
||||
assert d["keywords"] == ["drugs", "new mexico"] # from keywords.results
|
||||
# The showrunner (created_by → Creator) leads; the aggregate-credits EP follows; the Sound person is
|
||||
# filtered; TV jobs are read from jobs[0]. Crew is display-priority ordered (Creator before EP).
|
||||
assert d["crew"] == [
|
||||
{"name": "Vince Gilligan", "job": "Executive Producer", "tmdb_id": 66633, "profile_path": None}
|
||||
] # the Sound person is filtered; job read from jobs[0]
|
||||
{"name": "Vince Gilligan", "job": "Creator", "tmdb_id": 66633, "profile_path": "/vg.jpg"},
|
||||
{"name": "Vince Gilligan", "job": "Executive Producer", "tmdb_id": 66633, "profile_path": None},
|
||||
]
|
||||
assert d["tmdb_rating"] == 9.0
|
||||
assert ("Bryan Cranston", 17419) in {(p["name"], p["tmdb_id"]) for p in d["people"]}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user