The visuals were off because I'd invented CSS/markup instead of reusing the shared design system. Fixed to match music exactly: - Dashboard Library card now uses music's full markup — header icon, Refresh/ Deep Scan buttons WITH their icons, and stat rows with icons (movies/shows/ episodes/disk). Same .library-status-* classes, no custom CSS. - Tools 'Library Scan' card now mirrors the music Database Updater: a mode dropdown (Incremental/Full Refresh/Deep Scan) + one Scan button inside .tool-card-controls + the standard progress bar. Styling comes for free from the generic music classes. - Dropped bespoke .video-tool-btn/.video-scan-controls CSS and folded the separate video-tools.js into the shared video-scan.js (one fewer file). JS stays isolated only because it must hit /api/video + update video DOM. 110 tests green.
135 lines
4.9 KiB
JavaScript
135 lines
4.9 KiB
JavaScript
/*
|
|
* SoulSync — shared VIDEO scan controller (isolated).
|
|
*
|
|
* One place triggers + polls library scans for every surface (Library page,
|
|
* Tools page, Dashboard card), so nothing duplicates the fetch/poll logic. It
|
|
* reuses the music CSS/markup — it just targets /api/video/* and updates the
|
|
* video DOM.
|
|
*
|
|
* Triggers:
|
|
* - click on [data-video-scan-mode="full|incremental|deep"] (or [data-video-scan] = full)
|
|
* - click on [data-video-scan-run], reading the mode from [data-video-scan-select]
|
|
* - a 'soulsync:video-scan-start' event {detail:{mode}}
|
|
*
|
|
* Updates the Tools progress widgets ([data-video-scan-phase|bar|detail]) and
|
|
* emits 'soulsync:video-scan-progress' / 'soulsync:video-scan-done' so the
|
|
* Library page and Dashboard can react too. Self-contained IIFE, no globals.
|
|
*/
|
|
(function () {
|
|
'use strict';
|
|
|
|
var REQUEST_URL = '/api/video/scan/request';
|
|
var STATUS_URL = '/api/video/scan/status';
|
|
var scanning = false;
|
|
|
|
function emit(name, detail) {
|
|
document.dispatchEvent(new CustomEvent(name, { detail: detail }));
|
|
}
|
|
|
|
function setText(sel, text) {
|
|
var n = document.querySelector(sel);
|
|
if (n) n.textContent = text;
|
|
}
|
|
|
|
function setBar(pct) {
|
|
var bar = document.querySelector('[data-video-scan-bar]');
|
|
if (bar) bar.style.width = pct + '%';
|
|
}
|
|
|
|
function counts(s) {
|
|
return (s.movies || 0) + ' movies, ' + (s.shows || 0) + ' shows';
|
|
}
|
|
|
|
function reflectProgress(s) {
|
|
var phase = (s.phase || 'scanning');
|
|
setText('[data-video-scan-phase]', phase.charAt(0).toUpperCase() + phase.slice(1));
|
|
setText('[data-video-scan-detail]', counts(s));
|
|
setBar(100);
|
|
}
|
|
|
|
function reflectDone(s) {
|
|
var run = document.querySelector('[data-video-scan-run]');
|
|
if (run) run.disabled = false;
|
|
if (s.state === 'error') {
|
|
setText('[data-video-scan-phase]', 'Failed');
|
|
setText('[data-video-scan-detail]', s.error || 'Scan failed');
|
|
setBar(0);
|
|
} else {
|
|
setText('[data-video-scan-phase]', 'Complete');
|
|
setText('[data-video-scan-detail]',
|
|
counts(s) + (s.removed ? ', ' + s.removed + ' removed' : ''));
|
|
setBar(100);
|
|
}
|
|
}
|
|
|
|
function poll() {
|
|
fetch(STATUS_URL, { headers: { 'Accept': 'application/json' } })
|
|
.then(function (r) { return r.json(); })
|
|
.then(function (s) {
|
|
if (s && s.state === 'scanning') {
|
|
reflectProgress(s);
|
|
emit('soulsync:video-scan-progress', s);
|
|
setTimeout(poll, 1500);
|
|
} else {
|
|
scanning = false;
|
|
reflectDone(s || { state: 'idle' });
|
|
emit('soulsync:video-scan-done', s || { state: 'idle' });
|
|
}
|
|
})
|
|
.catch(function () {
|
|
scanning = false;
|
|
reflectDone({ state: 'error', error: 'Could not reach server' });
|
|
emit('soulsync:video-scan-done', { state: 'error' });
|
|
});
|
|
}
|
|
|
|
function start(mode) {
|
|
if (scanning) return;
|
|
scanning = true;
|
|
var run = document.querySelector('[data-video-scan-run]');
|
|
if (run) run.disabled = true;
|
|
var pending = { state: 'scanning', phase: 'starting', mode: mode, movies: 0, shows: 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 })
|
|
})
|
|
.then(function () { setTimeout(poll, 600); })
|
|
.catch(function () {
|
|
scanning = false;
|
|
reflectDone({ state: 'error' });
|
|
emit('soulsync:video-scan-done', { state: 'error' });
|
|
});
|
|
}
|
|
|
|
function init() {
|
|
var triggers = document.querySelectorAll('[data-video-scan-mode],[data-video-scan]');
|
|
for (var i = 0; i < triggers.length; i++) {
|
|
(function (el) {
|
|
el.addEventListener('click', function (e) {
|
|
e.preventDefault();
|
|
start(el.getAttribute('data-video-scan-mode') || 'full');
|
|
});
|
|
})(triggers[i]);
|
|
}
|
|
var run = document.querySelector('[data-video-scan-run]');
|
|
if (run) {
|
|
run.addEventListener('click', function (e) {
|
|
e.preventDefault();
|
|
var sel = document.querySelector('[data-video-scan-select]');
|
|
start(sel ? sel.value : 'full');
|
|
});
|
|
}
|
|
document.addEventListener('soulsync:video-scan-start', function (e) {
|
|
start((e.detail && e.detail.mode) || 'full');
|
|
});
|
|
}
|
|
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
} else {
|
|
init();
|
|
}
|
|
})();
|