fix(plex): never shrink cast_names on sync so a watermark reset can't regress it (B-sub-2)

_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.
This commit is contained in:
2026-08-18 00:21:59 +02:00
parent 367bcebc9f
commit f39c5bb25c
+10 -4
View File
@@ -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