Commit graph

10 commits

Author SHA1 Message Date
BoulderBadgeDad
b50f7c12f4 video: promote OMDb to a full 3rd enrichment worker (parity with TMDB/TVDB)
OMDb now has the same setup as TMDB/TVDB: a yellow dashboard orb (★ glyph) that
spins/idles in the worker-orb animation, an entry in Manage Workers (Ratings
coverage cards, pause/resume, retry, search), and a BACKGROUND ratings pass.

- Worker 'ratings mode' (is_ratings): instead of a match queue it pulls
  ratings_next() (library items with an imdb_id and ratings_synced=0), fetches
  IMDb/RT/Metacritic, applies + marks synced. So the whole library gets ratings,
  not just titles you open (schema v7: ratings_synced).
- enrichment_breakdown/unmatched/retry get an 'omdb' branch (coverage =
  ratings-filled, not matched). build_clients includes omdb; the lazy on-view
  backfill uses the omdb worker's client.
- Dashboard orb + Manage Workers entry (★ glyph fallback where there's no logo),
  yellow accent.

Seam tests: omdb worker rates the queue (ratings mode), ratings breakdown.
2026-06-14 23:07:35 -07:00
BoulderBadgeDad
f06728b0a7 video detail: IMDb / Rotten Tomatoes / Metacritic ratings (OMDb)
Next-level: real critic/audience scores beyond the TMDB star. OMDb (free key,
keyed by the imdb_id we already capture) returns IMDb / RT / Metacritic.

- OMDBClient (ratings + test); built as a non-worker 'ratings_client' on the
  engine. _backfill_ratings runs in both lazy detail refreshes (overwrites, since
  ratings are dynamic). schema v6: imdb_rating / rt_rating / metacritic on
  movies + shows; show/movie payloads return them.
- Billboard renders branded rating badges (IMDb yellow, RT tomato/splat by
  fresh/rotten, Metacritic green/yellow/red by score). Lazy refresh also triggers
  when an imdb_id exists but ratings are missing.
- OMDb API-key frame in Settings (parity with TMDB/TVDB) + config GET/POST +
  /enrichment/omdb/test.

Seam tests: OMDb parse, engine ratings backfill, apply_ratings + payload, config
includes omdb.
2026-06-14 22:54:53 -07:00
BoulderBadgeDad
53391372c3 video: fix detail reload (music router clobber) + reliably show missing episodes
Reload bug: music's router boots first, rewrites an unknown /video-detail/... URL
to /dashboard, and my init read the already-changed URL (no restore) AND dispatched
open-detail before video-detail.js was listening (empty page + stray back button).
Fix: capture the path at SCRIPT-EVAL time (before music boots) and DEFER the
restore to a macrotask so every DOMContentLoaded handler is registered and music's
initial routing has run — then re-assert the real URL. Reload/deep-link now restore
the exact item.

Missing-episodes bug: the full-episode-list cascade only ran via the lazy refresh,
which was gated on ART being missing — so a show that already had posters/logo
never pulled its episode list (stayed owned-only). Added shows.episodes_synced
(schema v5): the worker sets it after a full cascade; show_detail returns it; the
lazy refresh now triggers when NOT synced, so owned + missing episodes populate.
2026-06-14 22:26:37 -07:00
BoulderBadgeDad
efa0632883 video detail: clearlogo hero (TMDB images — no new key)
Phase 3: the stylized transparent title logo replaces the text title in the
billboard (the big Plex/Netflix 'premium' jump). Sourced from TMDB images
(append_to_response=images, include_image_language=en,null) in the same detail
call — no Fanart key needed.

- schema v4: movies.logo_url / shows.logo_url (idempotent migration).
- TMDB client picks an English logo (then language-neutral, then any); enrichment
  backfills logo_url gap-only; show/movie payloads return 'logo'.
- Billboard shows the logo img (with graceful fallback to the text title on error
  / when absent; title kept visually-hidden for a11y). Lazy on-view refresh now
  also triggers when the logo is missing, so existing libraries fill it in.

Seam tests: English-logo pick, backfill + payload, schema.
2026-06-14 21:36:14 -07:00
BoulderBadgeDad
b9b3b9eed3 video 'capture everything': cast & crew (people + credits) + cast row UI
Last capture piece, schema v3 (new people + credits tables; CREATE IF NOT EXISTS
migrates existing DBs on restart, no wipe):
- people deduped by tmdb_id; credits link to exactly one movie OR show (separate
  nullable FKs + CHECK, no polymorphic id) with department/job/character/order.
- TMDB client appends credits to the detail call (free) and parses cast (name,
  character, photo, billing order) + headline crew (directors/writers/creators).
- enrichment_apply backfills cast/crew gap-only (never clobbers); show/movie
  detail return cast + crew. Populates on view via the existing lazy refresh-art.
- Cast & Crew section on the detail page: grouped crew line + a horizontal
  cast row with circular TMDB headshots, names, characters (accent hover).

Seam tests: TMDB credit parse (+ job filtering, created_by), backfill + people
dedup across titles, gap-only no-clobber, payload shape.
2026-06-14 21:20:08 -07:00
BoulderBadgeDad
e1e0e29432 video 'capture everything' (phase 1): stills, genres, ratings, tagline
Captures the richer metadata the media server already exposes (schema v2;
idempotent migrations + CREATE IF NOT EXISTS, so an existing DB upgrades on
restart with no wipe):
- movies: tagline, rating (audience), rating_critic; shows: tagline, rating,
  first/last air date; episodes: still_url + rating.
- Genres as a normalised many-to-many (genres + movie_genres/show_genres link
  tables — no comma-blob), deduped, replace-on-upsert.
- Plex (.genres/.tagline/.audienceRating/.rating/.thumb) + Jellyfin (Genres/
  Taglines/CommunityRating/CriticRating/Premiere+EndDate/episode Primary) both
  extract them; episode stills served via /api/video/poster/episode/<id>.
- Detail payloads return genres/tagline/rating/air-dates + per-episode has_still;
  the billboard shows a tagline, ★ score, genre chips, and episode rows render
  REAL stills (no more orange placeholder once scanned).

Seam tests for genre dedup/replace, show+episode capture, episode still ref.
Cast/crew (people + credits) is the next phase.
2026-06-14 17:17:20 -07:00
BoulderBadgeDad
093e14bd5d video enrichment 1a: DB layer (match-status cols + migration + helpers)
Foundation for the video enrichment workers, mirroring music's per-source
columns/queries on video.db.
- Schema: tmdb_match_status/tmdb_last_attempted on movies; tmdb_+tvdb_ on shows.
  Idempotent ALTER-TABLE migration adds them to existing DBs on init.
- VideoDatabase helpers (service+kind -> columns map):
  enrichment_next (pending first, then not_found past retry window),
  enrichment_apply (sets id/status/last_attempted + whitelisted metadata,
  backfill-safe), enrichment_breakdown, enrichment_unmatched (paged), and
  enrichment_retry. Same shape as music's enrichment API so the shared modal can
  drive it. 30 DB tests green.
2026-06-14 11:18:46 -07:00
BoulderBadgeDad
13e03a624c video scan: capture the provider IDs the server already has (tmdb/imdb/tvdb)
The servers already matched everything to their agents — we were dropping the
IDs. Now we store them:
- Plex: parse item.guids (imdb://, tmdb://, tvdb://); Jellyfin: parse
  item.ProviderIds (added ProviderIds to the requested Fields).
- Stored on movies (tmdb_id, imdb_id), shows (tvdb_id, tmdb_id, imdb_id), and
  episodes (tvdb_id) via the upserts.
- Dropped the over-strict UNIQUE on movies.tmdb_id / shows.tvdb_id (same title
  can legitimately live in two libraries; we dedupe on server_id). Scanner now
  wraps each upsert in try/except so one bad item can't abort a scan.
Tests: guid/ProviderIds parsing + IDs persisted. 38 video-DB/scanner tests green.
2026-06-14 11:04:54 -07:00
BoulderBadgeDad
462fa50423 video DB: server-sourced scan upserts (movies/shows/seasons/episodes)
Server (Plex/Jellyfin) is the source of truth, so every scanned row carries
(server_source, server_id) for upsert + stale-removal — mirroring how music
keys on server_source + ratingKey.

- schema: server_source/server_id columns on movies/shows/episodes (+ server_id
  on seasons); unique (server_source,server_id) on movies/shows (multiple NULLs
  allowed so wishlist rows never block).
- VideoDatabase.upsert_movie / upsert_show_tree: take normalized, server-
  agnostic dicts (a Plex/Jellyfin adapter produces them — DB never touches a
  media SDK), set has_file + media_files, and prune episodes/seasons the server
  no longer reports.
- prune_missing(): removes top-level movies/shows the scan didn't see (cascades
  clean children).
6 new tests (insert/update/file-replace, season/episode build+prune, top-level
prune); 18 video-DB tests green.
2026-06-13 23:02:03 -07:00
BoulderBadgeDad
402a1fec50 video side: video.db schema + isolated VideoDatabase
Separate SQLite file (database/video_library.db, env VIDEO_DATABASE_PATH),
fully disconnected from music — never imported by music, imports nothing from
music, no shared write lock.

Schema (database/video_schema.sql), designed to dodge the music DB's known
pain points:
- movies; shows->seasons->episodes; channels->channel_videos (YouTube as a
  first-class peer); media_files (the library); downloads (queue+history);
  activity feed; root_folders / quality_profiles / video_settings config.
- No polymorphic ids: media_files/downloads use separate nullable FKs + a CHECK
  that exactly one owner is set; real cascades.
- Explicit external-id columns (tmdb/tvdb/imdb/youtube), no source-id blob.
- Watchlist/Wishlist/Calendar are DERIVED VIEWS over monitored + file state
  (single source of truth, can't drift like music's wishlist table did).

VideoDatabase mirrors music's conventions (WAL, foreign_keys ON, 30s busy
timeout, Row factory, once-per-process init, user_version backstop) but is an
independent implementation. 13 seam tests: schema builds, CHECK constraints
reject bad rows, cascades fire, views return correct membership, KV roundtrips,
and a guard that the module imports nothing from music.
2026-06-13 22:30:55 -07:00