diff --git a/backend/app/plex/sync.py b/backend/app/plex/sync.py index b4dd20fa..acdb5e5a 100644 --- a/backend/app/plex/sync.py +++ b/backend/app/plex/sync.py @@ -304,7 +304,12 @@ def _apply_facet_fields(row, meta: dict) -> None: row.originally_available_at = _date(meta.get("originallyAvailableAt")) row.genres = _tags(meta, "Genre") row.directors = _tags(meta, "Director") - row.cast_names = _tags(meta, "Role", limit=20) + # The listing carries only the top ~20 cast, so re-applying it would DOWNGRADE an already-enriched + # full cast (up to 24, grown by the rich backfill/info-page enrich) — and since a sync never clears + # `people_enriched_at`, nothing would regrow it, silently breaking the person-page cast filter for + # names past the top ~20. Only take the listing's cast before the row is enriched; keep it after. + if row.people_enriched_at is None: + row.cast_names = _tags(meta, "Role", limit=20) people = list(dict.fromkeys((row.directors or []) + (row.cast_names or []))) row.people_text = " ".join(people) or None diff --git a/backend/tests/test_plex_detail.py b/backend/tests/test_plex_detail.py index 481ebee8..cf471222 100644 --- a/backend/tests/test_plex_detail.py +++ b/backend/tests/test_plex_detail.py @@ -20,7 +20,7 @@ from app.db import get_db from app.main import app from app.models import PlexItem, PlexLibrary, PlexShow, User from app.plex import detail -from app.plex.sync import _enrich_people_batch +from app.plex.sync import _apply_facet_fields, _enrich_people_batch # A full Plex metadata dict: an explicit IMDb Rating entry (must win over audienceRating), an IMDb Guid, # a tagline, and a two-person cast (one with a CDN headshot → proxied, one without → thumb None). @@ -101,6 +101,31 @@ class _StubClient: return self._meta +def test_apply_facet_fields_preserves_enriched_cast_names(): + """A full sync must NOT downgrade an already-enriched full cast back to the listing top-20 — since + sync never clears people_enriched_at, nothing would regrow it and the person-page filter would lose + the names past the listing cap (regression guard for E5 S5b's enrich gating).""" + # The cheap listing's top few cast + the (always-present) director. + thin = {"Role": [{"tag": "Al Pacino"}, {"tag": "James Caan"}], "Director": [{"tag": "Francis Ford Coppola"}]} + + # Not-yet-enriched row: the listing cast IS applied. + fresh = SimpleNamespace(people_enriched_at=None, directors=None, cast_names=None) + _apply_facet_fields(fresh, thin) + assert fresh.cast_names == ["Al Pacino", "James Caan"] + + # Enriched row: the fuller stored cast is KEPT, not overwritten by the thin listing. + from datetime import datetime, timezone + enriched = SimpleNamespace( + people_enriched_at=datetime.now(timezone.utc), + directors=["Francis Ford Coppola"], + cast_names=["Marlon Brando", "Al Pacino", "James Caan", "Diane Keaton"], + ) + _apply_facet_fields(enriched, thin) + assert enriched.cast_names == ["Marlon Brando", "Al Pacino", "James Caan", "Diane Keaton"] + # people_text is rebuilt from the (updated) directors + the PRESERVED full cast. + assert "Diane Keaton" in enriched.people_text and "Francis Ford Coppola" in enriched.people_text + + def test_enrich_people_batch_persists_detail(db): lib = PlexLibrary(plex_key="1", title="Movies", kind="movie", enabled=True) db.add(lib) @@ -251,3 +276,26 @@ def test_show_detail_serves_from_row(db, monkeypatch): assert body["imdb_url"] == "https://www.imdb.com/title/tt0903747/" assert [c["name"] for c in body["cast"]] == ["Bryan Cranston"] assert resp.json()["related"] == [] + + +def test_show_detail_lazy_fills_when_unenriched(db, monkeypatch): + """A not-yet-enriched show is filled from the live meta on first view (imdb/full_cast persisted + + stamped) so subsequent views serve from the row.""" + lib = PlexLibrary(plex_key="2", title="Shows", kind="show", enabled=True) + db.add(lib) + db.flush() + show = PlexShow(rating_key="s1", library_id=lib.id, title="A Show") + db.add(show) + db.commit() + user = _user(db, "showfill@example.com") + db.commit() + + # _show_enrichment returns (live meta, no related) → the fill branch runs. + monkeypatch.setattr(browse, "_show_enrichment", lambda db_, rk: (RICH_META, [])) + resp = _get(db, user, monkeypatch, "/api/plex/show/s1") + assert resp.status_code == 200, resp.text + assert resp.json()["show"]["imdb_id"] == "tt0068646" + db.refresh(show) + assert show.imdb_id == "tt0068646" # persisted for next time + assert show.people_enriched_at is not None + assert [c["name"] for c in show.full_cast] == ["Marlon Brando", "Al Pacino"]