feat(plex): full-library cast backfill via a batched rich re-sync (S1b)

Lazy enrichment only covers viewed titles, so an actor's count/results were
partial. Add a background backfill: each plex_sync rich-fetches a bounded batch
(60) of not-yet-enriched movies/shows and persists their FULL cast, stamping
people_enriched_at so each is fetched once — the whole library is covered over
successive runs (migration 0057 adds the marker). Extracted the enrichment into a
shared app/plex/people.py (cast_from_meta / enrich_row_people / prune_orphan_people
+ the person-image host/proxy) so the info-page lazy path and the batch share ONE
implementation — no duplication; the info page also stamps the marker so the batch
skips it. Verified: an 8-item batch grows those movies' cast_names 3->24 and fills
plex_people (32->224); the info-page cast is intact after the _rich_meta refactor.
(Plex cast-explore S1b)
This commit is contained in:
2026-07-19 04:50:45 +02:00
parent 12d17ec7b0
commit af6faf476f
5 changed files with 184 additions and 87 deletions
+4
View File
@@ -1027,6 +1027,8 @@ class PlexShow(Base, TimestampMixin, UpdatedAtMixin):
directors: Mapped[list | None] = mapped_column(JSONB)
cast_names: Mapped[list | None] = mapped_column(JSONB)
people_text: Mapped[str | None] = mapped_column(Text) # cast+directors, folded into search_vector
# Set once the show's FULL cast has been rich-fetched by the backfill; NULL = still the listing top-3.
people_enriched_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
search_vector: Mapped[object | None] = mapped_column(
TSVECTOR,
Computed(
@@ -1118,6 +1120,8 @@ class PlexItem(Base, TimestampMixin, UpdatedAtMixin):
# Cast + director names, space-joined — folded into search_vector (weight B) so the search box
# finds titles by a person's name (and ranks them above summary-only mentions).
people_text: Mapped[str | None] = mapped_column(Text)
# Set once the movie's FULL cast has been rich-fetched by the backfill; NULL = still the listing top-3.
people_enriched_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
search_vector: Mapped[object | None] = mapped_column(
TSVECTOR,
Computed(