Video tools: scan a Movies OR TV library (not just both)
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.
This commit is contained in:
parent
66bce2b83a
commit
b26f664326
4 changed files with 55 additions and 7 deletions
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1616,7 +1616,12 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="tool-card-controls">
|
||||
<select data-video-scan-select>
|
||||
<select data-video-scan-target aria-label="Which library to scan" title="Movies and TV are separate libraries — scan one or both">
|
||||
<option value="all">All Libraries</option>
|
||||
<option value="movie">Movies Only</option>
|
||||
<option value="show">TV Shows Only</option>
|
||||
</select>
|
||||
<select data-video-scan-select aria-label="Scan type">
|
||||
<option value="incremental">Incremental Update</option>
|
||||
<option value="full">Full Refresh</option>
|
||||
<option value="deep">Deep Scan</option>
|
||||
|
|
|
|||
|
|
@ -37,6 +37,11 @@
|
|||
}
|
||||
|
||||
function counts(s) {
|
||||
// Reflect the scan target so a movies-only / TV-only scan doesn't show a
|
||||
// confusing "0 shows" half. 'all' shows both.
|
||||
var mt = s.media_type || 'all';
|
||||
if (mt === 'movie') return (s.movies || 0) + ' movies';
|
||||
if (mt === 'show') return (s.shows || 0) + ' shows, ' + (s.episodes || 0) + ' episodes';
|
||||
return (s.movies || 0) + ' movies, ' + (s.shows || 0) + ' shows';
|
||||
}
|
||||
|
||||
|
|
@ -134,16 +139,18 @@
|
|||
});
|
||||
}
|
||||
|
||||
function start(mode) {
|
||||
function start(mode, mediaType) {
|
||||
if (scanning) return;
|
||||
scanning = true;
|
||||
var pending = { state: 'scanning', phase: 'starting', mode: mode, movies: 0, shows: 0, percent: 0 };
|
||||
mediaType = mediaType || 'all'; // 'all' | 'movie' | 'show'
|
||||
var pending = { state: 'scanning', phase: 'starting', mode: mode,
|
||||
media_type: mediaType, movies: 0, shows: 0, percent: 0 };
|
||||
reflectProgress(pending);
|
||||
emit('soulsync:video-scan-progress', pending);
|
||||
fetch(REQUEST_URL, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
|
||||
body: JSON.stringify({ mode: mode })
|
||||
body: JSON.stringify({ mode: mode, media_type: mediaType })
|
||||
})
|
||||
.then(function () { setTimeout(poll, 600); })
|
||||
.catch(function () {
|
||||
|
|
@ -193,11 +200,12 @@
|
|||
e.preventDefault();
|
||||
if (scanning) { stop(); return; } // button doubles as Cancel mid-scan
|
||||
var sel = document.querySelector('[data-video-scan-select]');
|
||||
start(sel ? sel.value : 'full');
|
||||
var tgt = document.querySelector('[data-video-scan-target]');
|
||||
start(sel ? sel.value : 'full', tgt ? tgt.value : 'all');
|
||||
});
|
||||
}
|
||||
document.addEventListener('soulsync:video-scan-start', function (e) {
|
||||
start((e.detail && e.detail.mode) || 'full');
|
||||
start((e.detail && e.detail.mode) || 'full', (e.detail && e.detail.media_type) || 'all');
|
||||
});
|
||||
document.addEventListener('soulsync:video-page-shown', function (e) {
|
||||
if (e && e.detail === 'video-tools') { loadToolStats(); resumeIfScanning(); }
|
||||
|
|
|
|||
Loading…
Reference in a new issue