"""Shared Jellyfin API helpers for the gallery/run scripts. Consolidates the jf_get/find_item_id/fetch_cast_person_ids helpers that were previously copy-pasted across filter_gallery.py, make_jellyfin_gallery.py and run_from_jellyfin.py, plus the small gallery-id and URL-normalisation helpers. """ import sys import requests def jf_get(base_url: str, api_key: str, path: str, **params) -> dict: url = base_url.rstrip("/") + path headers = {"X-Emby-Token": api_key, "Accept": "application/json"} r = requests.get(url, params=params, headers=headers, timeout=30) r.raise_for_status() return r.json() def normalize_jellyfin_url(url: str) -> str: """Strip any query string and a trailing /Items from a base URL. Defends against a base URL that accidentally includes an API path, e.g. "https://host/Items?" — so we don't build double-nested, query-mangled URLs. """ url = url.split("?", 1)[0].rstrip("/") if url.endswith("/Items"): url = url[: -len("/Items")] return url def actor_jellyfin_id(actor: dict) -> str | None: """Return an actor's Jellyfin person id, tolerating the older key name. current make_jellyfin_gallery.py writes "jellyfin_id"; older galleries used "jellyfin_person_id" (see gallery_store.cpp's fallback for the same pair). """ return actor.get("jellyfin_id") or actor.get("jellyfin_person_id") def find_item_id(base_url: str, api_key: str, title: str, item_types: list[str]) -> str: data = jf_get( base_url, api_key, "/Items", Recursive="true", IncludeItemTypes=",".join(item_types), SearchTerm=title, Limit=10, ) items = data.get("Items", []) if not items: raise ValueError(f"No item found matching title {title!r}") if len(items) > 1: print("Multiple matches found:", file=sys.stderr) for it in items: print(f" {it['Id']} {it.get('Type')} {it.get('Name')} ({it.get('ProductionYear')})", file=sys.stderr) print(f"Using first match: {items[0]['Name']}", file=sys.stderr) return items[0]["Id"] def fetch_episode_info(base_url: str, api_key: str, item_id: str) -> dict | None: """Return episode locator info, or None if the item isn't an Episode. Pulls the season/episode numbers off the Episode item and the series' TMDB/IMDB ProviderIds off the parent Series — TMDB's episode-credits endpoint is keyed by (series id, season, episode). Returns a dict with keys: series_id, season, episode, series_tmdb, series_imdb (any of the id/number values may be None if Jellyfin hasn't populated them). """ data = jf_get(base_url, api_key, "/Items", Ids=item_id, Fields="ParentIndexNumber,IndexNumber", Recursive="true") items = data.get("Items", []) if not items: return None item = items[0] if item.get("Type") != "Episode": return None series_id = item.get("SeriesId") series_tmdb = series_imdb = None if series_id: series_data = jf_get(base_url, api_key, "/Items", Ids=series_id, Fields="ProviderIds", Recursive="true") series_items = series_data.get("Items", []) if series_items: providers = series_items[0].get("ProviderIds", {}) series_tmdb = providers.get("Tmdb") series_imdb = providers.get("Imdb") return { "series_id": series_id, "season": item.get("ParentIndexNumber"), "episode": item.get("IndexNumber"), "series_tmdb": series_tmdb, "series_imdb": series_imdb, } def fetch_cast_person_ids(base_url: str, api_key: str, item_id: str) -> set[str]: # /Items/{id} (no user context) returns 400 on recent Jellyfin servers; # /Items?Ids=... works with an API key alone. data = jf_get(base_url, api_key, "/Items", Ids=item_id, Fields="People", Recursive="true") items = data.get("Items", []) if not items: raise ValueError(f"Item {item_id} not found") item = items[0] cast_ids = {p["Id"] for p in item.get("People", []) if p.get("Type") == "Actor"} # Episodes generally only carry their own guest stars in "People" — the # regular/recurring cast lives on the parent Series item, so pull that # in too or the filtered gallery ends up missing the main cast. series_id = item.get("SeriesId") if series_id: series_data = jf_get(base_url, api_key, "/Items", Ids=series_id, Fields="People", Recursive="true") series_items = series_data.get("Items", []) if series_items: cast_ids |= {p["Id"] for p in series_items[0].get("People", []) if p.get("Type") == "Actor"} return cast_ids