From f39c5bb25ce00b8dc301862f3c668d09d95f7b19 Mon Sep 17 00:00:00 2001 From: npeter83 Date: Tue, 18 Aug 2026 00:21:59 +0200 Subject: [PATCH] fix(plex): never shrink cast_names on sync so a watermark reset can't regress it (B-sub-2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _apply_facet_fields seeded cast_names from the cheap top-~20 listing only while people_enriched_at was NULL, relying on "a sync never clears the watermark" to protect an enriched fuller cast. Migration 0065 broke that assumption by nulling people_enriched_at on every movie/show row, so the first post-deploy sync downgraded enriched casts back to the listing top-20 — silently dropping person-page cast-filter names for weeks until the backfill caught up (the S5b regression, re-opened by the migration). Guard structurally instead of trusting the watermark: union the listing cast onto whatever is stored, never dropping a name already present. A fresh row still seeds from the listing; a row whose watermark was reset but whose fuller cast survives keeps it. Robust to any future watermark reset, not just 0065. --- backend/app/plex/sync.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/backend/app/plex/sync.py b/backend/app/plex/sync.py index acdb5e5a..7c298920 100644 --- a/backend/app/plex/sync.py +++ b/backend/app/plex/sync.py @@ -305,11 +305,17 @@ def _apply_facet_fields(row, meta: dict) -> None: row.genres = _tags(meta, "Genre") row.directors = _tags(meta, "Director") # 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. + # full cast (up to 24, grown by the rich backfill/info-page enrich), silently breaking the + # person-page cast filter for names past the top ~20. The watermark gate below (`people_enriched_at + # is None`) normally protects this, but it is NOT enough on its own: a migration that resets the + # watermark re-opens the regression (0065 nulled it, which is exactly how the S5b fix got undone in + # prod). So guard STRUCTURALLY too — union the listing cast onto whatever is stored, never dropping a + # name we already have. On a fresh row (empty `cast_names`) this seeds the top ~20; on a row whose + # watermark was reset but whose fuller cast survives, it keeps the fuller cast. Robust to any future + # watermark reset, not just this one. if row.people_enriched_at is None: - row.cast_names = _tags(meta, "Role", limit=20) + merged = list(dict.fromkeys((row.cast_names or []) + (_tags(meta, "Role", limit=20) or []))) + row.cast_names = merged or None people = list(dict.fromkeys((row.directors or []) + (row.cast_names or []))) row.people_text = " ".join(people) or None