From b2dbd6dec5995369456584ee974db0c361a4cd44 Mon Sep 17 00:00:00 2001 From: BoulderBadgeDad Date: Mon, 15 Jun 2026 13:55:42 -0700 Subject: [PATCH] video: Video Source settings panel + library gating; move Detail prefs to own group MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Settings → Video Source shows which server video uses (✓ Plex/Jellyfin), a Plex/Jellyfin picker when both are connected, or a clear 'connect Plex or Jellyfin' message when neither (Navidrome/Standalone are music-only and not offered). The Library shows a non-breaking 'no video server' banner + disables Scan until one is connected. Detail Pages prefs moved into their own 'Video Preferences' group. /api/video/server GET+POST drives it. --- api/video/libraries.py | 26 ++++++++++++++++ tests/test_video_api.py | 1 + webui/index.html | 11 +++++++ webui/static/video/video-library.js | 33 ++++++++++++++++++++ webui/static/video/video-settings.js | 46 ++++++++++++++++++++++++++++ webui/static/video/video-side.css | 22 +++++++++++++ 6 files changed, 139 insertions(+) diff --git a/api/video/libraries.py b/api/video/libraries.py index a6ead6df..54fe6a9d 100644 --- a/api/video/libraries.py +++ b/api/video/libraries.py @@ -30,6 +30,32 @@ def register_routes(bp): logger.exception("Failed to list video libraries") return jsonify({"error": "Failed to list video libraries"}), 500 + @bp.route("/server", methods=["GET"]) + def video_server_status(): + """Which server the video side uses + which of Plex/Jellyfin are configured + (so the UI can show a picker, or a 'connect a server' message).""" + try: + from core.video.sources import resolve_video_server + from config.settings import config_manager + plex = bool((config_manager.get_plex_config() or {}).get("base_url")) + jelly = bool((config_manager.get_jellyfin_config() or {}).get("base_url")) + return jsonify({"server": resolve_video_server(), "plex": plex, "jellyfin": jelly}) + except Exception: + logger.exception("video server status failed") + return jsonify({"server": None, "plex": False, "jellyfin": False}) + + @bp.route("/server", methods=["POST"]) + def video_server_set(): + """Set the explicit video-side server pick (only meaningful when both Plex + and Jellyfin are configured).""" + from . import get_video_db + body = request.get_json(silent=True) or {} + choice = body.get("server") + if choice not in ("plex", "jellyfin"): + return jsonify({"error": "bad server"}), 400 + get_video_db().set_setting("video_server", choice) + return jsonify({"status": "saved", "server": choice}) + @bp.route("/libraries", methods=["POST"]) def save_video_libraries(): from . import get_video_db diff --git a/tests/test_video_api.py b/tests/test_video_api.py index d1ca6218..768295a0 100644 --- a/tests/test_video_api.py +++ b/tests/test_video_api.py @@ -34,6 +34,7 @@ def test_blueprint_exposes_dashboard_route(): assert "/api/video/scan/stop" in rules assert "/api/video/library" in rules assert "/api/video/libraries" in rules + assert "/api/video/server" in rules assert any(r.startswith("/api/video/poster/") for r in rules) assert "/api/video/enrichment/services" in rules assert "/api/video/enrichment//status" in rules diff --git a/webui/index.html b/webui/index.html index db72e38f..69c899d3 100644 --- a/webui/index.html +++ b/webui/index.html @@ -5133,6 +5133,13 @@ + +
+

Video Source

+
+
Checking…
+
+

API Configuration

@@ -5191,6 +5198,10 @@
+ + +
+

Video Preferences

