soulsync/webui/static/video/video-scan.js
BoulderBadgeDad 7ab62674ed video side: reuse music styling for Tools + dashboard scan controls
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.
2026-06-13 23:50:41 -07:00

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();
}
})();