';
}).join('');
}
var seg = document.getElementById('vq-codec');
if (seg) {
Array.prototype.forEach.call(seg.querySelectorAll('[data-vq-codec]'), function (b) {
b.classList.toggle('active', b.getAttribute('data-vq-codec') === p.codec);
});
}
var hdr = document.getElementById('vq-prefer-hdr'); if (hdr) hdr.checked = !!p.prefer_hdr;
var fb = document.getElementById('vq-fallback'); if (fb) fb.checked = p.fallback_enabled !== false;
var sl = document.getElementById('vq-max-size'); if (sl) sl.value = p.max_size_gb || 0;
var lab = document.getElementById('vq-size-label');
if (lab) lab.textContent = p.max_size_gb ? (p.max_size_gb + ' GB') : 'No limit';
}
function moveRes(k, dir) {
var p = _videoQuality; if (!p) return;
var keys = Object.keys(p.resolutions).sort(function (a, b) {
return p.resolutions[a].priority - p.resolutions[b].priority;
});
var i = keys.indexOf(k), j = i + dir;
if (j < 0 || j >= keys.length) return;
keys[i] = keys[j]; keys[j] = k; // swap
keys.forEach(function (key, idx) { p.resolutions[key].priority = idx + 1; });
renderQuality(); saveQuality(true);
}
function moveSrc(s, dir) {
var p = _videoQuality; if (!p) return;
var arr = p.source_priority, i = arr.indexOf(s), j = i + dir;
if (j < 0 || j >= arr.length) return;
arr[i] = arr[j]; arr[j] = s;
renderQuality(); saveQuality(true);
}
function saveQuality(silent) {
if (!_videoQuality) return Promise.resolve();
return fetch(QUALITY_URL, {
method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
body: JSON.stringify(_videoQuality)
}).then(function (r) { return r.ok ? r.json() : null; })
.then(function (d) { if (d) _videoQuality = d; if (!silent) toast('Quality profile saved', 'success'); })
.catch(function () { /* ignore */ });
}
// Delegated handlers for the quality profile (rows re-render, so delegate).
function wireQuality() {
var sec = document.getElementById('vq-resolution-rows');
if (!sec) return;
var card = sec.closest('.settings-group');
if (!card || card._vqWired) return;
card._vqWired = true;
card.addEventListener('click', function (e) {
var rm = e.target.closest('[data-vq-res-move]');
if (rm) { moveRes(rm.getAttribute('data-vq-res-move'), parseInt(rm.getAttribute('data-dir'), 10)); return; }
var sm = e.target.closest('[data-vq-src-move]');
if (sm) { moveSrc(sm.getAttribute('data-vq-src-move'), parseInt(sm.getAttribute('data-dir'), 10)); return; }
var cd = e.target.closest('[data-vq-codec]');
if (cd && _videoQuality) { _videoQuality.codec = cd.getAttribute('data-vq-codec'); renderQuality(); saveQuality(true); }
});
card.addEventListener('change', function (e) {
if (!_videoQuality) return;
var rt = e.target.closest('[data-vq-res-toggle]');
if (rt) { _videoQuality.resolutions[rt.getAttribute('data-vq-res-toggle')].enabled = rt.checked; renderQuality(); saveQuality(true); return; }
if (e.target.id === 'vq-prefer-hdr') { _videoQuality.prefer_hdr = e.target.checked; saveQuality(true); return; }
if (e.target.id === 'vq-fallback') { _videoQuality.fallback_enabled = e.target.checked; saveQuality(true); return; }
if (e.target.id === 'vq-max-size') { _videoQuality.max_size_gb = parseInt(e.target.value, 10) || 0; saveQuality(true); return; }
});
card.addEventListener('input', function (e) {
if (e.target.id === 'vq-max-size' && _videoQuality) {
var v = parseInt(e.target.value, 10) || 0;
var lab = document.getElementById('vq-size-label');
if (lab) lab.textContent = v ? (v + ' GB') : 'No limit';
}
});
}
function saveKeys(silent) {
var t = document.getElementById('tmdb-api-key');
var v = document.getElementById('tvdb-api-key');
var o = document.getElementById('omdb-api-key');
var fa = document.getElementById('fanart-api-key');
var sub = document.getElementById('opensubtitles-api-key');
var trakt = document.getElementById('trakt-api-key');
var ryd = document.getElementById('video-ryd-enabled');
var sb = document.getElementById('video-sponsorblock-enabled');
var dea = document.getElementById('video-dearrow-enabled');
var tvm = document.getElementById('video-tvmaze-enabled');
var anl = document.getElementById('video-anilist-enabled');
var wkd = document.getElementById('video-wikidata-enabled');
return fetch(CONFIG_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
body: JSON.stringify({
tmdb_api_key: t ? t.value : '', tvdb_api_key: v ? v.value : '',
omdb_api_key: o ? o.value : '',
fanart_api_key: fa ? fa.value : '',
opensubtitles_api_key: sub ? sub.value : '',
trakt_api_key: trakt ? trakt.value : '',
ryd_enabled: ryd ? ryd.checked : true,
sponsorblock_enabled: sb ? sb.checked : true,
dearrow_enabled: dea ? dea.checked : true,
tvmaze_enabled: tvm ? tvm.checked : true,
anilist_enabled: anl ? anl.checked : false,
wikidata_enabled: wkd ? wkd.checked : true,
})
}).then(function () { if (!silent) toast('API keys saved', 'success'); })
.catch(function () { /* ignore */ });
}
function toast(msg, type) {
if (typeof showToast === 'function') showToast(msg, type); // shared shell helper
}
// Mirrors music's testConnection(): save the key, then hit the test
// endpoint, then toast the result. Isolated -> /api/video/enrichment//test.
function testConnection(svc) {
var name = svc.toUpperCase();
toast('Testing ' + name + ' connection…', 'info');
saveKeys(true).then(function () {
return fetch('/api/video/enrichment/' + svc + '/test',
{ method: 'POST', headers: { 'Accept': 'application/json' } });
}).then(function (r) { return r.json(); }).then(function (res) {
if (res && res.success) toast(res.message || (name + ' connection successful'), 'success');
else toast(name + ' connection failed: ' + ((res && res.error) || 'unknown'), 'error');
}).catch(function () { toast('Failed to test ' + name + ' connection', 'error'); });
}
function onPageShown(e) {
if (e && e.detail !== PAGE_ID) return;
loadServer();
loadConn();
load();
loadKeys();
loadDownloads();
wireDownloads();
loadQuality();
wireQuality();
loadSlskd();
wireSlskd();
}
function init() {
// Save the moment a library is picked — same behaviour as the music
// 'Music Library' selector above (which saves on change, no button).
// NB: wrap each handler so the DOM Event isn't passed as the function's
// first arg (which is our `silent` flag — it would suppress the toast).
var selects = document.querySelectorAll('[data-video-lib-select]');
for (var i = 0; i < selects.length; i++) {
selects[i].addEventListener('change', function () { save(); });
}
// Enrichment keys save on blur/change (turns the workers on).
['tmdb-api-key', 'tvdb-api-key', 'omdb-api-key',
'fanart-api-key', 'opensubtitles-api-key', 'trakt-api-key',
'video-ryd-enabled', 'video-sponsorblock-enabled', 'video-dearrow-enabled',
'video-tvmaze-enabled', 'video-anilist-enabled',
'video-wikidata-enabled'].forEach(function (id) {
var el = document.getElementById(id);
if (el) el.addEventListener('change', function () { saveKeys(); });
});
var autoplay = document.getElementById('video-billboard-autoplay');
if (autoplay) autoplay.addEventListener('change', function () { savePrefs(); });
var region = document.getElementById('video-watch-region');
if (region) region.addEventListener('change', function () { savePrefs(); });
// Server toggle (Plex/Jellyfin) — select + reveal that server's config.
var toggles = document.querySelectorAll('[data-video-server-toggle]');
for (var t = 0; t < toggles.length; t++) {
(function (b) {
b.addEventListener('click', function () {
pickServer(b.getAttribute('data-video-server-toggle'));
});
})(toggles[t]);
}
// Server Connection (video's own creds) — save on change, test on click.
var connInputs = document.querySelectorAll('[data-video-conn]');
for (var c = 0; c < connInputs.length; c++) {
connInputs[c].addEventListener('change', function () { saveConn(); });
}
var connTests = document.querySelectorAll('[data-video-conn-test]');
for (var d = 0; d < connTests.length; d++) {
(function (b) {
b.addEventListener('click', function () {
testConn(b.getAttribute('data-video-conn-test'));
});
})(connTests[d]);
}
// Jellyfin user pick → store it + refresh that user's libraries.
var userSel = document.querySelector('[data-video-jellyfin-user]');
if (userSel) userSel.addEventListener('change', function () { selectJellyfinUser(userSel.value); });
// 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++) {
(function (b) {
b.addEventListener('click', function () {
testConnection(b.getAttribute('data-video-test-service'));
});
})(testBtns[k]);
}
// The shared "Save Settings" button belongs to MUSIC (it runs music's
// saveSettings, which would fire a music-config write from the video page).
// On the video side we intercept it (capture phase, before music's bubble
// listener), block music's handler, flush all video settings, and toast —
// so the button is real here and can't reach into music. Music side: this
// does nothing, so its behaviour is unchanged.
document.addEventListener('click', function (e) {
if (document.body.getAttribute('data-side') !== 'video') return;
if (!e.target.closest('#save-settings')) return;
e.preventDefault();
e.stopImmediatePropagation();
Promise.all([saveConn(true), save(true), saveKeys(true), savePrefs(true),
saveDownloads(true), saveQuality(true), saveSlskd(true)])
.then(function () { toast('Settings saved', 'success'); })
.catch(function () { toast('Some settings could not be saved', 'error'); });
}, true);
document.addEventListener('soulsync:video-page-shown', onPageShown);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();