Commit graph

33 commits

Author SHA1 Message Date
BoulderBadgeDad
877a86c9b0 Video Discover: split Top 10 today into Movies + TV Shows (Netflix-style)
The mixed /trending/all chart is movie-heavy — TV was nearly absent (only
1 of the top 10 was a show). Split it like Netflix: 'Top 10 today' now holds
two ranked rails, Movies and TV Shows, from the dedicated /trending/movie/day
and /trending/tv/day charts (full 10 of each).

- client.trending(window, kind): single-type charts force the kind into
  _disc_map (those endpoints omit media_type).
- engine.trending(window, kind): kind in the cache key.
- /discover/list: key=trending_movies_today / trending_tv_today, both treated
  as single un-paged charts; inherit the same hide-owned + ranked rendering.
- frontend: one 'Top 10 today' group, two ranked shelves titled Movies / TV
  Shows (group header carries the 'Top 10' framing).
2026-06-24 08:00:13 -07:00
BoulderBadgeDad
bbf7abad1b video discover: wire Netflix-class TMDB filters (keywords/studio/network/cast/runtime/cert/release-window + daily trending)
Foundation for the best-in-class discover build. All additive — existing callers unaffected:
- clients.discover() gains keywords (mood/theme), companies (studio), networks (TV), cast/crew
  (people), min/max_runtime, certification (+country), vote_count_min override, and release_window
  ('last_30/90/365', computed date ranges) — each mapped to the right TMDB /discover param + gated
  to movie/tv where TMDB requires.
- engine.discover_filter() threads them through + into the cache key.
- engine.trending(window) adds the real-time 'day' chart (cache keyed by window) for a Top 10 row.
- /discover/list parses the new query params and a key=trending_today shortcut.

py_compile + ruff clean. Frontend rails (Top 10, mood, studio, new&upcoming, quick-watches) next.
2026-06-23 23:45:41 -07:00
BoulderBadgeDad
28fe3d2b0b video discover: preferred-languages filter (keep Bollywood etc. out of general feeds)
The general/curated rails (Popular/Trending/Top Rated + genre/decade) pull TMDB's GLOBAL lists,
flooding feeds with foreign-language titles (Bollywood). Add a multi-language preference:
- _disc_map now carries original_language (+ popularity) on each item.
- discover_languages setting (default 'en'); /discover/list post-filters general/curated rails
  to it (dropping known non-preferred-language titles) and pages deeper to keep rails full.
  Rails with an explicit lang (the dedicated foreign rails) bypass the filter.
- GET/POST /discover/languages to read/set the preference.
- Removed the hardcoded lang=en on general rails (the setting drives it now).
Default 'en' immediately fixes the Bollywood flood; UI to pick languages next.
2026-06-23 00:09:51 -07:00
BoulderBadgeDad
d984895cba video discover: keep foreign titles out of general rails + fix hide-owned sparse rows
Two Discover UX issues:
- Foreign-language titles leaked into the general genre/decade rails. Added an
  original-language filter (with_original_language) through client.discover -> discover_filter
  -> /discover/list (?lang=); the genre/decade/'because you like' rails now pin lang=en, and a
  handful of dedicated foreign rails (Korean/Japanese/Spanish/French/Hindi) house non-English.
- 'Hide owned' + a huge library = nearly-empty rails (a 2-page batch was mostly owned, then
  CSS-hidden to almost nothing). /discover/list now takes hide_owned=1: it drops owned
  server-side and pages DEEPER (up to 8) until a rail has ~24 un-owned. fillShelf passes
  hide_owned when the toggle's on; toggling re-renders the rails (+ personalized rows) instead
  of just CSS-hiding cards.
2026-06-22 23:59:21 -07:00
BoulderBadgeDad
1f1a239486 video discover phase 0: persist TMDB collection (franchise) id per movie
Data layer for the 'complete your collections' gap engine. People/credits/genres are already
normalized + indexed, so the only missing signal was franchise membership:
- movies: + tmdb_collection_id (indexed) + tmdb_collection_name (schema + _COLUMN_MIGRATIONS,
  SCHEMA_VERSION 17->18, _ENRICH_META_COLS whitelist so enrichment_apply backfills them)
- enrichment match() reads belongs_to_collection (a standard movie-detail field, no extra call)
  and writes the id/name into the match metadata.
Additive + backfill-only (COALESCE), nothing existing rewired. (also noqa'd a pre-existing
OMDb S110 in the touched file to keep ruff clean.)
2026-06-22 23:24:42 -07:00
BoulderBadgeDad
2b3ca12310 Discover: streaming-provider filter + infinite scroll + a11y polish
Provider filter (#4):
- client.discover() + engine.discover_filter() take a TMDB provider id and pass
  with_watch_providers + watch_region (engine._region) + flatrate. Browse gets a
  streaming-service chip row (Netflix/Prime/Disney+/Max/Apple TV+/Hulu/Paramount+/
  Peacock); the grid title reflects 'on <service>'.

Infinite scroll (#6):
- Grid paginates via an IntersectionObserver sentinel (600px lookahead) with a
  bottom spinner; the Load more button stays only as a no-IO fallback.

Polish (#7):
- Hero keyboard nav (←/→ when Discover is the visible view, ignoring inputs and
  while the trailer is open); focus-visible rings on chips/segments/cards/arrows.

Note: 'complete-the-franchise' rail (#5) needs a collection_id per movie, which
the schema doesn't store yet — deferred (would need an enrichment pass).

Tests: +1 (provider watch_region params); updated the discover_filter fake for
the new kwargs. Enrichment + API suites: 115 passed.
2026-06-16 14:55:13 -07:00
BoulderBadgeDad
a9ec025706 Discover: 'More like…' rails + in-app hero trailer
More-like rails (real personalization beyond genre):
- db.random_owned_titles() seeds from a few random owned titles (with tmdb_id);
  client.recommendations() + engine.recommendations() (cached + owned-annotated)
  fetch TMDB recs. New /discover/morelike interleaves movie/show seeds (max 3
  rails) and the page prepends them, pre-filled, above the rail stack with a
  gradient 'for you' title.

Hero trailer (cheap big visual win):
- client.video_trailer() (light /videos call, Trailer over Teaser) + engine
  .trailer() (day-cached) + /discover/trailer. Hero gets a 'Trailer' button that
  opens an in-app YouTube lightbox (autoplay, Esc/backdrop close, pauses the
  slideshow) — nothing leaves SoulSync.

Tests: +11 (recs parse/annotate/cache, trailer pick+fallback+cache, random owned
seeds owned-with-tmdb only, /discover/trailer + /discover/morelike endpoints).
Enrichment + API suites: 114 passed.
2026-06-16 14:44:44 -07:00
BoulderBadgeDad
a8ebc9b8f7 Video Discover page: trending hero + lazy genre/decade rails + filter grid
A browse-everything page for TMDB titles you don't own yet.

Backend:
- TMDBClient: discover() (genre/year/decade), curated() (popular / top_rated /
  now_playing / upcoming / on_the_air / airing_today), genres(); shared
  _disc_map() that carries backdrop + overview. trending() now routes through it.
- Engine: discover_curated / discover_filter / genre_list (cached + owned-
  annotated via library_id_for_tmdb) + _stamp_owned helper.
- api/video/discover.py: /discover/hero, /discover/genres, /discover/list
  (curated key | filtered kind+genre+year+decade+sort, paged).

Frontend (video-discover.js + .vdsc-* CSS + subpage markup):
- Cross-fading trending hero slideshow (per-title hue, dots, hover-pause,
  reduced-motion safe, click -> detail).
- Deep stack of Netflix-style rails (curated + every movie genre + decades),
  each lazy-loaded on scroll via IntersectionObserver, arrow scrollers,
  reusing the search card (owned 'In Library' / 'Preview' ribbon + get button).
- Filter bar (kind / genre / decade / sort) flips into a paged 'Load more' grid.
- Cards open detail through the shared soulsync:video-open-detail event.
2026-06-16 13:35:31 -07:00
BoulderBadgeDad
8e00670491 Video Calendar: 7-day week grid with air times + episode modal
A new isolated Calendar page (/api/video/calendar) — every upcoming episode for
your owned shows across a real 7-day week (today first), as art cards sorted by
air time with a per-cell breathing colour glow.

- Air times: enrich shows with TVDB airsTime (new shows.airs_time column +
  migration); cells show + sort by time, streaming (untimed/00:00) = "Anytime".
  One-time background backfill re-queues already-matched shows for the time.
- Click an episode → styled modal (show backdrop hero, episode still/synopsis,
  air date+time, owned badge, genres, "about the show"), with an explicit
  "Open full show page" action instead of navigating on click.
- Isolated: reads only video_library.db, writes nothing to the music side.
2026-06-15 18:28:02 -07:00
BoulderBadgeDad
b2adc63a6a video: where-to-watch region (no longer hardcoded US)
A saved 'Where-to-watch region' picker in Settings → Detail Pages (19 common
regions, default US). The engine reads it for the providers in extras +
tmdb_detail (region in the cache key), and the detail page labels the section
'Where to Watch · <region>' so you know which market you're seeing.
2026-06-15 13:28:17 -07:00
BoulderBadgeDad
b8de46d2ad video: episode detail expand (guest stars + bigger still)
Click any episode (owned or missing) to expand it: a larger still, full overview,
and the episode's guest stars (clickable to the person page). Lazy-loaded per
episode from TMDB by the show's tmdb_id and cached. New client.episode_detail +
engine.episode_extra + /api/video/episode/<show_tmdb>/<season>/<episode>.
2026-06-15 13:20:15 -07:00
BoulderBadgeDad
be54fccc63 video: featured TMDB review on movie/TV detail pages
extras() now returns a featured review (author, rating, snippet, date); the detail
page shows it in a card with a clamped body + Read more/less. In-app (no external
link).
2026-06-15 11:52:07 -07:00
BoulderBadgeDad
df8f6a2acd video: person page — photos gallery + lightbox + 'also known as'
person() now returns profile images (thumb+full) and also_known_as. The person
page shows an 'Also known as' line and a Photos rail that opens the shared
fullscreen lightbox (arrows/Esc/counter).
2026-06-15 11:49:43 -07:00
BoulderBadgeDad
6fcebe7c4b video: detail extras data layer — gallery, all videos, keywords, facts, full cast (+ caching)
One TMDB call (append_to_response) now also returns: image gallery (backdrops +
posters, thumb+full), all YouTube videos (ordered trailer→teaser→clip→…), keyword
tags, facts (budget/revenue/language/country), and the FULL cast (tv via
aggregate_credits with per-actor episode counts). Shared by item_extras (owned)
and full_detail/tmdb_detail (preview).

Caching: the engine memoizes the live TMDB extras + preview payloads (30-min TTL)
so re-opening a title is instant instead of re-hitting TMDB. Tests added.
2026-06-15 11:17:33 -07:00
BoulderBadgeDad
8441ece6f0 video: person page — sort, department filter, age (best-in-class filmography)
- Sort dropdown: Newest / Oldest / Most popular.
- Department filter (Acting / Directing / Writing / …) for multi-hyphenates —
  only appears when a person has 2+ departments. Composes with the existing
  kind + ownership filters; every chip shows a CONTEXTUAL count (what you'd get
  if you clicked it, given the other active filters).
- Age in the hero meta ('47 years old', or lifespan + 'aged N' for the deceased).
- Backend: each person credit now carries its department (cast=Acting,
  crew=its TMDB department). Seam test added. 249 video-suite tests pass.
2026-06-15 09:57:39 -07:00
BoulderBadgeDad
f725235f44 video: detail extras now include collections, recommendations + next-episode
Backend groundwork for the best-in-class detail pages:
- Movies: belongs_to_collection → the franchise's films (2nd /collection call,
  release-ordered).
- Recommendations (better-curated than 'similar') alongside similar.
- TV: next_episode_to_air / last_episode_to_air stubs (season/ep/name/air date).
Shared by item_extras (owned) + full_detail/tmdb_detail (preview). Tests cover
recommendations+collection ordering and next-episode parsing.
2026-06-15 09:45:06 -07:00
BoulderBadgeDad
38d48bae37 video: OMDb Test button shows the real reason (not just 'HTTP 401')
OMDb returns a JSON body even on 401, so surface its actual Error: 'Invalid API
key!' (nudges the user to click OMDb's activation email) vs 'Request limit
reached!' (free-tier daily quota). The worker's pause note uses the same message.
2026-06-15 09:29:09 -07:00
BoulderBadgeDad
d0c8aa9338 video: OMDb worker survives a bad/expired API key gracefully
The log flood you saw was the OMDb worker hitting a 401 (invalid key) on every
owned title: it logged a full traceback per item AND marked each one
ratings_synced=1 — which would've stopped them ever retrying once the key was
fixed. Root-cause fixes:

- OMDBClient.ratings raises a distinct OMDbAuthError on 401 / 'Invalid API key!'
  (vs a transient error vs a genuine no-data 200).
- Worker: on an auth error it PAUSES (transient, not persisted) with a reason
  note + one warning, instead of churning the whole library; the item is NOT
  marked synced. Transient errors no longer burn items either — they back off and
  pause after 3 in a row. Only a genuine 'no data' marks an item synced. Warnings
  are concise (no per-item tracebacks). get_stats exposes the pause 'note'.
- Fixing the key auto-recovers: saving a new/changed OMDb key re-queues every
  still-unrated title (resets the wrongly-burned ones), and the engine rebuild
  un-pauses the worker.

Seam tests: bad-key pause-without-burn, transient keep-item, ratings() raises on
401, key-change re-queues unrated. 227 video-suite tests pass.
2026-06-15 09:20:00 -07:00
BoulderBadgeDad
d6d0dc537c video: smart multi-layer back button + next-level search/person
Smart back (mirrors music's artist-detail): the top-left back button now
remembers where you actually came from, many layers deep. It keeps an origin
stack ({page} or {detail title}) and stamps each history entry with its layer
depth, so:
- the label is dynamic — '← Back to Search' / '← Back to The Bear' / '← Back to
  <person>' — instead of a hardcoded 'Library'/'Back';
- backing out of the first layer returns to the page you started from (Search,
  Watchlist, wherever), not always the Library;
- browser Back and our button both unwind the chain one layer at a time, in sync.
Fixes: search → person → back → movie used to mislabel as 'Library' and dump you
in the library.

Next level:
- Search isn't a blank box when idle — a 'Trending this week' rail (TMDB
  trending, owned/preview annotated). Returns when you clear the query.
- Person page gets a 'Known For' hero rail (top titles by popularity) above a
  full filmography now sorted chronologically (newest first).

Backend: TMDBClient.trending + engine.trending (+library annotation), route
/api/video/trending. Isolated; 237 video-suite tests pass.
2026-06-15 08:30:17 -07:00
BoulderBadgeDad
288d44155d video: in-app Search + TMDB-backed (preview) detail + person pages
Search any movie / show / person (TMDB multi-search) entirely in-app. Results
that you already own link straight to the library detail; the rest open a
TMDB-backed 'preview' detail that reuses the exact same Netflix billboard UI
(direct image URLs, nothing owned/enriched). Everything resolves back into
SoulSync — no external links on un-owned titles.

- Search page (video-search.js): debounced /api/video/search, grouped
  movies/shows/people cards (reuses .library-artist-card) with owned/preview
  ribbons. People open the person page.
- Source-agnostic detail (video-detail.js): loads from /api/video/detail
  (library) or /api/video/tmdb (preview); art helpers pick proxy vs direct URLs;
  tmdb shows lazy-load episodes per season; owned-via-tmdb-url auto-redirects to
  the library detail.
- 'More Like This' now drills in-app (tmdb detail, redirects if owned); cast/crew
  link to a new in-app person page (bio + filmography, each credit owned/preview).
  Library credits now carry tmdb_id so owned-item cast is clickable too.
- Backend: TMDBClient.search/full_detail/person (+ shared _parse_extras);
  engine.search/tmdb_detail/tmdb_season/person_detail; db.library_id_for_tmdb;
  routes /search, /tmdb/<kind>/<id>, /tmdb/show/<id>/season/<n>, /person/<id>.

Isolated (one-way): video-only files, no music imports, music shell untouched.
Seam tests: search/full_detail parsing, tmdb_detail assemble+redirect, search +
person library annotation, library_id_for_tmdb, route registration, shell/JS
isolation. 234 video-suite tests pass.
2026-06-14 23:31:35 -07:00
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
c565fec59d video: show the FULL episode list (owned + missing), Sonarr-style
Previously the episodes table held only what the server has (all 'Owned'), so the
detail page never showed what you're missing. Now the metadata provider defines
the full series structure and the server marks ownership:

- TMDB returns the full season list (poster optional) + full episode fields
  (title/air date/runtime/still/rating) per season.
- backfill_episodes UPSERTs: owned episodes keep has_file=1; episodes the server
  lacks are inserted as MISSING (has_file=0); fully-missing seasons get created.
  The cascade now iterates every TMDB season, not just the ones on the server.
- The scan prune only removes SERVER-originated rows (server_id set) that vanished,
  so enrichment-added missing episodes/seasons are never pruned on re-scan.

Season coverage (X / Y) is now meaningful, and the episode list shows Owned +
Missing together. Seam tests: missing-episode insert, fully-missing season,
prune preserves missing.
2026-06-14 22:02:58 -07:00
BoulderBadgeDad
6e526d7745 video detail: deeper TMDB extras — trailer, where-to-watch, more like this
Phase 4: dynamic extras fetched LIVE per view (providers change, so not cached)
via GET /detail/<kind>/<id>/extras → engine.item_extras → TMDB
(videos + watch/providers + similar in one call).
- Trailer: a '▶ Trailer' action that opens an in-app YouTube modal embed (Esc /
  click-away to close).
- Where to Watch: provider logos for the region (JustWatch via TMDB).
- More Like This: a poster row of similar titles linking out to TMDB.
Both movie + show pages; all keyless (same TMDB key).

Seam tests: extras parse (trailer priority, provider/similar shape), item_extras
gating on tmdb_id, route registered, markup hooks. (RT/Metacritic via OMDb needs
its own key — offered separately.)
2026-06-14 21:42:41 -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
80f1051e8a video enrichment: cascade episode backfill from the TMDB show worker
Episodes ride along with their show instead of being a separate (tens-of-thousands)
queue: when the TMDB worker matches a show, it now backfills every season's
episodes — still / overview / rating — via /tv/<id>/season/<n> (one call per
season, gap-only so server data is never clobbered). Also backfills season
overviews.

The worker manager 'knows about it': the TMDB breakdown gains an Episodes
coverage entry (matched = has art, rest = pending), shown as its own card; the
Episodes view lists episodes still missing art. It's coverage-only, kept out of
the worker's idle/pending calc so it never blocks 'Complete'.

Seam tests: client season parse, worker cascade fills episodes, gap-only backfill
+ season overview, breakdown coverage (tmdb only), missing-art list, idle calc
ignores episode coverage.
2026-06-14 18:09:24 -07:00
BoulderBadgeDad
878e467f69 video enrichment: backfill season posters from TMDB (server usually lacks them)
The media server rarely has distinct per-season art, so season cards fell back to
a gradient. TMDB's show detail carries a poster_path per season — the show worker
now returns those, and enrichment_apply backfills seasons.poster_url for seasons
the server left without art (gap-only, never clobbers server art). The image
proxy streams a stored full URL (TMDB) directly vs. proxying a server path.

Seam tests: TMDB returns season posters, backfill fills only missing seasons.
2026-06-14 17:57:24 -07:00
BoulderBadgeDad
2306a5740c video enrichment: pull everything TMDB/TVDB offer + backfill gaps only
Enrichment now harvests the full detail payload (same call, no extra requests):
- TMDB: tagline, genres, rating (vote_average), runtime, status, first/last air
  date (shows), release date + runtime (movies) — on top of overview/backdrop/ids.
- TVDB: switches to /series/<id>/extended for overview + genres.

enrichment_apply now uses BACKFILL semantics: metadata columns are written via
COALESCE(NULLIF(col,''), ?) so enrichment only fills fields the media server
left empty — it never clobbers server-provided data. Genres backfill to the
normalised link tables only when the item has none yet. Whitelist expanded for
the new columns.

Seam tests: backfill-only (server overview/genres kept, gaps filled), genre
backfill when empty, TMDB full-metadata extraction.
2026-06-14 17:23:43 -07:00
BoulderBadgeDad
a483219746 video enrichment: distinguish transient 'error' from 'not_found' (match music)
A failed lookup CALL (network/429/5xx/timeout, or an expired TVDB token) was
recorded as 'not_found' — permanently logging a transient blip as 'no match'
and parking the item for retry_days. Now mirrors the music workers' proven
pattern:

- New 'error' status, distinct from 'not_found'; enrichment_next retries BOTH
  after retry_days, so errors recover and the queue still advances (no poison
  loop). breakdown/unmatched/retry-all and the modal account for it (shown with
  the outstanding/pending bucket).
- TMDB/TVDB clients raise on non-200 (429/5xx) so the worker records 'error',
  not a false not_found.
- TVDB re-authenticates once on a 401 (expired token) instead of failing every
  match for the rest of the run.

Seam tests: error!=not_found, error retried after window, 429 raises, TVDB
token refresh, UI accounts for errors.
2026-06-14 15:34:45 -07:00
BoulderBadgeDad
fc68a6e741 video enrichment: enrich by the server's provider id, not a title re-search
The deep scan stores tmdb_id/tvdb_id/imdb_id from Plex/Jellyfin, but the workers
only ever searched by title+year and ignored those ids — re-deriving matches the
server already had exact (wasteful, and a title search can mis-match).

enrichment_next now surfaces the row's known provider id; the worker forwards it
and the TMDB/TVDB clients fetch details BY ID (one call, no /search) when it's
present, falling back to title/year search only for items the server couldn't
identify. Still grabs the overview/backdrop the scan doesn't capture.
2026-06-14 15:03:26 -07:00
BoulderBadgeDad
2901e4ec4d video settings: per-connection Test button (mirrors music's testConnection)
Each video connection item (TMDB/TVDB) now has a Test button that behaves like
music's: saves the key, hits POST /api/video/enrichment/<svc>/test, and toasts
the result via the shared showToast — isolated (own endpoint, own data-attr
handler, reuses the .test-button CSS).
- Client .test() pings TMDB /configuration and TVDB /login to verify the key.
- Endpoint returns {success,message,error}; unknown service -> 404.
94 tests green; music untouched.
2026-06-14 12:26:15 -07:00
BoulderBadgeDad
c17138bf7d video enrichment 1b: worker engine + TMDB/TVDB clients
Isolated enrichment subsystem mirroring the music worker pattern, on video.db.
- VideoEnrichmentWorker: daemon loop pulling enrichment_next -> client.match ->
  enrichment_apply; pause/resume/stop; get_stats in the same shape the music
  enrichment API returns (enabled/running/paused/idle/current_item/stats/
  progress/breakdown). Client injected -> loop fully unit-tested with a fake.
- TMDB/TVDB clients (thin adapters, keys from video_settings): .enabled +
  match(kind,title,year) -> {id, metadata}; validated live, worker logic tested.
- VideoEnrichmentEngine registry + lazy get_video_enrichment_engine() singleton.
8 tests (match/not_found/error-resilience/stats/disabled/engine/isolation).
2026-06-14 11:21:30 -07:00