Files
scene-actor-extraction/scripts/sae_tmdb.py
dtourolle 152c34b1f4 refactor(scripts): extract shared sae_* helpers and dedupe gallery builders
Consolidate copy-pasted logic across the gallery/run scripts into shared
modules:
  - sae_env.py     — zero-dependency .env loader (populates os.environ)
  - sae_tmdb.py    — TMDB API helpers (tmdb_get, person images, id lookups)
  - sae_jellyfin.py— Jellyfin API helpers (jf_get, id/URL normalisation)
  - sae_gallery.py — image download + gallery.json writing

make_gallery, make_jellyfin_gallery and filter_gallery now import these
instead of carrying their own near-identical copies.
2026-07-04 18:55:37 +02:00

89 lines
3.8 KiB
Python

"""Shared TMDB API helpers for the gallery builder scripts.
Consolidates the tmdb_get/tmdb_person_images/tmdb_person_* helpers that were
byte-for-byte identical in make_gallery.py and make_jellyfin_gallery.py.
"""
import requests
TMDB_BASE = "https://api.themoviedb.org/3"
TMDB_IMG = "https://image.tmdb.org/t/p/original"
def tmdb_get(path: str, token: str, **params) -> dict:
url = TMDB_BASE + path
# TMDB accepts two credential styles. A v4 "API Read Access Token" is a JWT,
# which always starts with "eyJ" (base64 of '{"...'); it goes in an
# Authorization: Bearer header. A v3 API key is an opaque hex string and goes
# in the api_key query param instead. We auto-detect by that "eyJ" prefix.
if token.startswith("eyJ"):
headers = {"Authorization": f"Bearer {token}", "Accept": "application/json"}
r = requests.get(url, params=params, headers=headers, timeout=10)
else:
params["api_key"] = token
r = requests.get(url, params=params, headers={"Accept": "application/json"}, timeout=10)
r.raise_for_status()
return r.json()
def tmdb_person_images(tmdb_person_id: str, token: str) -> list[str]:
images_data = tmdb_get(f"/person/{tmdb_person_id}/images", token)
return [TMDB_IMG + p["file_path"] for p in images_data.get("profiles", []) if p.get("file_path")]
def tmdb_person_for_imdb(imdb_id: str, token: str) -> tuple[str | None, list[str]]:
"""Return (tmdb_person_id, profile_image_urls) for the TMDB person matching this IMDB person id."""
data = tmdb_get(f"/find/{imdb_id}", token, external_source="imdb_id")
people = data.get("person_results", [])
if not people:
return None, []
tmdb_person_id = str(people[0]["id"])
return tmdb_person_id, tmdb_person_images(tmdb_person_id, token)
def tmdb_person_by_name(name: str, token: str) -> tuple[str | None, list[str]]:
"""Return (tmdb_person_id, profile_image_urls) for the best name match on TMDB.
Used when Jellyfin has no IMDB ProviderId for this person (the common case —
Jellyfin rarely populates ProviderIds on Person items), so /find/{imdb_id}
isn't an option. /search/person is sorted by popularity; take the top hit.
"""
data = tmdb_get("/search/person", token, query=name)
people = data.get("results", [])
if not people:
return None, []
tmdb_person_id = str(people[0]["id"])
return tmdb_person_id, tmdb_person_images(tmdb_person_id, token)
def tmdb_id_from_imdb(imdb_id: str, token: str) -> int:
data = tmdb_get(f"/find/{imdb_id}", token, external_source="imdb_id")
results = data.get("movie_results", [])
if not results:
raise ValueError(f"No TMDB movie found for IMDB ID {imdb_id}")
return results[0]["id"]
def tmdb_tv_id_from_imdb(imdb_id: str, token: str) -> int | None:
"""Return the TMDB TV series id for an IMDB series id, or None if not found."""
data = tmdb_get(f"/find/{imdb_id}", token, external_source="imdb_id")
results = data.get("tv_results", [])
return results[0]["id"] if results else None
def tmdb_episode_cast(tv_id: int | str, season: int, episode: int, token: str) -> list[dict]:
"""Return the per-episode cast for one episode (cast + guest_stars).
Uses /tv/{id}/season/{s}/episode/{e}/credits, which is the episode-level
roster — strictly tighter than the whole-series cast Jellyfin exposes.
Each dict carries at least "id" (TMDB person id) and "name".
"""
data = tmdb_get(f"/tv/{tv_id}/season/{season}/episode/{episode}/credits", token)
# De-dupe by TMDB person id — a name can appear in both lists.
by_id: dict = {}
for person in data.get("cast", []) + data.get("guest_stars", []):
pid = person.get("id")
if pid is not None and pid not in by_id:
by_id[pid] = person
return list(by_id.values())