"""Parse the "info page" rich metadata out of a full Plex metadata dict, and persist the subset that the detail endpoints serve from the DB row. Shared by the live info-page path (routes.plex.browse) and the background rich re-sync (plex.sync) so the two never diverge — the same one-parser rule that keeps `people.py`'s cast handling consistent. `rich_detail` is the pure parser; `apply_rich_detail` writes the rich-ONLY fields onto a movie/show row (genres/directors/studio/content_rating already land from the cheap listing at `_apply_facet_fields`).""" from app.plex import people from app.plex.plaintext import plain_text def rich_detail(meta: dict) -> dict: """Pull the info-page extras out of a full Plex metadata dict: an IMDb score + link, content rating, genres, director(s), studio, tagline, and cast (name/role/photo). Best-effort — any missing field is just omitted.""" # Rating: IMDb-ONLY — the explicit `imdb://` entry in the Rating array, or NULL. We deliberately do # NOT fall back to audienceRating/rating here: that is a Plex audience score, not an IMDb score, and # labelling it "imdb_rating" mislabels it. The audience score is served separately (item/show detail # expose the `rating` column) so the UI can show it when there's no IMDb score. imdb_rating = None for r in meta.get("Rating") or []: if str(r.get("image") or "").startswith("imdb://"): try: imdb_rating = round(float(r.get("value")), 1) # silent-ok: no IMDb score, which is the common and legitimate case — the UI has a # separate audience score to fall back on and shows nothing if neither exists. except (TypeError, ValueError): pass break # IMDb id (tt…) from the Guid list → a clickable link. imdb_id = None for g in meta.get("Guid") or []: gid = str(g.get("id") or "") if gid.startswith("imdb://"): imdb_id = gid.split("imdb://", 1)[1] break return { "imdb_rating": imdb_rating, "imdb_id": imdb_id, "imdb_url": f"https://www.imdb.com/title/{imdb_id}/" if imdb_id else None, "content_rating": meta.get("contentRating"), "genres": [g.get("tag") for g in (meta.get("Genre") or []) if g.get("tag")][:8], "directors": [d.get("tag") for d in (meta.get("Director") or []) if d.get("tag")][:4], "studio": meta.get("studio"), "tagline": plain_text(meta.get("tagline")), "cast": people.cast_from_meta(meta), } def apply_rich_detail(row, meta: dict) -> None: """Persist the rich-ONLY detail fields onto a movie/show row so the info page serves them from the DB instead of a live Plex call. Call wherever a rich metadata dict is fetched (the cast backfill + the info-page lazy enrich). Empty cast persists as SQL NULL (none_as_null), so an EN fallback and the `full_cast IS NULL` "not yet enriched" signal stay meaningful.""" d = rich_detail(meta) row.imdb_rating = d["imdb_rating"] row.imdb_id = d["imdb_id"] row.tagline = d["tagline"] row.full_cast = d["cast"] or None