Follow-ups from the epic-end /code-review of the E5 branch:
- tmdb.resolve_recs + people._resolve_known_for: match recommendations/
credits to the library bucketed by media_type. TMDB movie ids and tv ids
are independent sequences, so a bare `tmdb:<id>` match could deep-link a
movie rec to an unrelated show (or vice-versa) on a numeric collision.
Regression test added (test_resolve_recs_matches_by_media_type_not_bare_id).
- routes/search: wrap the _ingest_candidates call so a DB/enrich failure
degrades to a 422 with a message instead of a bare 500 hidden behind the
FE's generic "search failed" (extends the B-core-1 rationale); rollback on
failure. Stamp enriched_at with its own timestamp taken after the enrich,
not the stub-insert placeholder.
- routes/plex/discover: key serialized cards back by rating_key (globally
unique in Plex) instead of id(obj); fix the stale rows 3-tuple annotation.
A dedicated Plex "Discover" landing — a stack of horizontal Carousel rails
served from ONE consolidated read (serve-from-DB, no live Plex/TMDB call):
- GET /api/plex/discover → continue_watching (in-progress movies + shows, most
recently progressed) · recently_added · genres (top-N tiles) ·
because_you_watched (recs of the most-recent watched, TMDB-enriched movie,
resolved to library rows by guid via serve_block's resolver).
- GET /api/plex/surprise → a fresh random, scope-bounded sample for the ↻ rail.
Frontend: PlexDiscover.tsx (rails reuse the S1 Carousel + S3 PlexPosterCard;
colored genre tiles → tap to filter the grid; ↻ reshuffles the surprise rail
alone). Discover is now the root sub-view; the flat grid becomes the bookmarkable
"Browse all" secondary view (?v=browse) reached by a header search/filter (a
browse-intent flip) or the new Discover⟷Browse toggle.
resolve_recs is promoted from _resolve_recs (now cross-module). Tests:
test_plex_discover.py (rail shape/order, continue-watching, because-you-watched
+ null, surprise bounds, scope) and PlexDiscover.dom.test.tsx; plexSubUrl
round-trip updated for discover-root/grid-browse. Backend lane 393, vitest 332.