After a grab (manual or Auto) the user now SEES what happened and can follow it:
- The chosen result card is spotlighted (Auto scrolls it into view) and grows a live
tracker: a progress bar that follows the real download + a 'Track on Downloads ↗'
button that closes the modal and jumps to the Downloads page. Polls the new
GET /api/video/downloads/status?id= until the download reaches a terminal state.
- A movie's detail page shows a live download chip (progress bar + %) for any in-flight
download of that title; clicking it jumps to Downloads. Looks up by media identity
via /downloads/status?media_id=&media_source=, polls while active, clears on
navigate-away. (video-detail.js)
- Result CARDS redesigned (the part you didn't like): a column card with a colour
resolution badge, a green 'accepted' edge, cleaner hierarchy, and the grab button is
now an accent 'Get' pill matching the source Auto button language.
Plumbing: new lightweight /downloads/status endpoint (by id, or by media for detail
pages); soulsync:video-navigate event in video-side.js to reach a top-level page from
anywhere; VideoGet.close exported so the tracker can dismiss the modal. All video-only.
Tests: status endpoint (by id + by media + null cases) in test_video_api.py;
tracking/detail/nav wiring in test_video_download_tracking.py. node --check clean;
isolation guards green.
Grabs now carry the movie's identity so the Downloads cards are rich, not anonymous:
- video_downloads gains media_id / media_source / year / poster_url (schema v15 +
migration); grab stores them (passed from the get-modal → download view → grab).
- Cards show the POSTER in the art tile (emoji fallback), 'Title (Year)', a quality
chip (1080p · BluRay · X265), and an ↗ Open button that jumps to the movie/show
detail page (dispatches soulsync:video-open-detail). Cancel/retry unchanged.
16 tests green, ruff + balance clean.
Wires the four scopes to the /downloads/search pipeline and renders beautiful,
profile-aware result cards (mock indexer for now):
- Movie: per-source ⌕ / Search all → movie scope.
- Episode: expand an episode → per-source ⌕ → episode scope (its own results panel).
- Season: the season ⌕ → SEASON-PACK scope, results under the season header.
- Whole show: top-bar 'Search whole show' → complete-SERIES-pack scope.
Each card shows the full release name (mono), colour-coded quality badges
(resolution/source/codec/HDR/audio/repack), size, seeders, and a verdict — green
'✓ Meets profile' or a dimmed '✕ <reason>' (e.g. not in your enabled tiers, wrong
season, over size cap). Accepted hits sort first and carry a ⤓ Grab button (stub).
Spinner while searching; staggered card entrance; reduced-motion honoured.
Replaced the faux-scan stubs. JS/CSS balance clean.
Shows now get their own download experience instead of the movie layout. Clicking
Download on a show WIDENS the modal (~680→940px, animated) and renders a tree:
- Quality target chips + a show summary (N seasons · M episodes · X in library ·
Y missing) + a master select-all + a primary 'Search N selected'.
- Season cards (collapsible) with a tri-state season checkbox, 'N eps · M missing',
and a per-season search button (auto-opens the season to reveal the scan).
- Episode rows: checkbox, E# · title, In library/Missing/Upcoming badge. Default
selection = everything you're MISSING (owned shown but unticked, upcoming locked).
- Each episode expands inline to its branded per-source search strip (Soulseek/
Torrent/Usenet), so you can grab one episode à la carte; or batch via the season/
bulk buttons. Searches are the same faux-scan stub — no backend yet.
Library shows use shipped episodes; tmdb shows prefetch seasons via the existing
/tmdb/show/<id>/season/<n> endpoint. get-modal stashes the detail + tvId and routes
shows to VideoDownload.render({kind:'show',…}); movie/youtube paths unchanged.
Vibey: staggered entrance, branded scan bars, smooth collapse. Reduced-motion honored.
JS + CSS balance clean.
Per feedback — one modal with a view transition beats close+open. Clicking
'Download' now swaps the get-modal's detail body for the download view (quality
target + owned verdict + per-source search) with a '← Back to details' button;
the selection-only sections (episodes/next/follow) and the Full page/Download/Add
buttons collapse, the all-related movie bits stay.
- video-download-modal.js → video-download-view.js: now a reusable RENDERER,
VideoDownload.render(container, opts), not its own overlay. A future YouTube
trigger can render the same view into its own container (still universal).
- get-modal: enterDownload/exitDownload toggle the in-place view; stashes the
owned file on modalState to feed the verdict. Removed the separate .vdl
overlay/hero/close CSS; added .vgm-dl/.vgm-back + [hidden] guards; chips now
pick up the modal's --vgm-h vibe hue.
Pure front-end; balance + div + css brace checks clean.
Adds a 'Download' action to the get-modal that closes it and opens a new universal
Download modal (VideoDownload.open) shared by movies, shows and YouTube:
- Quality TARGET shown as chips, read live from the profile you configured
(cutoff / codec / HDR / rejects / size for p2p; resolution/codec/container/
60fps/HDR for YouTube).
- Owned copies get a REAL verdict: 'In your library · 720p · BluRay · X265' +
a 'Meets your target' / 'Eligible for upgrade' badge with reasons, via
/downloads/evaluate (the quality_eval seam).
- Per-source rows (soulseek/torrent/usenet from the download config, or yt-dlp
for YouTube) each with a Search button + a 'Search all'.
Per the ask, the searches are STUBS — clicking flips the row to 'coming soon'; no
backend yet (that's the engine phase). Beautiful .vdl-* styling mirrors the get-modal
vibe (per-title hue glow). New JS file wired into index.html. Pure front-end.
More data in the roomier episode cards (asked for): episodes now carry a synopsis
(new video_wishlist.episode_overview, SCHEMA_VERSION 12 + migration; captured at
add-time, and the art-backfill fills it for old rows from the same tmdb_season
call). The card shows a 2-line synopsis under the meta line and is now clickable
-> opens the show detail (episodes have no page of their own).
Organize the wishlist two ways: added 'Oldest first' (FIFO) alongside 'Recently
added' (newest) — query_wishlist gains the 'oldest' sort for both movies + shows.
Tests: FIFO/newest ordering (with pinned add-times) + overview roundtrip/backfill.
105 passed. Music wishlist untouched.
Season tiles showed the show poster because the wishlist had no season art. Now
stored + used: new video_wishlist.season_poster_url (SCHEMA_VERSION 11 + migration);
the get-modal captures each season's poster at add-time (owned -> /poster/season
proxy, tmdb -> direct); query_wishlist exposes season.poster_url; tiles render the
real season poster (falling back to show poster, then placeholder).
Backfill generalized: /wishlist/backfill-stills -> /wishlist/backfill-art fills
BOTH stills and season posters from the same cached tmdb_season call (it already
returns the season poster). The page fires it once when either is missing.
Tests updated/added: art backfill targets + season-poster set + endpoint. 104 passed.
#4 acquisition progress: a thin done÷wanted bar across each bubble's bottom
(forward-prep — fills once the download engine lands).
Expanded-view richness: episodes now carry a still thumbnail. New video_wishlist
.still_url column (SCHEMA_VERSION 10 + migration); the get-modal captures the
still per episode (owned -> /poster/episode proxy, tmdb -> direct still_url) and
sends it through add; query_wishlist returns it; the episode row renders a 16:9
thumb (film-frame placeholder when absent) in a roomier expanded tile.
Subtle video identity: the bubbles are now rounded-SQUARES (the music orbs stay
circles). Every rule stays scoped under .vwsh-nebula — verified no bare .wl-*
rules in the video CSS, so the music wishlist is untouched.
Tests: +1 (still roundtrip). Backend 102 passed.
No longer a stub. The modal now captures the title's tmdb id + per-episode meta
(title/air_date, including lazily-loaded tmdb seasons), and on submit:
- movie -> POST /wishlist/add {movie} (owned -> 'queued for re-download')
- show -> POST /wishlist/add {show, episodes:[selected S/E]}; if the 'Add to
watchlist' tick is on, also POST /watchlist/add for the show.
Fires soulsync:video-wishlist-changed (+ watchlist-changed) so badges/pages
refresh, toasts the result, and closes. Every card surface (discover, search,
library, watchlist, detail) funnels through here, so they're all live now.
- Get-modal: prefetch the first real (un-owned) season on open so its first
expand is instant and its missing episodes pre-count in the footer (prefers
Season 1 over Specials).
- Tests: 10 new seam tests for the Discover data layer — TMDBClient curated/
discover/genres parsing (forced kind, decade date-range, tv first_air_date_year,
backdrop+overview), and engine discover_curated/discover_filter/genre_list
(owned annotation, caching, kind normalization, disabled-worker + error
swallowing). Full video enrichment suite: 79 passed.
A show you don't own ships its seasons with no episodes (loaded per-season
on expand, like the full detail page), so the modal mis-read '0 episodes'
as 'all owned' and seasons expanded to nothing.
- Render tmdb seasons as collapsible with an 'expand to load' placeholder;
fetch /api/video/tmdb/show/<id>/season/<n> on first expand, render rows,
pre-select the missing-aired episodes, enable the season select-all.
- Skip the 'you have every episode' hint when any season is lazy/un-owned;
lazy seasons aren't tagged owned, so they stay visible under 'Missing only'.
- Next-episode line falls back to the payload's next_episode stub when no
episodes are loaded (the only next-up source for un-owned shows).
- Owned movie shows 'In your library' (with best version quality) and the
footer flips to 'Re-download' instead of '+ Add to Wishlist'.
- Poster thumbnail floats over the hero (hue halo + own rise animation),
works for both library and TMDB sources.
- Ratings strip: branded IMDb / Rotten Tomatoes / Metacritic chips from the
existing payload fields; renders only what's present.
- Airing shows show the soonest upcoming episode above the selector.
The footer's 'Add to Wishlist' + 'Full page' now use the same buttons as the
artist-detail / Download-Discography modal — .discog-submit-btn (primary, ⬇ +
label) and .discog-cancel-btn (secondary) — instead of the custom vgm buttons,
for consistency across the app. updateFooter sets the label span (keeps the
icon). Per-title hue glows stay on the hero/title/ambient.
Each modal now glows in its own colour (stable hue hashed from the title, set as
--vgm-h):
- ambient hue halo around the modal + a hue-tinted hero scrim
- slow Ken Burns drift on the backdrop + a drifting light sweep across the hero
- staggered content rise on open; spring entrance for the modal
- title glow; hue-aware gradient CTA with lift + glow on hover
- episode rows grow an accent edge on hover; close button spins
- the 'Add to watchlist' row breathes a soft accent glow to invite the tick
All guarded by prefers-reduced-motion.
Remove the 'Download' button and all related handling from the video-get modal. The diff deletes the data-vgm-download button markup, removes download branching in the click handler and the isDl flag, and stops updating download text/disabled state in the UI refresh. The wishlist path is now the single visual flow (toast always reports Wishlist + optional Watchlist); actual write and download-from-wishlist behavior will be implemented later.
Fully-owned seasons used to render with a disabled checkbox and expand to
nothing (every episode hidden by the filter) — looked broken. Now:
- Fully-owned seasons are HIDDEN while 'Missing only' is on (the filter doing
its job); turn it off to see + re-download them (owned episodes are
selectable). Season meta reads 'owned · N eps' when shown.
- If you own EVERY episode, a hint appears: 'You have every episode. Turn off
Missing only to re-download.' (instead of a blank list).
Partial seasons (with gaps) show + expand as before.
- Owned episodes are now SELECTABLE (checkbox, not pre-checked) so you can
re-download a season/episode you already have; flip 'Missing only' off to see
them. Every season (incl. fully-owned) gets a select-all. Select-all + the
season checkbox state respect the 'Missing only' filter (no silently grabbing
hidden owned episodes).
- Airing show you don't follow yet → an accent 'Add to watchlist' tickbox
(default on) so grabbing episodes also starts you watching for new ones.
Hidden for ended shows + shows you already follow. The Add-to-Wishlist stub
toast notes '+ Watchlist' when it's ticked.
Shows aren't wishlist items — episodes are. So the show modal now grows an
inline season/episode selector below the overview:
- Collapsible season cards (season-level select-all + 'N missing · M eps').
- Per-episode rows with three states: owned (locked ✓, 'In library'),
upcoming (locked ◷, air date — these belong to the watchlist), and
missing-aired (accent checkbox, PRE-SELECTED).
- 'Missing only' toggle (default on) hides owned to cut noise; live selected
count; season checkboxes go indeterminate as you pick episodes.
Footer is now [count] · [Full page] · [Download N] · [Add N to Wishlist].
Movies keep a simple [Download] · [Add to Wishlist]. Add/Download are visual
stubs (toast the count) — the curated video_wishlist + download path come later.
Data is the existing show_detail seasons/episodes (owned + air_date).
An airing show is ongoing (follow for new episodes) AND has a back-catalog you
may be missing (acquire) — so it needs both controls, not one or the other.
VideoGet.cardButton() now returns a control GROUP:
person -> eye | movie -> get | airing show -> eye + get | ended show -> get
Library + watchlist-page cards route through it (the other surfaces already did).
New .vcard-ctrls wrapper lays the buttons out top-right; hover-reveal unchanged.
(Before, only ended shows showed the download button — which is why airing
shows looked like they had no way to acquire missing episodes.)
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.
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).