From b26f664326a580220df66352183fc9615823794b Mon Sep 17 00:00:00 2001 From: BoulderBadgeDad Date: Sun, 21 Jun 2026 23:27:35 -0700 Subject: [PATCH] Video tools: scan a Movies OR TV library (not just both) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The video Library Scan tool only scanned 'all' — but movies and TV are independent libraries (unlike music's single library). The scanner backend already supported media_type='movie'|'show'|'all'; this just wires it up: - /api/video/scan/request now reads media_type and threads it to request_scan - the Tools card gains a target selector (All / Movies Only / TV Shows Only) alongside the existing mode dropdown, matching the music scan's UX - the live status detail reflects the target (no confusing '0 shows' on a movies-only scan) Seam test: the endpoint passes both mode and media_type through (default all/full, explicit movie/deep, TV-only). Existing scanner media-type/scope tests unchanged. --- api/video/scan.py | 5 ++++- tests/test_video_api.py | 32 ++++++++++++++++++++++++++++++++ webui/index.html | 7 ++++++- webui/static/video/video-scan.js | 18 +++++++++++++----- 4 files changed, 55 insertions(+), 7 deletions(-) diff --git a/api/video/scan.py b/api/video/scan.py index 99e83984..06c3eb58 100644 --- a/api/video/scan.py +++ b/api/video/scan.py @@ -24,8 +24,11 @@ def register_routes(bp): from core.video.sources import get_active_video_source body = request.get_json(silent=True) or {} mode = body.get("mode", "full") + # Which library to scan — movies and TV are independent libraries, so the + # UI can target one or both. The scanner normalizes/validates it. + media_type = body.get("media_type", "all") scanner = get_video_scanner(get_video_db()) - return jsonify(scanner.request_scan(get_active_video_source, mode)) + return jsonify(scanner.request_scan(get_active_video_source, mode, media_type)) @bp.route("/scan/status", methods=["GET"]) def video_scan_status(): diff --git a/tests/test_video_api.py b/tests/test_video_api.py index 8b302829..72c89cc6 100644 --- a/tests/test_video_api.py +++ b/tests/test_video_api.py @@ -62,6 +62,38 @@ def test_blueprint_exposes_dashboard_route(): assert "/api/video/discover/trailer" in rules +def test_scan_request_threads_mode_and_media_type(tmp_path, monkeypatch): + # The Tools-page scan can target one library (movies / TV) or both. The + # endpoint must pass BOTH mode and media_type through to the scanner — movies + # and TV are independent, so a TV scan must never touch movies. + client, _ = _make_client(tmp_path) + calls = {} + + class _FakeScanner: + def request_scan(self, source_factory, mode="full", media_type="all"): + calls["mode"] = mode + calls["media_type"] = media_type + return {"status": "started", "mode": mode, "media_type": media_type} + + import core.video.scanner as scanner_mod + import core.video.sources as sources_mod + monkeypatch.setattr(scanner_mod, "get_video_scanner", lambda db: _FakeScanner()) + monkeypatch.setattr(sources_mod, "get_active_video_source", lambda: None) + + # default body → both libraries, full + assert client.post("/api/video/scan/request", json={}).get_json()["media_type"] == "all" + assert calls == {"mode": "full", "media_type": "all"} + + # explicit movies-only deep scan + r = client.post("/api/video/scan/request", json={"mode": "deep", "media_type": "movie"}) + assert calls == {"mode": "deep", "media_type": "movie"} + assert r.get_json() == {"status": "started", "mode": "deep", "media_type": "movie"} + + # TV-only + client.post("/api/video/scan/request", json={"media_type": "show"}) + assert calls["media_type"] == "show" + + def test_discover_trailer_returns_key(tmp_path, monkeypatch): client, _ = _make_client(tmp_path) import core.video.enrichment.engine as eng_mod diff --git a/webui/index.html b/webui/index.html index 9ae5ee89..0d416577 100644 --- a/webui/index.html +++ b/webui/index.html @@ -1616,7 +1616,12 @@
- + + + + +