video side: Movies/TV library mapping UI on the settings page

Right next to music's 'Music Library' selector, the video side now shows
'Movies Library' + 'TV Shows Library' dropdowns (data-video-only, hidden on the
music side). video-settings.js populates them from /api/video/libraries when
Settings opens on the video side and saves the choice back; the scanner then
reads only those libraries. Isolated IIFE, data-attr wired. 83 tests green.
This commit is contained in:
BoulderBadgeDad 2026-06-14 00:26:56 -07:00
parent 5b0b64bf3b
commit 576199bb2f
4 changed files with 129 additions and 0 deletions

View file

@ -19,6 +19,7 @@ _JS = (_ROOT / "webui" / "static" / "video" / "video-side.js").read_text(encodin
_DASH_JS = (_ROOT / "webui" / "static" / "video" / "video-dashboard.js").read_text(encoding="utf-8")
_LIB_JS = (_ROOT / "webui" / "static" / "video" / "video-library.js").read_text(encoding="utf-8")
_SCAN_JS = (_ROOT / "webui" / "static" / "video" / "video-scan.js").read_text(encoding="utf-8")
_VSETTINGS_JS = (_ROOT / "webui" / "static" / "video" / "video-settings.js").read_text(encoding="utf-8")
_CSS_PATH = _ROOT / "webui" / "static" / "video" / "video-side.css"
EXPECTED_VIDEO_PAGES = {
@ -194,6 +195,28 @@ def test_video_settings_reuses_real_music_settings_page():
assert 'data-video-page="video-settings"] #video-page-host' in css
def test_video_library_mapping_ui_present_and_video_only():
# Movies/TV selectors live next to the music library selector, marked
# data-video-only so they show only on the video side.
assert 'data-video-lib-select="movies"' in _INDEX
assert 'data-video-lib-select="tv"' in _INDEX
assert "data-video-lib-save" in _INDEX
assert "data-video-only" in _INDEX
css = _CSS_PATH.read_text(encoding="utf-8")
assert 'body[data-side="music"] [data-video-only]' in css # hidden on music side
def test_video_settings_module_referenced_and_isolated():
assert "video/video-settings.js" in _INDEX
stripped = _VSETTINGS_JS.strip()
assert stripped.startswith("/*") or stripped.startswith("(function")
assert "(function" in _VSETTINGS_JS and "})();" in _VSETTINGS_JS
assert "window." not in _VSETTINGS_JS
assert "addEventListener" in _VSETTINGS_JS
assert "/api/video/libraries" in _VSETTINGS_JS
assert "soulsync:video-page-shown" in _VSETTINGS_JS
def test_controller_is_isolated_iife_with_no_globals():
stripped = _JS.strip()
# Wrapped in an IIFE → declares no module-level globals that could collide

View file

@ -4795,6 +4795,25 @@
Select which music library to use (doesn't affect config file)
</small>
</div>
<!-- Video side: which libraries are Movies / TV (shown only on the
video side; populated + saved by video/video-settings.js). -->
<div class="form-group video-library-selectors" data-video-only>
<label>Movies Library:</label>
<select data-video-lib-select="movies">
<option value="">Loading...</option>
</select>
<label style="margin-top: 12px;">TV Shows Library:</label>
<select data-video-lib-select="tv">
<option value="">Loading...</option>
</select>
<small style="color: #999; font-size: 0.9em; display: block; margin-top: 5px;">
Which server libraries the video side scans (doesn't affect config file)
</small>
<div style="margin-top: 8px;">
<button type="button" class="detect-button" data-video-lib-save>Save Libraries</button>
<span data-video-lib-status style="margin-left: 10px; font-size: 0.9em; color: #999;"></span>
</div>
</div>
<div class="form-actions">
<button id="plex-config-action-button" class="detect-button" onclick="clearPlexConfiguration()">Clear Configuration</button>
</div>
@ -8889,6 +8908,8 @@
<script src="{{ url_for('static', filename='video/video-side.js', v=static_v) }}"></script>
<!-- Shared video scan controller (triggers + polls scans for all video pages) -->
<script src="{{ url_for('static', filename='video/video-scan.js', v=static_v) }}"></script>
<!-- Video settings additions (isolated; library mapping on the settings page) -->
<script src="{{ url_for('static', filename='video/video-settings.js', v=static_v) }}"></script>
<!-- Video dashboard data layer (isolated; populates the dashboard from the video DB) -->
<script src="{{ url_for('static', filename='video/video-dashboard.js', v=static_v) }}"></script>
<!-- Video library page (isolated; lists movies/shows + scan trigger) -->

View file

@ -0,0 +1,80 @@
/*
* SoulSync Video settings additions (isolated).
*
* The video side shows the real music settings page; this module only drives the
* VIDEO-specific bits added to it for now, the Movies/TV library mapping
* (which server library the scan reads). It populates the dropdowns from
* /api/video/libraries when the Settings page is shown on the video side and
* saves the choice back. Self-contained IIFE, no globals, no inline handlers.
*/
(function () {
'use strict';
var PAGE_ID = 'video-settings';
var URL = '/api/video/libraries';
function status(text) {
var n = document.querySelector('[data-video-lib-status]');
if (n) n.textContent = text || '';
}
function fill(select, items, selected) {
if (!select) return;
select.textContent = '';
var none = document.createElement('option');
none.value = '';
none.textContent = '— None —';
select.appendChild(none);
for (var i = 0; i < items.length; i++) {
var opt = document.createElement('option');
opt.value = items[i].title;
opt.textContent = items[i].title;
if (items[i].title === selected) opt.selected = true;
select.appendChild(opt);
}
}
function load() {
status('Loading…');
fetch(URL, { headers: { 'Accept': 'application/json' } })
.then(function (r) { return r.ok ? r.json() : null; })
.then(function (d) {
if (!d || d.error) { status('Could not load libraries'); return; }
var sel = d.selected || {};
fill(document.querySelector('[data-video-lib-select="movies"]'), d.movies || [], sel.movies);
fill(document.querySelector('[data-video-lib-select="tv"]'), d.tv || [], sel.tv);
status('');
})
.catch(function () { status('Could not load libraries'); });
}
function save() {
var m = document.querySelector('[data-video-lib-select="movies"]');
var t = document.querySelector('[data-video-lib-select="tv"]');
status('Saving…');
fetch(URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
body: JSON.stringify({ movies: m ? m.value : '', tv: t ? t.value : '' })
})
.then(function (r) { return r.json(); })
.then(function () { status('Saved'); })
.catch(function () { status('Save failed'); });
}
function onPageShown(e) {
if (e && e.detail === PAGE_ID) load();
}
function init() {
var btn = document.querySelector('[data-video-lib-save]');
if (btn) btn.addEventListener('click', save);
document.addEventListener('soulsync:video-page-shown', onPageShown);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();

View file

@ -117,6 +117,11 @@ body[data-side="video"][data-video-page="video-settings"] #settings-page {
body[data-side="video"][data-video-page="video-settings"] #video-page-host {
display: none !important;
}
/* Video-only additions inside the shared settings page (e.g. the Movies/TV
library mapping) hidden on the music side. */
body[data-side="music"] [data-video-only] {
display: none !important;
}
/* ── Dashboard header: match music, minus the sweep animation ──────────── */
/* The header reuses music's .dashboard-header markup/CSS for an identical look;