refactor(e5): deferred review-tail (backend) — enrich off request path, reuse, tests
Address the deep-review's deferred backend items so E5 is review-complete: - person-enrich OFF the request path (ABUSE finding): person_detail no longer makes the TMDB call inline (even bounded to 8s it sat on the serve thread, un-throttled, demo-reachable — it violated the module's "no live TMDB call at serve time" invariant). It now schedules a FastAPI BackgroundTask (people._enrich_person_bg, fresh session, best-effort) after the response; the first open serves the row as-is and the bio lands by the next open. The inline try/rollback recovery is gone with it. - reuse: a shared tmdb.resolve_library_guids owns the collision-safe guid→library bucketing (movie vs tv ids are independent sequences) that resolve_recs and people._resolve_known_for each hand-duplicated; a new rollup.item_state_map dedups the "PlexState-by-item_id" fetch across the person + discover paths. - test gaps: enrich_batch transient TmdbError leaves the watermark NULL to retry; guid_to_tmdb_id resolves a tvdb source + tv media_type via /find's tv_results; _continue_watching includes a NULL-status started movie and excludes a hidden one; /api/search/youtube returns 422 (not 5xx) when ingest fails (route-level, B-core-1). Backend lane 403 → 413, ruff clean. No migration; serve contracts unchanged.
This commit is contained in:
@@ -142,6 +142,19 @@ def test_guid_to_tmdb_id_direct_find_and_bad():
|
||||
assert tmdb.guid_to_tmdb_id("local://abc", "movie", fake) is None
|
||||
|
||||
|
||||
def test_guid_to_tmdb_id_tvdb_source_and_tv_media_type():
|
||||
# A tvdb guid for a TV item goes through /find and must pick the tv_results list (not movie_results),
|
||||
# and a tvdb source maps to the tvdb_id external_source. The media_type branch is the whole point.
|
||||
fake = FakeTmdb(find_map={"81189": {"movie_results": [], "tv_results": [{"id": 1396}]}})
|
||||
assert tmdb.guid_to_tmdb_id("tvdb:81189", "tv", fake) == 1396
|
||||
# The SAME /find result, asked for a movie, reads the (empty) movie_results → None.
|
||||
assert tmdb.guid_to_tmdb_id("tvdb:81189", "movie", fake) is None
|
||||
# A direct tmdb guid for a tv item is just the int (no /find).
|
||||
assert tmdb.guid_to_tmdb_id("tmdb:1396", "tv", fake) == 1396
|
||||
# An unknown provider (only imdb/tvdb are /find sources) is unresolvable.
|
||||
assert tmdb.guid_to_tmdb_id("plex:abc", "tv", fake) is None
|
||||
|
||||
|
||||
# ---- DB-lane: batch enrich + serve --------------------------------------------------------------
|
||||
|
||||
def _lib(db, key="1", kind="movie"):
|
||||
@@ -245,6 +258,31 @@ def test_run_enrich_noops_without_key(db):
|
||||
assert out["enriched"] == 0 and "skipped" in out
|
||||
|
||||
|
||||
class _RaisingTmdb:
|
||||
"""A client whose fetch raises a transient TmdbError (e.g. a 429), to exercise the retry path."""
|
||||
|
||||
def find(self, external_id, source):
|
||||
return None
|
||||
|
||||
def fetch(self, tmdb_id, media_type):
|
||||
raise tmdb.TmdbError("429 rate limited")
|
||||
|
||||
|
||||
def test_enrich_batch_transient_error_leaves_watermark_null(db):
|
||||
# A transient TmdbError mid-batch must leave the row's tmdb_enriched_at NULL so the next run retries
|
||||
# it (unlike a definitive no-match, which stamps the watermark). The row is NOT counted as processed.
|
||||
lib = _lib(db)
|
||||
mv = PlexItem(rating_key="m1", library_id=lib.id, kind="movie", title="Godfather", guid="tmdb:238")
|
||||
db.add(mv)
|
||||
db.commit()
|
||||
|
||||
n = tmdb.enrich_batch(db, _RaisingTmdb(), 50)
|
||||
assert n == 0 # the transient failure is skipped, not processed
|
||||
db.refresh(mv)
|
||||
assert mv.tmdb_enriched_at is None # left NULL → retried next run
|
||||
assert mv.tmdb_id is None
|
||||
|
||||
|
||||
def test_enrich_batch_skips_shows_without_guid(db):
|
||||
lib = _lib(db, key="2", kind="show")
|
||||
sh = PlexShow(rating_key="s1", library_id=lib.id, title="No Guid Show") # guid None → not selected
|
||||
|
||||
Reference in New Issue
Block a user