video: Video Source settings panel + library gating; move Detail prefs to own group
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.
This commit is contained in:
parent
f7d1b725d7
commit
b2dbd6dec5
6 changed files with 139 additions and 0 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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/<service>/status" in rules
|
||||
|
|
|
|||
|
|
@ -5133,6 +5133,13 @@
|
|||
<!-- Video API Configuration (placeholders; shown only on the video
|
||||
side in place of the music API services). Same structure as the
|
||||
music service frames above. -->
|
||||
<!-- Video Source (which Plex/Jellyfin the video side uses) -->
|
||||
<div class="settings-group" data-video-only>
|
||||
<h3>Video Source</h3>
|
||||
<div class="vid-source-panel" data-video-source-panel>
|
||||
<div class="callback-help">Checking…</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-group" data-stg="connections" data-video-only>
|
||||
<h3>API Configuration <button class="stg-accordion-toggle" onclick="toggleAllServiceAccordions(this)">Expand All</button></h3>
|
||||
<div class="api-service-frame stg-service" data-video-service="tmdb">
|
||||
|
|
@ -5191,6 +5198,10 @@
|
|||
<button class="test-button" type="button" data-video-test-service="omdb">Test OMDb</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Video Preferences (video-side only) -->
|
||||
<div class="settings-group" data-video-only>
|
||||
<h3>Video Preferences</h3>
|
||||
<div class="api-service-frame stg-service" data-video-service="prefs">
|
||||
<div class="stg-service-header" onclick="toggleStgService(this)">
|
||||
<span class="stg-service-dot" style="color: #38bdf8;"></span>
|
||||
|
|
|
|||
|
|
@ -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 <strong>Settings → Video Source</strong> ' +
|
||||
'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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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, '<').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 '<button class="vid-source-btn' + (id === active ? ' active' : '') +
|
||||
'" type="button" data-video-server-pick="' + id + '">' + label + '</button>';
|
||||
}
|
||||
function renderServer(host, d) {
|
||||
var plex = !!d.plex, jelly = !!d.jellyfin;
|
||||
if (!plex && !jelly) {
|
||||
host.innerHTML = '<div class="vid-source-none">No video server connected. ' +
|
||||
'Connect <strong>Plex</strong> or <strong>Jellyfin</strong> under <em>Server Connections</em> ' +
|
||||
'below to scan and browse your video library. <em>(Navidrome and Standalone are music-only.)</em></div>';
|
||||
} else if (plex && jelly) {
|
||||
host.innerHTML = '<div class="vid-source-pick">' +
|
||||
srvBtn('plex', 'Plex', d.server) + srvBtn('jellyfin', 'Jellyfin', d.server) + '</div>' +
|
||||
'<div class="callback-help">Both are connected — pick which one the video side uses.</div>';
|
||||
} else {
|
||||
host.innerHTML = '<div class="vid-source-using"><span class="vid-source-check">✓</span> ' +
|
||||
'Video uses <strong>' + (plex ? 'Plex' : 'Jellyfin') + '</strong></div>';
|
||||
}
|
||||
}
|
||||
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++) {
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
Loading…
Reference in a new issue