diff --git a/webui/static/video/video-library.js b/webui/static/video/video-library.js index 4a397b0f..5da9a962 100644 --- a/webui/static/video/video-library.js +++ b/webui/static/video/video-library.js @@ -192,8 +192,41 @@ function onScanDone() { if (state.loaded) load(); } + // Gate the library/scan when no video server (Plex/Jellyfin) is connected — + // nothing breaks, the user is just told what to do. + function ensureServerBanner() { + var content = document.querySelector('#video-library-page .library-content'); + if (!content) return null; + var b = content.querySelector('[data-video-noserver]'); + if (!b) { + b = document.createElement('div'); + b.setAttribute('data-video-noserver', ''); + b.className = 'video-noserver hidden'; + b.innerHTML = 'No video server connected. Go to Settings → Video Source ' + + 'and connect Plex or Jellyfin to scan and browse your video library.'; + content.insertBefore(b, content.firstChild); + } + return b; + } + function checkServer() { + fetch('/api/video/server', { headers: { 'Accept': 'application/json' } }) + .then(function (r) { return r.ok ? r.json() : null; }) + .then(function (d) { + var ok = !!(d && d.server), banner = ensureServerBanner(); + if (banner) banner.classList.toggle('hidden', ok); + var scan = document.querySelector('[data-video-scan-mode]'); + if (scan) { + scan.disabled = !ok; + scan.style.opacity = ok ? '' : '0.45'; + scan.title = ok ? 'Scan the media server' : 'Connect Plex or Jellyfin in Settings first'; + } + }) + .catch(function () { /* ignore */ }); + } + function onPageShown(e) { if (!e || e.detail !== PAGE_ID) return; + checkServer(); if (!state.loaded) load(); } diff --git a/webui/static/video/video-settings.js b/webui/static/video/video-settings.js index 7df8ec67..4b5d1dda 100644 --- a/webui/static/video/video-settings.js +++ b/webui/static/video/video-settings.js @@ -13,6 +13,46 @@ var PAGE_ID = 'video-settings'; var URL = '/api/video/libraries'; var CONFIG_URL = '/api/video/enrichment/config'; + var SERVER_URL = '/api/video/server'; + + function esc(s) { + return String(s == null ? '' : s).replace(/&/g, '&').replace(//g, '>'); + } + + // ── Video Source (which Plex/Jellyfin the video side uses) ────────────── + function loadServer() { + var host = document.querySelector('[data-video-source-panel]'); + if (!host) return; + fetch(SERVER_URL, { headers: { 'Accept': 'application/json' } }) + .then(function (r) { return r.ok ? r.json() : null; }) + .then(function (d) { renderServer(host, d || {}); }) + .catch(function () { renderServer(host, {}); }); + } + function srvBtn(id, label, active) { + return ''; + } + function renderServer(host, d) { + var plex = !!d.plex, jelly = !!d.jellyfin; + if (!plex && !jelly) { + host.innerHTML = '
No video server connected. ' + + 'Connect Plex or Jellyfin under Server Connections ' + + 'below to scan and browse your video library. (Navidrome and Standalone are music-only.)
'; + } else if (plex && jelly) { + host.innerHTML = '
' + + srvBtn('plex', 'Plex', d.server) + srvBtn('jellyfin', 'Jellyfin', d.server) + '
' + + '
Both are connected — pick which one the video side uses.
'; + } else { + host.innerHTML = '
' + + 'Video uses ' + (plex ? 'Plex' : 'Jellyfin') + '
'; + } + } + function pickServer(id) { + fetch(SERVER_URL, { + method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, + body: JSON.stringify({ server: id }) + }).then(function () { loadServer(); load(); }).catch(function () { /* ignore */ }); + } function status(text) { var n = document.querySelector('[data-video-lib-status]'); @@ -129,6 +169,7 @@ function onPageShown(e) { if (e && e.detail !== PAGE_ID) return; + loadServer(); load(); loadKeys(); } @@ -149,6 +190,11 @@ if (autoplay) autoplay.addEventListener('change', savePrefs); var region = document.getElementById('video-watch-region'); if (region) region.addEventListener('change', savePrefs); + // Video source picker (delegated — the panel is rendered async). + document.addEventListener('click', function (e) { + var btn = e.target.closest('[data-video-server-pick]'); + if (btn) pickServer(btn.getAttribute('data-video-server-pick')); + }); // Per-connection Test buttons (same behaviour as music's testConnection). var testBtns = document.querySelectorAll('[data-video-test-service]'); for (var k = 0; k < testBtns.length; k++) { diff --git a/webui/static/video/video-side.css b/webui/static/video/video-side.css index 602dbf5b..55e4aedc 100644 --- a/webui/static/video/video-side.css +++ b/webui/static/video/video-side.css @@ -1465,3 +1465,25 @@ a.vd-guest:hover .vd-guest-name { color: rgb(var(--vd-accent-rgb)); } color: var(--text-primary, #fff); border-radius: 8px; padding: 7px 10px; font-size: 13px; cursor: pointer; } .vid-pref-row--select select option { background: #16161c; color: #fff; } + +/* ── settings: Video Source panel ────────────────────────────────────────── */ +.vid-source-using { font-size: 14px; color: var(--text-primary, #fff); display: flex; align-items: center; gap: 8px; } +.vid-source-check { color: #22c55e; font-weight: 800; } +.vid-source-none { font-size: 13.5px; line-height: 1.6; color: rgba(255, 255, 255, 0.72); } +.vid-source-none strong { color: #fff; } +.vid-source-pick { display: flex; gap: 10px; margin-bottom: 10px; } +.vid-source-btn { + padding: 8px 18px; border-radius: 8px; cursor: pointer; font-size: 13px; font-weight: 700; + background: rgba(255, 255, 255, 0.06); border: 1px solid rgba(255, 255, 255, 0.14); color: rgba(255, 255, 255, 0.8); + transition: all 0.18s ease; +} +.vid-source-btn:hover { background: rgba(255, 255, 255, 0.12); color: #fff; } +.vid-source-btn.active { background: rgba(56, 189, 248, 0.9); border-color: transparent; color: #fff; } + +/* library: no-server gate banner */ +.video-noserver { + margin: 0 0 18px; padding: 16px 18px; border-radius: 12px; font-size: 14px; line-height: 1.6; + color: rgba(255, 255, 255, 0.85); + background: rgba(245, 158, 11, 0.1); border: 1px solid rgba(245, 158, 11, 0.32); +} +.video-noserver.hidden { display: none; }