diff --git a/tests/test_video_side_shell.py b/tests/test_video_side_shell.py index c18ec8f8..e7631f2d 100644 --- a/tests/test_video_side_shell.py +++ b/tests/test_video_side_shell.py @@ -235,6 +235,25 @@ def test_video_side_hides_music_api_config_and_shows_placeholders(): assert 'class="api-service-frame stg-service" data-service="tvdb"' in _INDEX +def test_dashboard_enrichment_buttons_present(): + block = _block( + _INDEX, r'
") + assert 'data-video-enrich="tmdb"' in block and 'data-video-enrich="tvdb"' in block + assert "data-video-manage-workers" in block + assert "video-enrich-spinner" in block # spins while running + assert "onclick" not in block + + +def test_video_enrichment_module_referenced_and_isolated(): + assert "video/video-enrichment.js" in _INDEX + src = (_ROOT / "webui" / "static" / "video" / "video-enrichment.js").read_text(encoding="utf-8") + assert src.strip().startswith("/*") or src.strip().startswith("(function") + assert "(function" in src and "})();" in src + assert "window." not in src + assert "/api/video/enrichment/" in src + assert "music" not in src.lower() + + def test_video_settings_module_referenced_and_isolated(): assert "video/video-settings.js" in _INDEX stripped = _VSETTINGS_JS.strip() diff --git a/webui/index.html b/webui/index.html index cc991879..11c998a8 100644 --- a/webui/index.html +++ b/webui/index.html @@ -434,8 +434,39 @@

System Dashboard

Monitor your video library, downloads and services

- + +
+
+ +
+
TMDB Enrichment
+
Idle
+
No active matches
+
0 / 0
+
+
+
+ +
+
TVDB Enrichment
+
Idle
+
No active matches
+
0 / 0
+
+
+ +
@@ -9013,6 +9044,8 @@ + + diff --git a/webui/static/video/video-enrichment.js b/webui/static/video/video-enrichment.js new file mode 100644 index 00000000..24780957 --- /dev/null +++ b/webui/static/video/video-enrichment.js @@ -0,0 +1,99 @@ +/* + * SoulSync — Video enrichment dashboard buttons (isolated). + * + * Polls /api/video/enrichment//status and reflects it on the dashboard + * TMDB/TVDB buttons (spin while running, tooltip status). Click toggles + * pause/resume. The Manage Workers button fires 'soulsync:video-open-workers' + * for the (isolated) video enrichment modal. Music code is never touched. + * + * Self-contained IIFE, no globals, no inline handlers. Only polls on the video + * side so the video engine isn't spun up while the user is on the music side. + */ +(function () { + 'use strict'; + + var SERVICES = ['tmdb', 'tvdb']; + + function onVideoSide() { + return document.body.getAttribute('data-side') === 'video'; + } + + function btn(svc) { return document.querySelector('[data-video-enrich="' + svc + '"]'); } + + function setText(root, sel, text) { + var n = root.querySelector(sel); + if (n) n.textContent = text; + } + + function reflect(svc, d) { + var b = btn(svc); + if (!b) return; + b.classList.remove('active', 'paused', 'complete', 'disabled'); + if (!d.enabled) b.classList.add('disabled'); + else if (d.idle) b.classList.add('complete'); + else if (d.running && !d.paused) b.classList.add('active'); + else if (d.paused) b.classList.add('paused'); + + var tip = document.querySelector('[data-video-enrich-tooltip="' + svc + '"]'); + if (!tip) return; + var status = !d.enabled ? 'Not configured' + : d.idle ? 'Complete' + : (d.running && !d.paused) ? 'Running' + : d.paused ? 'Paused' : 'Idle'; + setText(tip, '[data-video-enrich-status]', status); + var cur = (d.current_item && d.current_item.name) + ? ((d.current_item.type || 'item') + ': ' + d.current_item.name) + : (d.idle ? 'All matched' : 'No active matches'); + setText(tip, '[data-video-enrich-current]', cur); + var matched = 0, total = 0, prog = d.progress || {}; + for (var k in prog) { + if (Object.prototype.hasOwnProperty.call(prog, k)) { + matched += prog[k].matched || 0; + total += prog[k].total || 0; + } + } + setText(tip, '[data-video-enrich-progress]', matched + ' / ' + total); + } + + function pollOne(svc) { + fetch('/api/video/enrichment/' + svc + '/status', { headers: { 'Accept': 'application/json' } }) + .then(function (r) { return r.ok ? r.json() : null; }) + .then(function (d) { if (d && !d.error) reflect(svc, d); }) + .catch(function () { /* ignore */ }); + } + + function poll() { + if (onVideoSide() && !document.hidden) SERVICES.forEach(pollOne); + } + + function toggle(svc) { + var b = btn(svc); + if (!b || b.classList.contains('disabled')) return; + var action = b.classList.contains('paused') ? 'resume' : 'pause'; + fetch('/api/video/enrichment/' + svc + '/' + action, + { method: 'POST', headers: { 'Accept': 'application/json' } }) + .then(function () { setTimeout(function () { pollOne(svc); }, 200); }) + .catch(function () { /* ignore */ }); + } + + function init() { + SERVICES.forEach(function (svc) { + var b = btn(svc); + if (b) b.addEventListener('click', function () { toggle(svc); }); + }); + var manage = document.querySelector('[data-video-manage-workers]'); + if (manage) { + manage.addEventListener('click', function () { + document.dispatchEvent(new CustomEvent('soulsync:video-open-workers')); + }); + } + poll(); + setInterval(poll, 2500); + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } +})(); diff --git a/webui/static/video/video-side.css b/webui/static/video/video-side.css index 63760137..ec0e0e99 100644 --- a/webui/static/video/video-side.css +++ b/webui/static/video/video-side.css @@ -149,6 +149,86 @@ body[data-side="video"] .dashboard-header-sweep { display: none; } +/* ── Dashboard enrichment-worker buttons (isolated; music untouched) ────── */ +.video-enrich-container { position: relative; } +.video-enrich-button { + position: relative; + width: 46px; + height: 46px; + border-radius: 50%; + border: 2px solid rgba(var(--ve-accent, 88 101 242), 0.5); + background: rgba(18, 18, 22, 0.85); + color: #fff; + font-size: 11px; + font-weight: 700; + letter-spacing: 0.02em; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.15s ease; +} +.video-enrich-button:hover { transform: translateY(-1px); border-color: rgb(var(--ve-accent, 88 101 242)); } +.video-enrich-label { position: relative; z-index: 1; } +.video-enrich-spinner { + position: absolute; + inset: -2px; + border-radius: 50%; + border: 2px solid transparent; + border-top-color: rgb(var(--ve-accent, 88 101 242)); + opacity: 0; + transition: opacity 0.3s ease; +} +.video-enrich-button.active { box-shadow: 0 0 14px rgba(var(--ve-accent, 88 101 242), 0.45); } +.video-enrich-button.active .video-enrich-spinner { opacity: 1; animation: video-enrich-spin 1.1s linear infinite; } +.video-enrich-button.paused { opacity: 0.6; } +.video-enrich-button.complete { border-color: rgb(var(--ve-accent, 88 101 242)); } +.video-enrich-button.disabled { opacity: 0.4; cursor: default; } +@keyframes video-enrich-spin { to { transform: rotate(360deg); } } + +.video-enrich-tooltip { + position: absolute; + top: 54px; + left: 50%; + transform: translateX(-50%); + min-width: 190px; + padding: 10px 12px; + background: rgba(12, 12, 16, 0.97); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 10px; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5); + opacity: 0; + pointer-events: none; + transition: opacity 0.15s ease; + z-index: 60; + font-size: 12px; + text-align: left; +} +.video-enrich-container:hover .video-enrich-tooltip { opacity: 1; } +.video-enrich-tt-title { font-weight: 700; margin-bottom: 4px; color: var(--text-primary, #e8e8ea); } +.video-enrich-tt-status, .video-enrich-tt-current, .video-enrich-tt-progress { + color: var(--text-secondary, #9aa0aa); + line-height: 1.5; +} + +.video-manage-workers-btn { + display: inline-flex; + align-items: center; + gap: 8px; + height: 46px; + padding: 0 16px; + border-radius: 23px; + background: rgba(18, 18, 22, 0.85); + border: 1px solid rgba(255, 255, 255, 0.1); + color: var(--text-primary, #e8e8ea); + font-size: 13px; + font-weight: 600; + cursor: pointer; + transition: border-color 0.2s ease, transform 0.15s ease; +} +.video-manage-workers-btn:hover { transform: translateY(-1px); border-color: rgba(var(--accent-rgb, 88 101 242), 0.55); } +.video-manage-workers-icon img { width: 22px; height: 22px; display: block; } + /* ── Subpages: one built video page shown at a time ────────────────────── */ /* Each real page is a .video-subpage inside #video-page-host; the controller toggles the [hidden] attribute. Be explicit so no reused .dash-* rule can