The active-downloads poll kept running in the background — the page-change event that
stops it only fires for video-side navigations, not when switching to the music side,
so /api/video/downloads/active was being hit forever. poll() now bails (and clears the
timer) whenever the Downloads page isn't on screen (data-side!=video or the subpage is
hidden). Also matched the music page's 2s active cadence (6s idle). Still HTTP polling,
same as the music downloads page (which polls /api/downloads/all every 2s — the app's
SocketIO is for other realtime, not the downloads list).
When a grabbed release fails (transfer error / peer-cancel / never lands), the engine
now retries instead of giving up — the music-style depth:
- Grab stores the OTHER accepted results as a retry pool + the search context (schema
v16: candidates / search_ctx / tried_queries / tried_files / attempts).
- core/video/retry.py (pure, tested): plan_retry() → try the next-best candidate; when
the pool is dry, next_query() generates an ALTERNATE query (movie: drop the year; TV:
numbering variants) to re-search; budget MAX_ATTEMPTS=6. merge_candidates dedupes
against already-tried releases.
- Monitor: on failure, _fail_or_retry hops to the next candidate inline; if none, flips
the row to a new 'searching' state and a background requery thread re-searches the
alternate query, evaluates against the profile, and starts the best fresh hit — or
marks failed once truly exhausted. 'searching' rows are owned by their thread.
- Page: 'Searching' status (Trying another release…) + a 'Nx' attempt badge.
16 tests (retry engine + candidate-retry transition); ruff clean on touched files.
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.
Scrapped the bespoke centered .vdpg- design and reused the music downloads page's
.adl-* classes for real parity: full-width .adl-layout, the segmented .adl-filter-pills,
the title with the accent download glyph, and the compact .adl-row (44px art tile +
.adl-row-info title/meta/error + .adl-row-status dot+label) — driven by the video JS
via data-vdpg-* hooks (no #id clashes with the music page). Per-row cancel reuses the
music hover-reveal .adl-row-cancel; retry mirrors it in accent. Kept the smooth
in-place patching (slim progress line for active rows). Removed the old .vdpg-* block;
balance clean.
Brings the Downloads page up toward the music page's depth:
- Filter tabs (music-style pills): All / Active / Completed / Failed, each with a live
count; clicking filters the list. Cancelled rolls under Failed.
- Header actions: Cancel all (active) + Clear finished, shown only when relevant; a
live 'N active · N done · N failed' subline.
- Per-row actions: ✕ Cancel on active rows (→ /downloads/cancel), ↻ Retry on
failed/cancelled rows (→ /downloads/retry, re-grabs the release).
- Cancelled status styling (pill + dimmed row). Still the smooth in-place patching
(no blink), adaptive polling, empty + filtered-empty states.
Balance clean. Phase C (auto-retry + alternate-query retry) next.
The blink was every poll re-running each card's entrance animation via innerHTML
churn. Now cards are created ONCE and PATCHED in place (data-st attribute swaps,
progress width glides over a 1.4s transition, meta only rewrites when its text
changes), so nothing re-animates on a tick — the music-downloads smoothness.
Also: adaptive polling (1.5s while active, 6s idle), border/pill colours TRANSITION
on status change, downloading cards get an accent tint + glow, a relative 'started Xs
ago', and the % moved inline. Balance clean.
Phase 3 — the UX:
- Grab button now actually grabs (Soulseek results only — they carry the slskd
username/filename): POSTs /downloads/grab with the result + search context, shows
✓ on success, toasts 'Sent to Downloads', fires a refresh event. Endpoint returns
size_bytes for the payload.
- New Downloads PAGE (video-downloads-page.js, .vdpg-*): every grab lands here.
Polls /downloads/active while open and shows live status — type icon (🎬/📺/▶️),
title + release, a pulsing 'Downloading' pill with a shimmering progress bar, then
'Completed' with the file's library destination (→ /media/movies/…), or 'Failed'
with the reason. Clear-finished button, empty state, staggered entrance, vibes.
Reduced-motion honored. JS/CSS/HTML balance clean (verified w/ a real tokenizer).