"""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())