Two Sonarr-parity features. 1) Per-episode live tracking. "Grab season" was headless (only a button label changed); episode rows had a status span that was never populated. Now every episode ROW shows its own live state — Searching → Downloading % → Downloaded / Failed — via epTrack() polling /downloads/status?id, matching the inline movie tracker. Grab season lights all target rows at once; manual + per-source auto grabs also light their row; reopening the modal resumes tracking in-flight episodes (resumeEpisodeTracking via /downloads/active + search_ctx match). Season batch grabs through the same payload as a manual grab (_pickAndGrab → sendGrab(buildGrabPayload)). 2) Auto-wishlist airing episodes. New daily automation (video_add_airing_episodes): reads the calendar for episodes airing TODAY for followed shows, skips owned ones, adds the rest to the wishlist (idempotent). Handler uses injected seams (calendar read + wishlist write) so it's unit-tested without a DB/server. Registered + action block + seeded as a daily system automation (01:00), owned_by=video.
125 lines
5.6 KiB
Python
125 lines
5.6 KiB
Python
"""Live download tracking wiring: after a grab the user must (a) see WHICH release
|
|
was selected, (b) get a live progress bar on it, and (c) get a button to the
|
|
Downloads page — and a movie's detail page shows live progress for an in-flight
|
|
download that jumps back to Downloads.
|
|
|
|
Backed by GET /api/video/downloads/status (tested in test_video_api.py
|
|
::test_downloads_status_lookup_by_id_and_media). These pin the frontend wiring so
|
|
a refactor can't quietly unhook the tracker, the detail chip, or the navigation.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
_ROOT = Path(__file__).resolve().parent.parent
|
|
_VIEW = (_ROOT / "webui" / "static" / "video" / "video-download-view.js").read_text(encoding="utf-8")
|
|
_DETAIL = (_ROOT / "webui" / "static" / "video" / "video-detail.js").read_text(encoding="utf-8")
|
|
_SIDE = (_ROOT / "webui" / "static" / "video" / "video-side.js").read_text(encoding="utf-8")
|
|
_GETMODAL = (_ROOT / "webui" / "static" / "video" / "video-get-modal.js").read_text(encoding="utf-8")
|
|
_CSS = (_ROOT / "webui" / "static" / "video" / "video-side.css").read_text(encoding="utf-8")
|
|
|
|
|
|
# --- result card: redesign + selected/tracking state ----------------------
|
|
|
|
def test_result_card_redesigned_as_column_with_get_button():
|
|
assert 'vdl-res-main' in _VIEW # row wrapper so a tracker can dock below
|
|
assert 'data-vdl-card="' in _VIEW # addressable card (auto-pick targets it)
|
|
assert '[ GET ]' in _VIEW # brutalist grab button is a bracketed label
|
|
assert '.vdl-res-main' in _CSS
|
|
|
|
|
|
def test_grab_begins_live_tracking_on_the_card():
|
|
assert 'function beginTracking(' in _VIEW
|
|
assert 'vdl-res--grabbed' in _VIEW
|
|
assert 'data-vdl-track-fill' in _VIEW # the progress bar fill
|
|
assert "/api/video/downloads/status?id=" in _VIEW
|
|
# both the manual grab and the auto-pick start tracking the chosen card
|
|
assert _VIEW.count('beginTracking(') >= 3 # 1 def + doGrab + _autoPick
|
|
|
|
|
|
def test_track_button_goes_to_downloads_and_closes_modal():
|
|
assert 'Track on Downloads' in _VIEW
|
|
assert 'function gotoDownloads(' in _VIEW
|
|
assert 'VideoGet.close' in _VIEW
|
|
assert "'soulsync:video-navigate'" in _VIEW
|
|
assert '.vdl-res-track' in _CSS
|
|
|
|
|
|
def test_modal_exposes_close():
|
|
assert 'close: closeModal' in _GETMODAL
|
|
|
|
|
|
# --- movie detail page: live download chip --------------------------------
|
|
|
|
def test_detail_page_watches_movie_download():
|
|
assert 'function watchMovieDownload(' in _DETAIL
|
|
assert '/api/video/downloads/status?media_id=' in _DETAIL
|
|
assert 'data-vd-dlchip' in _DETAIL
|
|
# the chip jumps to the Downloads page
|
|
assert "'soulsync:video-navigate'" in _DETAIL
|
|
assert '.vd-dlchip' in _CSS
|
|
|
|
|
|
def test_detail_watch_started_for_library_movies():
|
|
assert 'watchMovieDownload(id)' in _DETAIL
|
|
assert 'stopMovieDownloadWatch()' in _DETAIL # cleared on (re)load / navigate away
|
|
|
|
|
|
# --- result cards: flat / brutalist redesign ------------------------------
|
|
|
|
def test_result_cards_are_flat_brutalist_three_line():
|
|
# 3 hard lines: [QUALITY] + title, an UPPERCASE spec line, then verdict + GET.
|
|
assert 'vdl-r-l1' in _VIEW and 'vdl-r-q' in _VIEW and 'vdl-r-title' in _VIEW
|
|
assert 'vdl-r-l2' in _VIEW and 'vdl-r-l3' in _VIEW and 'vdl-r-verdict' in _VIEW
|
|
assert '.vdl-r-q' in _CSS and '.vdl-r-l2' in _CSS and '.vdl-r-verdict' in _CSS
|
|
# the cinematic pill/badge language was redesigned out
|
|
assert 'vdl-info-tags' not in _VIEW and 'vdl-tag' not in _VIEW
|
|
assert 'vdl-q-res' not in _VIEW and 'vdl-flag' not in _VIEW
|
|
# sharp corners + monospace are the brutalist signature
|
|
assert 'border-radius: 0' in _CSS and 'monospace' in _CSS
|
|
|
|
|
|
# --- modal resumes an in-flight download on reopen ------------------------
|
|
|
|
def test_modal_resumes_active_download_on_reopen():
|
|
assert 'data-vdl-active' in _VIEW
|
|
assert 'function watchActiveDownload(' in _VIEW
|
|
assert '/api/video/downloads/status?media_id=' in _VIEW
|
|
# suppressed while a result card is already tracking inline (no double indicator)
|
|
assert "querySelector('[data-vdl-track]')" in _VIEW
|
|
assert '.vdl-active' in _CSS
|
|
|
|
|
|
# --- per-episode live tracking (TV parity with the movie inline tracker) ---
|
|
|
|
def test_episode_rows_get_live_download_status():
|
|
# each episode ROW shows its own Searching → Downloading % → Downloaded status
|
|
assert 'function epTrack(' in _VIEW and 'function epStatusRender(' in _VIEW
|
|
assert "/api/video/downloads/status?id=" in _VIEW
|
|
assert 'data-vdl-ep-status' in _VIEW
|
|
assert 'vdl-ep--dl-active' in _VIEW and 'vdl-ep--dl-done' in _VIEW and 'vdl-ep--dl-fail' in _VIEW
|
|
assert '.vdl-ep-dl' in _CSS and '.vdl-ep-dl-bar' in _CSS
|
|
|
|
|
|
def test_season_grab_lights_rows_and_resumes_on_reopen():
|
|
# season grab gives immediate per-row feedback (not headless), and reopening the
|
|
# modal resumes tracking in-flight episodes
|
|
assert 'function epSearching(' in _VIEW
|
|
assert 'eps.forEach(function (en) { epSearching(' in _VIEW # rows light up at once
|
|
assert 'function resumeEpisodeTracking(' in _VIEW
|
|
assert "/api/video/downloads/active" in _VIEW
|
|
assert 'resumeEpisodeTracking(container, st)' in _VIEW # called when rows build
|
|
|
|
|
|
def test_episode_grab_uses_the_shared_single_grab_path():
|
|
# the season batch grabs via the same payload as a manual grab (can't diverge)
|
|
assert 'function _pickAndGrab(' in _VIEW
|
|
assert 'sendGrab(buildGrabPayload(panel, best))' in _VIEW
|
|
|
|
|
|
# --- navigation plumbing --------------------------------------------------
|
|
|
|
def test_video_side_handles_navigate_event():
|
|
assert "'soulsync:video-navigate'" in _SIDE
|
|
assert 'navigate(pageId)' in _SIDE
|