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.
Completes the plex-package 1.x->2.0 conversion, behaviour-equivalent (full
suite 253 passed, 2 skipped; the characterization net locks every path):
- routes/plex/browse.py (61 sites): libraries, facets (the wsq builder +
jsonb genre unnest), collections, playlists (+ bulk add/remove/reorder),
seasons, people/cards, collection strips, episode-nav, item detail, and
the unified_library union builder.
- plex/rollup.py: agg_subquery now returns a select(); state_map executes it
(the atomic unit coupled to unified_library via .subquery()).
Query builders -> select(); .filter/.filter_by -> .where; terminals ->
db.execute(...).scalars()/.all()/db.scalar(...); bulk .delete()/.update() ->
execute(delete()/update()). Zero db.query()/.filter_by remain in the package.
Cleanup only, no behaviour change (ruff clean + pytest 193):
- imports hoisted above `router =` and grouped/sorted (isort) across the split
files; ruff-format pass restores the 2-blank-line PEP8 spacing the generator
collapsed to 1.
- the shared on-disk cache root moves to a neutral app/plex/cache.py, so
subtitles.py no longer imports it from imgcache.py (drops the cross-import).
- the former plex.py module docstring's access-model note (read endpoints open
to any authed user; admin endpoints test/sync) is restored in the package
__init__.py.
R12 S1 step A. Move the pure, leaf-level helpers out of the 2227-line
routes/plex.py into the app/plex service package with public names:
- app/plex/imgcache.py — on-disk poster/art cache + proxy (was _img_cache_*,
_image_key, _img_response, _IMG_CACHE/_IMG_SIZES). Owns the shared cache dir.
- app/plex/subtitles.py — WebVTT convert/time-shift + subtitle cache (was
_srt_to_vtt, _shift_vtt, _ffmpeg_to_vtt, _sub_cache_*, _BITMAP_SUBS).
- app/plex/rollup.py — show watch-state aggregation (was _rollup,
_show_status, _show_agg_subq, _show_state_map).
Behaviour-equivalent move: bodies verbatim, only the public API renamed and
call sites re-qualified. No endpoint or SQL change (db.query kept as-is; the
2.0 select() conversion is a later step). plex.py 2227 -> 2011 lines.
Gate: ruff clean, pytest 193 passed (pure-logic + DB lane).