fix(plex,audit): E30 review round 1 — the people job keeps going, HU genres everywhere, results as data

- people_enrich: a failed TMDB lookup or a dead headshot is skipped, not the end of the run (a dead
  file is marked and left for a week; three failures in a row end it); portraits wait for the Plex
  module switch; a no-Plex original keeps its own cache key, so the resize still replaces it; the job
  entry lives in the plex domain (run_people_enrich)
- person-image takes only a headshot-shaped CDN url (no query variants of one file)
- every synced row gets Hungarian genres, and a HU genre filter matches the word as picked too;
  migration 0094 fills the rows no overlay reached
- a job run's audit row carries its result as data; audit_clear / demo_reset store their counts
- one credit-column list for the prune and the enrichment order; _overlay_pass typed
- tests: batch skip + stop, HU bio stored, dead headshots, module switch, the person-image route,
  the job body, genre filter + facet write + twin fallback, tagline/summary fallbacks, audit data,
  the reader's alphabet at every remaining collated() call site
This commit is contained in:
2026-09-30 03:26:57 +02:00
parent 59e6cc027d
commit ab963c81d2
15 changed files with 571 additions and 118 deletions
+10 -20
View File
@@ -18,6 +18,7 @@ from app.downloads.gc import run_download_gc
from app.models import SchedulerSetting, User
from app.plex.sync import sync as run_plex_sync
from app.plex.tmdb import run_enrich as run_tmdb_enrich
from app.plex.tmdb import run_people_enrich
from app.plex.watch_sync import run_plex_watch_reconcile, run_plex_watch_sync
from app.notifications import create_notification
from app.state import is_sync_paused
@@ -160,6 +161,9 @@ def _audit_run(db, actor_id, job_id, status, summary, result) -> None:
they changed something, so routine no-op polls don't spam the trail. Best-effort."""
if actor_id is None and status == "ok" and not _run_changed(result):
return
# The result as data too (E30): the audit log names it in the reader's language, as the inbox
# does; `summary` stays the English line of the row's own summary column.
data = _result_data(result)
try:
audit.record(
db,
@@ -168,7 +172,7 @@ def _audit_run(db, actor_id, job_id, status, summary, result) -> None:
target_type="job",
target_id=job_id,
summary=f"{job_id}: {status}" + (f" — {summary}" if summary else ""),
after={"status": status, "summary": summary},
after={"status": status, "summary": summary, **({"result": data} if data else {})},
)
db.commit()
except Exception:
@@ -205,7 +209,7 @@ def _job(name: str, fn) -> None:
)
if actor_id is not None:
_notify_done(db, actor_id, name, "skipped", "sync paused", _PAUSED)
_audit_run(db, actor_id, name, "skipped", "sync paused", None)
_audit_run(db, actor_id, name, "skipped", "sync paused", _PAUSED)
return
with progress.bind(sink):
result = fn(db)
@@ -252,7 +256,7 @@ _PAUSED = {"skipped": "sync paused"}
def _result_data(result) -> dict | None:
"""A job's result as the client names it in the reader's language (E30): the dict itself (counts,
a `skipped` / `reason` word, a nested dict per phase), `enrich`'s bare count as one. The one-line
`_summarize` is the English fallback — and what the audit log keeps."""
`_summarize` is the English fallback."""
if isinstance(result, dict):
return result or None
if isinstance(result, int) and not isinstance(result, bool):
@@ -410,23 +414,9 @@ def _tmdb_enrich_job() -> None:
def _people_enrich_job() -> None:
# Make the people local (E30 S6): a batch of person bios from TMDB (the most-credited first) and a
# batch of portraits resized through Plex into the image cache, so a person page and a cast strip
# never wait on an outside service. Each half is a no-op without its key / without Plex.
def work(db):
from app import sysconfig
from app.plex import people as plex_people
from app.plex import tmdb as plex_tmdb
batch = settings.people_enrich_batch
if sysconfig.get_str(db, "tmdb_api_key"):
with plex_tmdb.TmdbClient(db) as tmdb:
bios = plex_tmdb.enrich_people_batch(db, tmdb, batch)
else:
bios = {"skipped": "tmdb_api_key not set"}
return {"bios": bios, "portraits": plex_people.warm_portraits(db, batch)}
_job("people_enrich", work)
# Make the people local (E30 S6): a batch of person bios from TMDB and a batch of portraits resized
# through Plex, so a person page and a cast strip never wait on an outside service.
_job("people_enrich", run_people_enrich)
def _audit_gc_job() -> None: