Commit graph

4201 commits

Author SHA1 Message Date
BoulderBadgeDad
21163239da Video detail page: follow control uses the new curated watchlist
The detail actions row still toggled the old shows.monitored flag via
/api/video/monitor. Now it's consistent with the cards:
- The 'Watchlist' button appears only for AIRING shows (movies + ended shows are
  terminal — they keep acquisition, not a watch-follow).
- It reads/writes the new /api/video/watchlist add/remove, resolves the real
  watched state on load (airing library shows are on by default), confirms on
  remove (the standard dialog), toasts, and broadcasts the change so the nav
  badge + watchlist page stay in sync.
2026-06-16 10:44:59 -07:00
BoulderBadgeDad
240e386871 Video: wire the eye/get button onto every card surface
A shared VideoGet.cardButton({kind,tmdbId,libraryId,title,poster,status,source})
picks the right control (person->eye, movie->get, show->eye when airing/unknown
else get) so every surface stays consistent. Injected into person filmography
(known-for + credits), search results (titles + people), and detail-page cast +
similar/collection rails. Each renderer hydrates watched state after render;
card roots get position:relative so the button anchors; reveal on hover.
2026-06-16 10:41:07 -07:00
BoulderBadgeDad
964675443b Video watchlist: nav badge with the live count
Adds a .dl-nav-badge to the Watchlist nav entry, updated from
/api/video/watchlist/counts on boot, on every watchlist change, and whenever
the page loads. Hidden at 0.
2026-06-16 10:20:33 -07:00
BoulderBadgeDad
58ceca86b1 Video watchlist: richer show cards (status pill + ep count) + sort dropdown
- Backend: effective shows now carry status + owned/total episode counts (joined
  off the shows table); query_watchlist gains a sort (default | title | added).
- Cards: a status pill (Airing / Upcoming / Ended) top-left + '12/20 eps' meta
  under the title for shows.
- Toolbar: a sort select (Following / A-Z / Recently added) next to search.

82 video tests green.
2026-06-16 10:19:13 -07:00
BoulderBadgeDad
1b23b7da78 Video: contextual get-symbol button + detail/download modal (visual only)
The terminal-content counterpart to the watchlist eye. On library cards:
- airing show  -> watchlist eye (monitor for new episodes)
- movie / ended show -> a 'get' download symbol that opens a detail modal

- video-get-modal.js: VideoGet.btn() + VideoGet.isAiring() (the shared status
  test), and the modal — hero backdrop, eyebrow, title, meta (runtime/rating/
  tagline), genres, overview, pulled from the existing detail endpoint. Action
  buttons are VISUAL STUBS for now: 'Open full page' navigates; 'Add to
  Wishlist' just toasts 'coming soon' (real population is a later phase).
- query_library now selects s.status so cards can pick eye vs get.
- CSS for .vget-btn (hover-reveal, accent on hover) + the .vgm-* modal, styled
  to match the calendar episode modal.

82 video tests green (status is an additive column).
2026-06-16 10:11:16 -07:00
BoulderBadgeDad
4287385af6 Video watchlist page: server-paged + search bar (like the library)
The page rendered every follow + airing-default show at once (DOM + all posters)
— slow once the watchlist grows. Now it pages like the library:

- /api/video/watchlist?kind=&search=&page=&limit= returns {items, pagination,
  counts}; query_watchlist() filters by title + slices (effective list is
  bounded, so compute-then-slice, not heavier UNION SQL).
- Page reworked to a single grid: Shows/People tabs each load their own page;
  debounced search box; Prev/Next pager; tab badges show totals from counts.
- Only a page of cards (and lazy posters) render at a time.

4 tests added (DB paginate/search + endpoint). 82 video tests green.
2026-06-16 09:40:41 -07:00
BoulderBadgeDad
b344303f75 Watchlist eye: confirm before removing (the standard yes/no dialog)
The eye is small/easy to mis-click and the card vanishes on the watchlist page,
so removal now goes through the shared showConfirmDialog (core.js) — 'Remove
"<title>" from your watchlist?' — before un-following. Adding stays one-click
instant. Cancelling re-enables the button with no change.
2026-06-16 09:29:31 -07:00
BoulderBadgeDad
d49eb967a4 Fix: watchlist card click did a full page reload (~15s freeze)
The watchlist cards were bare <a href='/video-detail/...'> with no click
interceptor, so clicking one triggered a REAL browser navigation to the
client-routed URL — the server returns the app shell and the entire app
re-boots (every static asset re-fetched = the ~15s 'restart' freeze).

Mirror video-library.js: intercept plain left-clicks on the cards and dispatch
soulsync:video-open-detail {kind, id, source} for in-app SPA navigation instead.
Encodes the open target on each card (library shows by library id, people +
un-owned shows by tmdb id). Modified clicks (new tab) still use the real href;
the eye button's capture-phase handler already stops its own clicks.
2026-06-16 09:16:18 -07:00
BoulderBadgeDad
248e2c32f2 Video dashboard: curated watchlist count + clear the wishlist for now
The dashboard still read the old monitored-based views: watchlist from
v_watchlist (every monitored show) and wishlist from v_wishlist (every missing
movie/episode, since monitored defaults to 1). Repoint both:
- watchlist -> the curated watchlist_counts() total (follows + airing default).
- wishlist  -> 0 for now. The auto-everything v_wishlist isn't the intended
  curated wishlist; zero it (no live-DB mutation) until 'add to wishlist'
  population lands, then repoint at the real source.

Test updated to the new semantics (airing show counts; wishlist cleared).
2026-06-16 09:09:10 -07:00
BoulderBadgeDad
ffaa36105b Video watchlist: actively-airing library shows are watched by default
Owning a still-running show means you want its new episodes, so it's on the
watchlist without a click. Implemented as a computed default + explicit-override
so it stays correct:

- video_watchlist gains a 'state' column: 'follow' (explicit) | 'mute' (a
  tombstone — user un-followed an airing show that's on by default, so the
  default must not silently re-add it).
- Effective watchlist (list/state/counts) = explicit follows  ∪  library shows
  whose status isn't ended/canceled, minus mutes. Computed at READ time, so it
  always tracks the library + a show's status — no scanner hook, no re-seeding.
- remove() now writes a mute tombstone (idempotent) instead of deleting; add()
  sets state='follow' and clears any mute. Scoped to the active video server.

The existing library-card eye now paints 'watched' on airing shows by default;
clicking mutes, clicking again re-follows.

4 tests updated/added incl. the airing-default + mute + re-follow flow. 80 video
tests green.
2026-06-16 08:47:48 -07:00
BoulderBadgeDad
e5a4dda117 Video watchlist (shows + people): DB + endpoints + button + page (v1, no scan yet)
A curated follow-list for the video side, mirroring the music watchlist. v1 is
membership only — the monitoring/discovery engine is a later phase.

Backend:
- video_watchlist table (kind 'show'|'person', keyed on tmdb_id — the stable
  cross-context id both carry; library_id kept when owned). NOT the existing
  shows.monitored flag (that defaults to 1 / is library-only / has no people).
- VideoDatabase: add/remove/list/state/counts (upsert COALESCEs library_id +
  poster so a TMDB re-add can't wipe known data).
- /api/video/watchlist {GET, /add, /remove, /check, /counts}.
- query_library now selects s.tmdb_id so show cards can carry the key.

Frontend:
- video-watchlist-btn.js: shared eye button (the music ya-watchlist-btn mirror)
  — build/toggle/hydrate, one delegated capture-phase click handler, broadcasts
  soulsync:video-watchlist-changed so pages can react.
- Watchlist page (new subpage + video-watchlist.js): Shows / People tab switcher,
  poster grid to detail-page quality, reloads each visit, drops cards on unfollow.
- Wired the eye onto library TV-show cards (movies excluded — wishlist, not
  watch) + hydrate on render.

Tests: 6 new (DB upsert/COALESCE/state/counts + endpoint roundtrip/validation).
76 video tests green. Other card surfaces (cast, search, similar, filmography)
are the same VideoWatchlist.btn(...) one-liner — wired next.
2026-06-16 01:06:24 -07:00
BoulderBadgeDad
6d72b7ca11 Video Calendar: EPG-style guide with a shared time axis + premium polish
The 7 independent day-stacks had no shared time axis — you couldn't scan
'what's on in prime time' across the week. Restructured into a real guide:

- Rows = time-of-day bands (Morning / Afternoon / Prime Time / Late Night /
  Anytime) down a frozen left time-rail; 7 day columns; frozen header row +
  sticky corner. Untimed streaming drops land in Anytime; each card keeps its
  exact time. Empty bands (none all week) are hidden; empty cells show a dot.
- Live 'now' cue lights the today-column × current-time-band intersection.
- Prime Time rail glints gold.
- Keyboard nav (←/→ weeks, T = today), scoped to the visible page + no modal.
- Week-change crossfade; staggered card entrance runs only on load/week-change
  (not on filter re-renders); keyboard focus rings on cards.
- Subheader now shows owned/missing split.

Kept all existing interactions: tilt cards, breathing glows, skeleton shimmer,
parallax billboard, rich episode modal. Vanilla static files, no build step.
2026-06-16 00:35:35 -07:00
BoulderBadgeDad
57c5148207 Video Calendar: week navigation + owned/missing filter
Finish the TV calendar as a planning tool.

- Week nav: ‹ prev / Today / next › steps the 7-day window (endpoint takes
  ?start=; each window starts on today's weekday so "current day first" holds).
  Title reflects it (This Week / Next Week / In N weeks); Today highlight only
  shows when the real today is in view.
- Filter: All / In library / Missing, applied client-side (no refetch) — the
  hero + grid + count all respect it.
- Hero eyebrow is context-aware (NEXT UP this week, FEATURED on other weeks).
2026-06-15 19:32:57 -07:00
BoulderBadgeDad
5856eefd7c Video Calendar: featured billboard, tactile cards, faster art
Take the calendar visuals up a level + fix slow image loads.

- Featured "Next up" billboard: the soonest upcoming episode as a cinematic hero
  (backdrop, pulsing accent, show title, S·E, air time, "View details" → modal).
- Cards: cursor-following 3D tilt + hover lift/scale/glow bloom; ambient glow
  dialed way down so it's calm at rest and only blooms on interaction.
- Faster art: poster proxy takes ?w= and asks the source for a thumbnail (Plex
  transcoder w/ original fallback, Jellyfin maxWidth, TMDB size bucket) — calendar
  requests ~500px instead of full backdrops. Skeleton shimmer → fade-in on load.
2026-06-15 19:25:45 -07:00
BoulderBadgeDad
3a8c803a54 Video: scope library reads to the active server (Plex/Jellyfin don't commingle)
Storage was already per-server (movies/shows UNIQUE(server_source, server_id),
episodes via per-server show_id, prune_missing scoped) — but reads returned
every server's rows, so a Jellyfin scan would show up alongside Plex.

Mirror the music standard: scope reads to the active video server
(resolve_video_server). query_library, calendar_upcoming, dashboard_stats and
library_id_for_tmdb take a server_source; the dashboard/library/calendar
endpoints pass it. server_source=None keeps "all servers" (enrichment processes
every server; tests unchanged). No schema change, no data migration — existing
Plex data is untouched and simply hidden while Jellyfin is the active server.

Regression tests: same title on both servers stays two rows; scoped reads only
return the active server's data; deep-scan prune never touches the other server.
2026-06-15 18:45:28 -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
5b6ef295f8 Video detail: trailer billboard reveals with a left→right wipe + fade
Replace the flat opacity crossfade with a soft, feathered left→right wipe of the
trailer in (mask via @property, falls back to crossfade) — "a rollover and a
fade". The wipe now fires ONLY when YouTube reports PLAYING (handshake + state
events), not on a blind timer, with a 4.5s safety net. When the trailer ENDS the
original backdrop fades back in and the iframe is torn down.
2026-06-15 16:55:00 -07:00
BoulderBadgeDad
80dd2ff21c Video side: own server connection (Plex/Jellyfin), music-style picker, isolated
Give the video side its OWN server connection — pre-filled from music but stored
separately in video.db, fully isolated (video never writes music config/state).

- Effective config helpers (video_plex_config / video_jellyfin_config): video's
  own creds when set, else inherited read-only from music. resolve_video_server +
  _build_source + watch-link/poster/dashboard all use these (own db threaded in).
- Server Connection UI mirrors music's server picker (toggle = select + configure),
  scoped to Plex/Jellyfin, at the bottom of the Connections tab.
- Jellyfin: independent client built from video's creds; explicit USER picker like
  music (list users → that user's libraries); honors the pick, admin fallback.
- Honest connection diagnostics (reachable vs 401 vs no-users) instead of a vague
  "auth failed".
- Auto-save on change with toasts; the shared Save button is intercepted on the
  video side so it saves video settings (and can't fire a music save).
- Enrichment status now PUSHES over the socket like music (no browser polling /
  access-log flood); config save only rebuilds workers when an API key changed.
- Seam tests for effective-config inheritance/override + isolation guard.
2026-06-15 16:42:22 -07:00
BoulderBadgeDad
8f6f992670 video: self-contained video Source section; hide music's server settings on video
Per the desired model: video Settings has its OWN server settings, separate from
music. The 'Video Source' group now holds the Plex/Jellyfin pick AND the Movies/TV
library mapping (moved out of music's Plex panel so it's not coupled to the music
active server). The whole MUSIC 'Server Connections' group is hidden on the video
side. Video reuses the shared Plex/Jellyfin credentials: the picker shows both
servers, the configured ones selectable, the rest 'not connected — set up in Music
settings'. Music settings keep their own server settings, untouched.
2026-06-15 15:10:53 -07:00
BoulderBadgeDad
f93c211de3 fix: video side can configure server connections again, without changing music
The previous hard guard blocked saveSettings entirely on the video side — which
also blocked entering/saving Plex/Jellyfin connection creds (shared, legit). Now
the save runs, but on the video side it PRESERVES the persisted active_media_
server (window._persistedActiveServer) instead of reading the toggle — so video
can set up a connection without ever switching the music server. Auto-save stays
suppressed on the video side (no heavy music re-init from video field edits).
2026-06-15 14:59:36 -07:00
BoulderBadgeDad
5f1ad517c2 video: fully decouple video server from music (no fallback + hard save guard)
Two real coupling bugs, fixed:
- resolve_video_server still fell back to the music active server when no explicit
  video pick was set, so changing the music server changed video. Removed: video
  now uses ONLY an explicit video pick or the configured server(s) (Plex default
  when both). Changing the music server never changes video.
- The shared settings page could persist active_media_server from the video side.
  Guarded saveSettings itself (not just the debounced auto-save) so it NEVER runs
  while data-side=video — video saves only via /api/video/*.

Test: video does not follow the music active server. One-way isolation, both ways.
2026-06-15 14:52:37 -07:00
BoulderBadgeDad
a4d6e78bce fix: video side can no longer change the music server via the shared settings page
The settings page is shared; on the video side its auto-save (debouncedAutoSave-
Settings) still fired when editing VIDEO fields (TMDB key, watch region, autoplay),
and saveSettings reads the server toggle from the DOM + persists active_media_
server — so clicking the toggle to Jellyfin on the video side and then editing any
field would write active_media_server=jellyfin to the shared config (and trigger a
full music save). Guard: never auto-save music settings while data-side=video.
Video settings persist via /api/video/*; the toggle re-syncs to the real active
server on every Settings load. One-way isolation restored.
2026-06-15 14:42:36 -07:00
BoulderBadgeDad
b7ae32220b video: coverage-card click re-queues failed matches (match music manager)
The video worker manager's coverage cards only switched the view — they never
re-queued failed items, so not_found/error items sat in the retry cooldown and
the worker reported 'all matched' / idle while failures piled up. Clicking a
Movies/Shows coverage card now pins that group AND resets its failed matches
(not_found/error -> pending) so the worker sweeps them, mirroring the music
worker manager. (Episodes are a sync cascade, not a match queue, so unchanged.)
2026-06-15 14:27:25 -07:00
BoulderBadgeDad
df09111831 video: move Detail Pages prefs to the Library settings tab
Video Preferences group now carries data-stg='library' so it lives under the
Library settings tab on the video side (the infra already supported video-only
library settings). The Video Source panel moves to data-stg='connections' so it
stops showing on every tab.
2026-06-15 14:19:57 -07:00
BoulderBadgeDad
c9900d91b0 video: hide Navidrome + Standalone server toggles on the video side
They're music-only, so a CSS rule scoped to body[data-side=video] hides those two
toggle buttons in the shared Server Connections section. Plex/Jellyfin stay
(for connecting a video server). Music side untouched.
2026-06-15 14:12:04 -07:00
BoulderBadgeDad
b2dbd6dec5 video: Video Source settings panel + library gating; move Detail prefs to own group
Settings → Video Source shows which server video uses (✓ Plex/Jellyfin), a
Plex/Jellyfin picker when both are connected, or a clear 'connect Plex or
Jellyfin' message when neither (Navidrome/Standalone are music-only and not
offered). The Library shows a non-breaking 'no video server' banner + disables
Scan until one is connected. Detail Pages prefs moved into their own 'Video
Preferences' group. /api/video/server GET+POST drives it.
2026-06-15 13:55:42 -07:00
BoulderBadgeDad
f7d1b725d7 video: resolve the video server independently of the music active pointer
The video side now uses a configured Plex/Jellyfin on its own (resolve_video_
server), not config_manager.get_active_media_server(). So a music-only server
(Navidrome/SoulSync) never applies to video, and 'Navidrome for music + Plex for
video' works. Order: explicit video pick (video_server setting) → music-active if
video-capable → the single configured one → Plex if both → None. Seam tests cover
each case incl. the mixed setup.
2026-06-15 13:50:19 -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
3359e3c111 video: owned-media tech specs on movie detail (Plex-grade)
We already scanned codec/audio/source/size but only showed resolution. The movie
detail Details block now surfaces Quality / Video (HEVC, H.264…) / Audio / Source
(Blu-ray, WEB-DL…) / Size, and lists every version/edition you own when there are
multiple files. movie_detail now returns all media_files (not just the largest).
2026-06-15 13:13:26 -07:00
BoulderBadgeDad
621693ecc8 video: real TTL+LRU cache (thread-safe) + cache search — the kettui way
The inline dict cache was a latent bug: no lock (the engine singleton is hit by
concurrent Flask + worker threads) and a wholesale-clear cliff at 256 (nuked hot
entries). Extracted a thread-safe TTL+LRU TTLCache into an importable core/ module
with seam tests (expiry via injected clock, LRU-not-wholesale eviction, a
concurrency stress test). Engine now uses it; search is cached too (60s, ownership
re-stamped fresh). Deliberately NOT persisted to disk — durable data already lives
in video.db; that tier would be over-engineering for a self-hosted app.
2026-06-15 13:02:24 -07:00
BoulderBadgeDad
b567eb4808 video: cache person pages, lazy preview seasons + trending (close the gaps)
The owned-item extras and preview detail payloads were already cached (30-min
TTL); person_detail, tmdb_season, and trending were not — so person pages and
lazy preview seasons re-hit TMDB each view. Now cached too (trending 1h). Library
ownership is re-annotated fresh on each call so 'In Library' badges stay current
while the expensive TMDB fetch is reused. Tests for person + season caching.
2026-06-15 12:55:56 -07:00
BoulderBadgeDad
e5724c6faa video: sticky back button on detail/person pages
The detail pages are long now (cast, videos, photos, reviews…), so the absolute
back button scrolled out of reach. It's now position:sticky (pinned top-left as
you scroll), with a negative bottom margin so the billboard still sits full-bleed
under it.
2026-06-15 12:40:48 -07:00
BoulderBadgeDad
1423e50ccd video: fix Play/Trailer buttons vanishing on owned movies
maybeRefreshMovie re-fetched the library payload (no trailer/server — those come
from the extras call) and replaced data, so renderActions re-rendered without the
buttons right after they appeared. It now carries over the live extras fields
(server/trailer/next_episode), mirroring the show reloadDetail fix.
2026-06-15 12:28:32 -07:00
BoulderBadgeDad
3f439e4277 video: fix back-button loop when a 'More Like This' item is owned
Clicking a recommendation/cast/similar link to a title you OWN opens it via a
tmdb URL that redirects to the library detail — but the redirect PUSHED a new
history entry, so Back landed on the tmdb URL which redirected forward again =>
stuck loop + a self-referencing 'Back to <same movie>' label.

The redirect now REPLACES the redirecting entry (keeping its layer + origin)
instead of pushing, so Back unwinds cleanly to where you actually came from.
2026-06-15 12:18:05 -07:00
BoulderBadgeDad
2f3e4b128b video: autoplay billboard trailer (opt-in setting)
After a couple seconds on a detail page, a muted trailer plays behind the hero
(Netflix/Disney+ style) with mute/unmute + stop controls; the backdrop fades back
when stopped. Stops on navigate-away/modal-open (no orphaned audio).

Gated by a 'Autoplay trailers in the billboard' toggle in video Settings →
Detail Pages (default on). Backed by billboard_autoplay in video_settings, read
via a lightweight /api/video/prefs. Tests updated for the new config field.
2026-06-15 11:57:29 -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
8bbdd712f9 video: per-title accent on preview + person pages (same-origin image proxy)
Owned detail pages sample the poster for the per-title glow, but preview/person
pages fell back to the theme accent because their TMDB images are cross-origin
(canvas taint). Added /api/video/img — a same-origin proxy restricted to
image.tmdb.org (SSRF-safe) — so:
- preview (tmdb) detail samples its poster via the proxy → real accent;
- the person page samples the portrait → per-person accent on the ring/glow/role.
Tests: route registered + proxy rejects non-tmdb URLs.
2026-06-15 11:46:49 -07:00
BoulderBadgeDad
c5e30530c7 video: card hover shows an info badge, not a misleading play button
Search / trending / filmography poster cards open the DETAIL page on click, so
the center ▶ read as 'play' wrongly. Swapped it for an italic 'i' info badge
('view details').
2026-06-15 11:42:32 -07:00
BoulderBadgeDad
9c29414e6d video: detail pages — Photos gallery+lightbox, all Videos, Details/keywords, full cast
Frontend for the new data, on both movie + TV detail pages:
- Photos: a backdrops rail → fullscreen lightbox (‹ › nav, keyboard arrows, Esc,
  counter).
- Videos: a rail of every trailer/teaser/clip/featurette (YouTube thumbs) → opens
  in the existing player modal.
- Details: budget / box office / language / country + keyword tag chips.
- Cast & Crew gets a 'View all N' → full-cast modal (clickable to person; TV shows
  per-actor episode counts).
All cached server-side (instant re-open) and lazy-loaded images. Isolated; shell
tests cover the new sections + modals.
2026-06-15 11:23:31 -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
595ea2bfbd video: Where to Watch — provider logos are badges + one real link
TMDB only exposes a single aggregate 'where to watch' link (no per-provider deep
links), so N identical links read as broken. Streaming providers are now
non-clickable availability badges, followed by ONE accent 'Where to watch ↗'
link to the JustWatch/TMDB page. The Plex/Jellyfin tile keeps its real per-item
server deep link.
2026-06-15 11:00:15 -07:00
BoulderBadgeDad
e0b9245ba3 video: Play button reads 'Play on <logo>' (drop redundant text, better contrast)
The button showed the Plex logo AND the word 'Plex' ('[logo] Play on Plex'), all
white on a bright green bg — cluttered + hard to read. Now it's '▶ Play on
<server logo>' (the logo is the brand name) on a deeper green gradient so the
light Plex/Jellyfin wordmark reads clearly.
2026-06-15 10:54:49 -07:00
BoulderBadgeDad
b937343768 video: fix stuck ep-sync banner + vanishing Play/Trailer buttons on shows
Two bugs when a show's episode list backfills on view:
- The 'Fetching full episode list…' banner never hid: .vd-ep-syncing (and
  .vd-next-ep) set display:flex, which overrode the [hidden] attribute's
  display:none, so el.hidden=true did nothing. Added a guard so [hidden] always
  wins on the detail/search/person pages.
- Play & Trailer buttons vanished after the sync: reloadDetail replaced data with
  a fresh show_detail payload (no server/trailer — those come from extras), so
  renderActions re-rendered without them. reloadDetail now carries over the live
  extras fields (server/trailer/next_episode).
2026-06-15 10:32:04 -07:00
BoulderBadgeDad
81546cff69 video: detail-page feedback fixes (Play button style, crew links, ep-sync UX)
- Play button now matches the Trailer/Watchlist buttons exactly (same size/shape),
  just green — consistent hero buttons.
- Where to Watch: drop the duplicate streaming provider that matches your server
  (no more two 'Plex' entries). Providers still share TMDB's single JustWatch
  'where to watch' link (that's all TMDB gives).
- Director/Creator names (hero line + Cast & Crew section) are clickable → the
  in-app person page.
- Opening a show whose full episode list isn't cached yet now shows a 'Fetching
  the full episode list…' banner with a spinner, instead of a silent ~20s gap
  before missing episodes pop in.
2026-06-15 10:21:04 -07:00
BoulderBadgeDad
e94ea39187 video: skeleton loader for detail pages (shimmer billboard, not 'Loading…')
Replaces the plain loading text with a shimmering billboard placeholder (logo /
meta / overview / button bars) over the accent wash, so opening a title (esp. a
TMDB preview) feels instant and premium. Honors prefers-reduced-motion.
2026-06-15 09:59:39 -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
6eed3d7775 video: best-in-class detail billboards (Play CTA, collections, recs, next-ep)
Movie + TV detail pages get the things a premium app surfaces:
- Primary 'Play on Plex/Jellyfin' button (white Netflix-style CTA with the server
  logo) in the billboard for owned items — deep-links straight to the item.
- 'Directed by' (movies) / 'Created by' (shows) line in the hero.
- Movies: a Collection/franchise row (the other films in the set), release-ordered.
- 'More Like This' now uses TMDB recommendations (better curated), similar as
  fallback.
- TV: a 'Next Episode' banner (S/E + name + air date) for continuing shows, and
  the selected season's overview under the season nav.
All in-app (cards drill into library/preview detail). Shell tests updated.
2026-06-15 09:50:07 -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