surface the youtube_template (built into organization.py) on Settings -> Library Organization, next to the movie/episode templates: input + variable hints ($channel/$year/$date/$month/$day/$title/$videoid) + live preview, wired into load/collect/save/reset. backend already round-trips it (load/save normalize).
905 lines
49 KiB
JavaScript
905 lines
49 KiB
JavaScript
/*
|
||
* 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';
|
||
var CONFIG_URL = '/api/video/enrichment/config';
|
||
var SERVER_URL = '/api/video/server';
|
||
var CONN_URL = '/api/video/server-config';
|
||
var DOWNLOADS_URL = '/api/video/downloads/config';
|
||
var QUALITY_URL = '/api/video/downloads/quality';
|
||
var YT_QUALITY_URL = '/api/video/downloads/youtube-quality';
|
||
var SLSKD_URL = '/api/video/downloads/slskd';
|
||
var _videoQuality = null;
|
||
var _videoYtQuality = null;
|
||
// Pretty labels for the source×resolution quality ladder (keys come from the backend).
|
||
var TIER_LABEL = {
|
||
'remux-2160p': 'Remux · 4K', 'bluray-2160p': 'BluRay · 4K', 'web-2160p': 'WEB · 4K',
|
||
'remux-1080p': 'Remux · 1080p', 'bluray-1080p': 'BluRay · 1080p', 'web-1080p': 'WEB-DL · 1080p',
|
||
'webrip-1080p': 'WEBRip · 1080p', 'hdtv-1080p': 'HDTV · 1080p',
|
||
'bluray-720p': 'BluRay · 720p', 'web-720p': 'WEB-DL · 720p', 'hdtv-720p': 'HDTV · 720p',
|
||
'dvd': 'DVD', 'sdtv': 'SDTV'
|
||
};
|
||
var REJECT_LABEL = {
|
||
'cam': 'CAM / TS', 'screener': 'Screener', 'workprint': 'Workprint', '3d': '3D', 'x264': 'x264 / AVC'
|
||
};
|
||
var REJECT_ORDER = ['cam', 'screener', 'workprint', '3d', 'x264'];
|
||
|
||
function esc(s) {
|
||
return String(s == null ? '' : s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||
}
|
||
|
||
// ── Server Connection ───────────────────────────────────────────────────
|
||
// Mirrors the MUSIC server picker (toggle = select + configure), scoped to
|
||
// Plex/Jellyfin. Clicking a toggle reveals that server's creds AND sets it as
|
||
// the active video server. Creds are video's own (video.db), pre-filled from
|
||
// music; the picker writes only to /api/video/* — never the music config.
|
||
function connEl(name) { return document.querySelector('[data-video-conn="' + name + '"]'); }
|
||
function note(server, text) {
|
||
var n = document.querySelector('[data-video-conn-note="' + server + '"]');
|
||
if (n) n.textContent = text || '';
|
||
}
|
||
function showServerConfig(server) {
|
||
var btns = document.querySelectorAll('[data-video-server-toggle]');
|
||
for (var i = 0; i < btns.length; i++) {
|
||
btns[i].classList.toggle('active', btns[i].getAttribute('data-video-server-toggle') === server);
|
||
}
|
||
var cfgs = document.querySelectorAll('[data-video-server-config]');
|
||
for (var j = 0; j < cfgs.length; j++) {
|
||
cfgs[j].classList.toggle('hidden', cfgs[j].getAttribute('data-video-server-config') !== server);
|
||
}
|
||
}
|
||
// Which server's config to show on load: the explicit pick, else the
|
||
// configured one, else Plex (so there's always a panel to fill in).
|
||
function loadServer() {
|
||
fetch(SERVER_URL, { headers: { 'Accept': 'application/json' } })
|
||
.then(function (r) { return r.ok ? r.json() : null; })
|
||
.then(function (d) {
|
||
d = d || {};
|
||
var active = d.server || (d.jellyfin && !d.plex ? 'jellyfin' : 'plex');
|
||
showServerConfig(active);
|
||
if (active === 'jellyfin') loadJellyfinUsers();
|
||
})
|
||
.catch(function () { showServerConfig('plex'); });
|
||
}
|
||
function loadConn() {
|
||
fetch(CONN_URL, { headers: { 'Accept': 'application/json' } })
|
||
.then(function (r) { return r.ok ? r.json() : null; })
|
||
.then(function (d) {
|
||
if (!d) return;
|
||
var p = d.plex || {}, j = d.jellyfin || {};
|
||
var pu = connEl('plex-url'); if (pu) pu.value = p.base_url || '';
|
||
var pt = connEl('plex-token'); if (pt) pt.value = p.has_token ? p.token : '';
|
||
var ju = connEl('jellyfin-url'); if (ju) ju.value = j.base_url || '';
|
||
var jk = connEl('jellyfin-key'); if (jk) jk.value = j.has_key ? j.api_key : '';
|
||
note('plex', p.base_url
|
||
? (p.inherited ? 'Inherited from your Music Plex connection — edit to use a different server for video.'
|
||
: 'Custom video connection.')
|
||
: 'Not connected — add a server URL and token.');
|
||
note('jellyfin', j.base_url
|
||
? (j.inherited ? 'Inherited from your Music Jellyfin connection — edit to use a different server for video.'
|
||
: 'Custom video connection.')
|
||
: 'Not connected — add a server URL and API key.');
|
||
})
|
||
.catch(function () { /* ignore */ });
|
||
}
|
||
function saveConn(silent) {
|
||
var pu = connEl('plex-url'), pt = connEl('plex-token');
|
||
var ju = connEl('jellyfin-url'), jk = connEl('jellyfin-key');
|
||
return fetch(CONN_URL, {
|
||
method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
|
||
body: JSON.stringify({
|
||
plex: { base_url: pu ? pu.value : '', token: pt ? pt.value : '' },
|
||
jellyfin: { base_url: ju ? ju.value : '', api_key: jk ? jk.value : '' }
|
||
})
|
||
}).then(function () { loadConn(); if (!silent) toast('Connection saved', 'success'); })
|
||
.catch(function () { if (!silent) toast('Could not save connection', 'error'); });
|
||
}
|
||
// Toggle click: reveal that server's config immediately (like the music
|
||
// toggle) and persist it as the active video server pick.
|
||
function pickServer(server) {
|
||
showServerConfig(server);
|
||
if (server === 'jellyfin') loadJellyfinUsers();
|
||
fetch(SERVER_URL, {
|
||
method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
|
||
body: JSON.stringify({ server: server })
|
||
}).then(function () {
|
||
load();
|
||
toast('Video server set to ' + (server === 'plex' ? 'Plex' : 'Jellyfin'), 'success');
|
||
}).catch(function () { /* ignore */ });
|
||
}
|
||
function testConn(server) {
|
||
var name = server === 'plex' ? 'Plex' : 'Jellyfin';
|
||
toast('Testing ' + name + ' connection…', 'info');
|
||
saveConn(true).then(function () {
|
||
return fetch(CONN_URL + '/test', {
|
||
method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
|
||
body: JSON.stringify({ server: server })
|
||
});
|
||
}).then(function (r) { return r.json(); }).then(function (res) {
|
||
if (res && res.success) {
|
||
toast(res.message || (name + ' connection successful'), 'success');
|
||
if (server === 'jellyfin') { loadJellyfinUsers(); load(); } // user + libraries
|
||
} else {
|
||
toast(name + ' connection failed: ' + ((res && res.error) || 'unknown'), 'error');
|
||
}
|
||
}).catch(function () { toast('Failed to test ' + name + ' connection', 'error'); });
|
||
}
|
||
|
||
// ── Jellyfin user picker (mirrors music: pick a user, then its libraries) ──
|
||
function loadJellyfinUsers() {
|
||
var wrap = document.querySelector('[data-video-jellyfin-user-wrap]');
|
||
var sel = document.querySelector('[data-video-jellyfin-user]');
|
||
if (!wrap || !sel) return;
|
||
fetch('/api/video/jellyfin/users', { headers: { 'Accept': 'application/json' } })
|
||
.then(function (r) { return r.ok ? r.json() : null; })
|
||
.then(function (d) {
|
||
if (!d || !d.success || !d.users || !d.users.length) { wrap.style.display = 'none'; return; }
|
||
sel.textContent = '';
|
||
var none = document.createElement('option');
|
||
none.value = ''; none.textContent = 'Select User';
|
||
sel.appendChild(none);
|
||
d.users.forEach(function (u) {
|
||
var o = document.createElement('option');
|
||
o.value = u.id;
|
||
o.textContent = u.name + (u.admin ? ' (admin)' : '');
|
||
if (u.id === d.selected) o.selected = true;
|
||
sel.appendChild(o);
|
||
});
|
||
wrap.style.display = 'block';
|
||
})
|
||
.catch(function () { wrap.style.display = 'none'; });
|
||
}
|
||
function selectJellyfinUser(id) {
|
||
fetch('/api/video/jellyfin/user', {
|
||
method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
|
||
body: JSON.stringify({ user: id })
|
||
}).then(function () {
|
||
load(); // refresh libraries for the user
|
||
if (id) toast('Jellyfin user updated', 'success');
|
||
}).catch(function () { /* ignore */ });
|
||
}
|
||
|
||
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(silent) {
|
||
var m = document.querySelector('[data-video-lib-select="movies"]');
|
||
var t = document.querySelector('[data-video-lib-select="tv"]');
|
||
status('Saving…');
|
||
return 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'); if (!silent) toast('Library selection saved', 'success'); })
|
||
.catch(function () { status('Save failed'); if (!silent) toast('Could not save libraries', 'error'); });
|
||
}
|
||
|
||
// ── Enrichment API keys (TMDB / TVDB) ───────────────────────────────────
|
||
function loadKeys() {
|
||
fetch(CONFIG_URL, { headers: { 'Accept': 'application/json' } })
|
||
.then(function (r) { return r.ok ? r.json() : null; })
|
||
.then(function (d) {
|
||
if (!d) return;
|
||
var t = document.getElementById('tmdb-api-key');
|
||
var v = document.getElementById('tvdb-api-key');
|
||
var o = document.getElementById('omdb-api-key');
|
||
if (t && d.tmdb_api_key != null) t.value = d.tmdb_api_key;
|
||
if (v && d.tvdb_api_key != null) v.value = d.tvdb_api_key;
|
||
if (o && d.omdb_api_key != null) o.value = d.omdb_api_key;
|
||
var fa = document.getElementById('fanart-api-key');
|
||
if (fa && d.fanart_api_key != null) fa.value = d.fanart_api_key;
|
||
var sub = document.getElementById('opensubtitles-api-key');
|
||
if (sub && d.opensubtitles_api_key != null) sub.value = d.opensubtitles_api_key;
|
||
var trakt = document.getElementById('trakt-api-key');
|
||
if (trakt && d.trakt_api_key != null) trakt.value = d.trakt_api_key;
|
||
var ryd = document.getElementById('video-ryd-enabled');
|
||
if (ryd && d.ryd_enabled != null) ryd.checked = !!d.ryd_enabled;
|
||
var sb = document.getElementById('video-sponsorblock-enabled');
|
||
if (sb && d.sponsorblock_enabled != null) sb.checked = !!d.sponsorblock_enabled;
|
||
var dea = document.getElementById('video-dearrow-enabled');
|
||
if (dea && d.dearrow_enabled != null) dea.checked = !!d.dearrow_enabled;
|
||
var tvm = document.getElementById('video-tvmaze-enabled');
|
||
if (tvm && d.tvmaze_enabled != null) tvm.checked = !!d.tvmaze_enabled;
|
||
var anl = document.getElementById('video-anilist-enabled');
|
||
if (anl && d.anilist_enabled != null) anl.checked = !!d.anilist_enabled;
|
||
var wkd = document.getElementById('video-wikidata-enabled');
|
||
if (wkd && d.wikidata_enabled != null) wkd.checked = !!d.wikidata_enabled;
|
||
var ap = document.getElementById('video-billboard-autoplay');
|
||
if (ap && d.billboard_autoplay != null) ap.checked = !!d.billboard_autoplay;
|
||
var wr = document.getElementById('video-watch-region');
|
||
if (wr && d.watch_region) wr.value = d.watch_region;
|
||
})
|
||
.catch(function () { /* ignore */ });
|
||
}
|
||
|
||
function savePrefs(silent) {
|
||
var ap = document.getElementById('video-billboard-autoplay');
|
||
var wr = document.getElementById('video-watch-region');
|
||
return fetch(CONFIG_URL, {
|
||
method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
|
||
body: JSON.stringify({
|
||
billboard_autoplay: ap ? ap.checked : true,
|
||
watch_region: wr ? wr.value : 'US',
|
||
})
|
||
}).then(function () { if (!silent) toast('Preferences saved', 'success'); })
|
||
.catch(function () { /* ignore */ });
|
||
}
|
||
|
||
// ── Downloads tab: folders + source mode + hybrid chain ──
|
||
var VIDEO_SOURCES = ['soulseek', 'torrent', 'usenet'];
|
||
var SRC_DL_LABEL = { soulseek: 'Soulseek', torrent: 'Torrent', usenet: 'Usenet' };
|
||
var SRC_DL_EMOJI = { soulseek: '🎵', torrent: '🧲', usenet: '📰' };
|
||
var _videoMode = 'soulseek';
|
||
var _videoHybrid = ['soulseek'];
|
||
|
||
function loadDownloads() {
|
||
fetch(DOWNLOADS_URL, { headers: { 'Accept': 'application/json' } })
|
||
.then(function (r) { return r.ok ? r.json() : null; })
|
||
.then(function (d) {
|
||
if (!d) return;
|
||
var setP = function (id, v) { var el = document.getElementById(id); if (el && v != null) el.value = v; };
|
||
setP('video-download-path', d.download_path);
|
||
setP('video-movies-path', d.movies_path);
|
||
setP('video-tv-path', d.tv_path);
|
||
setP('video-youtube-path', d.youtube_path);
|
||
_videoMode = d.download_mode || 'soulseek';
|
||
_videoHybrid = (d.hybrid_order && d.hybrid_order.length) ? d.hybrid_order : ['soulseek'];
|
||
var ms = document.getElementById('video-download-mode');
|
||
if (ms) ms.value = _videoMode;
|
||
renderVideoHybrid();
|
||
updateVideoSourceUI();
|
||
})
|
||
.catch(function () { /* ignore */ });
|
||
}
|
||
|
||
function saveDownloads(silent) {
|
||
var val = function (id) { var el = document.getElementById(id); return el ? el.value : ''; };
|
||
return fetch(DOWNLOADS_URL, {
|
||
method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
|
||
body: JSON.stringify({
|
||
download_path: val('video-download-path'),
|
||
movies_path: val('video-movies-path'),
|
||
tv_path: val('video-tv-path'),
|
||
youtube_path: val('video-youtube-path'),
|
||
download_mode: _videoMode,
|
||
hybrid_order: _videoHybrid,
|
||
})
|
||
}).then(function () { if (!silent) toast('Download folders saved', 'success'); })
|
||
.catch(function () { /* ignore */ });
|
||
}
|
||
|
||
// Hybrid chain — reuses music's .hybrid-source-item markup/CSS for visual
|
||
// parity. Enabled sources (ordered, numbered) first, disabled ones appended.
|
||
// No album-level/track-level badge — that's a music-only concept.
|
||
function renderVideoHybrid() {
|
||
var host = document.getElementById('video-hybrid-rows');
|
||
if (!host) return;
|
||
var enabled = _videoHybrid.filter(function (s) { return VIDEO_SOURCES.indexOf(s) >= 0; });
|
||
var disabled = VIDEO_SOURCES.filter(function (s) { return enabled.indexOf(s) < 0; });
|
||
var visual = enabled.concat(disabled);
|
||
host.innerHTML = visual.map(function (s) {
|
||
var on = enabled.indexOf(s) >= 0;
|
||
var i = enabled.indexOf(s);
|
||
return '<div class="hybrid-source-item' + (on ? '' : ' disabled') + '">' +
|
||
'<span class="hybrid-source-arrows">' +
|
||
'<button type="button" class="hybrid-arrow-btn" data-vh-move="' + s + '" data-dir="-1"' + ((!on || i === 0) ? ' disabled' : '') + ' title="Move up">▲</button>' +
|
||
'<button type="button" class="hybrid-arrow-btn" data-vh-move="' + s + '" data-dir="1"' + ((!on || i === enabled.length - 1) ? ' disabled' : '') + ' title="Move down">▼</button>' +
|
||
'</span>' +
|
||
'<span class="hybrid-source-icon emoji-icon">' + (SRC_DL_EMOJI[s] || '') + '</span>' +
|
||
'<span class="hybrid-source-name">' + SRC_DL_LABEL[s] + '</span>' +
|
||
'<span class="hybrid-source-priority">' + (on ? (i + 1) : '') + '</span>' +
|
||
'<label class="hybrid-source-toggle"><input type="checkbox" data-vh-toggle="' + s + '"' + (on ? ' checked' : '') + '><span class="toggle-track"></span></label>' +
|
||
'</div>';
|
||
}).join('');
|
||
}
|
||
|
||
function moveVH(s, dir) {
|
||
var i = _videoHybrid.indexOf(s), j = i + dir;
|
||
if (i < 0 || j < 0 || j >= _videoHybrid.length) return;
|
||
_videoHybrid[i] = _videoHybrid[j]; _videoHybrid[j] = s;
|
||
renderVideoHybrid(); saveDownloads(true);
|
||
}
|
||
|
||
function toggleVH(s, on) {
|
||
if (on) {
|
||
if (_videoHybrid.indexOf(s) < 0) _videoHybrid.push(s);
|
||
} else {
|
||
if (_videoHybrid.length <= 1) { renderVideoHybrid(); return; } // keep at least one
|
||
_videoHybrid = _videoHybrid.filter(function (x) { return x !== s; });
|
||
}
|
||
renderVideoHybrid(); saveDownloads(true);
|
||
}
|
||
|
||
function soulseekActive() {
|
||
return _videoMode === 'soulseek' ||
|
||
(_videoMode === 'hybrid' && _videoHybrid.indexOf('soulseek') >= 0);
|
||
}
|
||
|
||
function updateVideoSourceUI() {
|
||
var hc = document.getElementById('video-hybrid-container');
|
||
if (hc) hc.style.display = _videoMode === 'hybrid' ? 'block' : 'none';
|
||
// slskd connection only matters when soulseek is in play.
|
||
var sc = document.getElementById('video-slskd-container');
|
||
if (sc) sc.style.display = soulseekActive() ? 'block' : 'none';
|
||
}
|
||
|
||
// ── Shared slskd connection (writes the app-wide soulseek.* — affects Music too) ──
|
||
function _byId(id) { return document.getElementById(id); }
|
||
function loadSlskd() {
|
||
fetch(SLSKD_URL, { headers: { 'Accept': 'application/json' } })
|
||
.then(function (r) { return r.ok ? r.json() : null; })
|
||
.then(function (d) {
|
||
if (!d) return;
|
||
if (_byId('video-slskd-url')) _byId('video-slskd-url').value = d.slskd_url || '';
|
||
if (_byId('video-slskd-api-key')) _byId('video-slskd-api-key').value = d.api_key || '';
|
||
if (_byId('video-slskd-search-timeout')) _byId('video-slskd-search-timeout').value = d.search_timeout != null ? d.search_timeout : 60;
|
||
if (_byId('video-slskd-search-timeout-buffer')) _byId('video-slskd-search-timeout-buffer').value = d.search_timeout_buffer != null ? d.search_timeout_buffer : 15;
|
||
if (_byId('video-slskd-search-min-delay')) _byId('video-slskd-search-min-delay').value = d.search_min_delay_seconds != null ? d.search_min_delay_seconds : 0;
|
||
if (_byId('video-slskd-min-peer-speed')) _byId('video-slskd-min-peer-speed').value = d.min_peer_upload_speed != null ? d.min_peer_upload_speed : 0;
|
||
if (_byId('video-slskd-max-peer-queue')) _byId('video-slskd-max-peer-queue').value = d.max_peer_queue != null ? d.max_peer_queue : 0;
|
||
// config stores seconds; UI shows minutes.
|
||
if (_byId('video-slskd-download-timeout')) _byId('video-slskd-download-timeout').value = Math.round((d.download_timeout != null ? d.download_timeout : 600) / 60);
|
||
if (_byId('video-slskd-auto-clear')) _byId('video-slskd-auto-clear').checked = d.auto_clear_searches !== false;
|
||
})
|
||
.catch(function () { /* ignore */ });
|
||
}
|
||
|
||
function _num(id, dflt) { var el = _byId(id); var v = el ? parseInt(el.value, 10) : NaN; return Number.isFinite(v) ? v : dflt; }
|
||
|
||
function saveSlskd(silent) {
|
||
var url = _byId('video-slskd-url');
|
||
if (!url) return Promise.resolve(); // section not in DOM
|
||
return fetch(SLSKD_URL, {
|
||
method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
|
||
body: JSON.stringify({
|
||
slskd_url: url.value,
|
||
api_key: _byId('video-slskd-api-key') ? _byId('video-slskd-api-key').value : '',
|
||
search_timeout: _num('video-slskd-search-timeout', 60),
|
||
search_timeout_buffer: _num('video-slskd-search-timeout-buffer', 15),
|
||
search_min_delay_seconds: _num('video-slskd-search-min-delay', 0),
|
||
min_peer_upload_speed: _num('video-slskd-min-peer-speed', 0),
|
||
max_peer_queue: _num('video-slskd-max-peer-queue', 0),
|
||
download_timeout: _num('video-slskd-download-timeout', 10) * 60, // minutes → seconds
|
||
auto_clear_searches: _byId('video-slskd-auto-clear') ? _byId('video-slskd-auto-clear').checked : true,
|
||
})
|
||
}).then(function () { if (!silent) toast('slskd settings saved (shared with Music)', 'success'); })
|
||
.catch(function () { /* ignore */ });
|
||
}
|
||
|
||
function wireSlskd() {
|
||
var ids = ['video-slskd-url', 'video-slskd-api-key', 'video-slskd-search-timeout',
|
||
'video-slskd-search-timeout-buffer', 'video-slskd-search-min-delay',
|
||
'video-slskd-min-peer-speed', 'video-slskd-max-peer-queue',
|
||
'video-slskd-download-timeout', 'video-slskd-auto-clear'];
|
||
ids.forEach(function (id) {
|
||
var el = _byId(id);
|
||
if (el && !el._vsWired) { el._vsWired = true; el.addEventListener('change', function () { saveSlskd(true); }); }
|
||
});
|
||
}
|
||
|
||
function wireDownloads() {
|
||
var ms = document.getElementById('video-download-mode');
|
||
if (ms && !ms._vdWired) {
|
||
ms._vdWired = true;
|
||
ms.addEventListener('change', function () {
|
||
_videoMode = ms.value; updateVideoSourceUI(); saveDownloads(true);
|
||
});
|
||
}
|
||
var host = document.getElementById('video-hybrid-rows');
|
||
if (host && !host._vdWired) {
|
||
host._vdWired = true;
|
||
host.addEventListener('click', function (e) {
|
||
var mv = e.target.closest('[data-vh-move]');
|
||
if (mv) moveVH(mv.getAttribute('data-vh-move'), parseInt(mv.getAttribute('data-dir'), 10));
|
||
});
|
||
host.addEventListener('change', function (e) {
|
||
var tg = e.target.closest('[data-vh-toggle]');
|
||
if (tg) toggleVH(tg.getAttribute('data-vh-toggle'), tg.checked);
|
||
});
|
||
}
|
||
// Folder inputs save on change too.
|
||
['video-download-path', 'video-movies-path', 'video-tv-path', 'video-youtube-path'].forEach(function (id) {
|
||
var el = document.getElementById(id);
|
||
if (el && !el._vdWired) { el._vdWired = true; el.addEventListener('change', function () { saveDownloads(true); }); }
|
||
});
|
||
}
|
||
|
||
// ── Video quality profile (resolution tiers + source/codec/HDR/size) ──
|
||
function loadQuality() {
|
||
fetch(QUALITY_URL, { headers: { 'Accept': 'application/json' } })
|
||
.then(function (r) { return r.ok ? r.json() : null; })
|
||
.then(function (d) { if (d) { _videoQuality = d; renderQuality(); } })
|
||
.catch(function () { /* ignore */ });
|
||
}
|
||
|
||
function _vqSizeLabel(id, v) {
|
||
var lab = document.getElementById(id);
|
||
if (lab) lab.textContent = v ? (v + ' GB') : 'No limit';
|
||
}
|
||
|
||
function _vqSeg(id, attr, value) {
|
||
var seg = document.getElementById(id);
|
||
if (!seg) return;
|
||
Array.prototype.forEach.call(seg.querySelectorAll('[' + attr + ']'), function (b) {
|
||
b.classList.toggle('active', b.getAttribute(attr) === value);
|
||
});
|
||
}
|
||
|
||
function renderQuality() {
|
||
var p = _videoQuality;
|
||
if (!p) return;
|
||
var tiers = Array.isArray(p.tiers) ? p.tiers : [];
|
||
|
||
// Quality ladder — ranked, toggleable (same .hybrid-source-item styling as Download Source).
|
||
var host = document.getElementById('vq-tier-rows');
|
||
if (host) {
|
||
host.innerHTML = tiers.map(function (t, i) {
|
||
return '<div class="hybrid-source-item' + (t.enabled ? '' : ' disabled') + '">' +
|
||
'<span class="hybrid-source-arrows">' +
|
||
'<button type="button" class="hybrid-arrow-btn" data-vq-tier-move="' + t.key + '" data-dir="-1"' + (i === 0 ? ' disabled' : '') + ' title="Move up">▲</button>' +
|
||
'<button type="button" class="hybrid-arrow-btn" data-vq-tier-move="' + t.key + '" data-dir="1"' + (i === tiers.length - 1 ? ' disabled' : '') + ' title="Move down">▼</button>' +
|
||
'</span>' +
|
||
'<span class="hybrid-source-name">' + (TIER_LABEL[t.key] || t.key) + '</span>' +
|
||
'<span class="hybrid-source-priority">' + (i + 1) + '</span>' +
|
||
'<label class="hybrid-source-toggle"><input type="checkbox" data-vq-tier-toggle="' + t.key + '"' + (t.enabled ? ' checked' : '') + '><span class="toggle-track"></span></label>' +
|
||
'</div>';
|
||
}).join('');
|
||
}
|
||
|
||
// Cutoff — a loose resolution target (static <option>s); "" = always upgrade.
|
||
var cut = document.getElementById('vq-cutoff');
|
||
if (cut) cut.value = (p.cutoff_resolution == null ? '1080p' : p.cutoff_resolution);
|
||
|
||
// Hard rejects — toggle chips (on = blocked).
|
||
var rj = document.getElementById('vq-rejects');
|
||
if (rj) {
|
||
var set = Array.isArray(p.rejects) ? p.rejects : [];
|
||
rj.innerHTML = REJECT_ORDER.map(function (k) {
|
||
var on = set.indexOf(k) !== -1;
|
||
return '<button type="button" class="vq-chip' + (on ? ' on' : '') + '" data-vq-reject="' + k + '">' + (REJECT_LABEL[k] || k) + '</button>';
|
||
}).join('');
|
||
}
|
||
|
||
// Soft preferences.
|
||
_vqSeg('vq-codec', 'data-vq-codec', p.prefer_codec);
|
||
_vqSeg('vq-hdr', 'data-vq-hdr', p.prefer_hdr);
|
||
_vqSeg('vq-audio', 'data-vq-audio', p.prefer_audio);
|
||
var rep = document.getElementById('vq-prefer-repack'); if (rep) rep.checked = !!p.prefer_repack;
|
||
|
||
// Size guard — split by runtime so a movie and an episode aren't judged the same.
|
||
var mv = document.getElementById('vq-movie-size'); if (mv) mv.value = p.max_movie_gb || 0;
|
||
var ep = document.getElementById('vq-episode-size'); if (ep) ep.value = p.max_episode_gb || 0;
|
||
_vqSizeLabel('vq-movie-label', p.max_movie_gb || 0);
|
||
_vqSizeLabel('vq-episode-label', p.max_episode_gb || 0);
|
||
}
|
||
|
||
function moveTier(k, dir) {
|
||
var p = _videoQuality; if (!p || !Array.isArray(p.tiers)) return;
|
||
var arr = p.tiers, i = -1;
|
||
for (var n = 0; n < arr.length; n++) { if (arr[n].key === k) { i = n; break; } }
|
||
var j = i + dir;
|
||
if (i < 0 || j < 0 || j >= arr.length) return;
|
||
var tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; // swap
|
||
renderQuality(); saveQuality(true);
|
||
}
|
||
|
||
function toggleReject(k) {
|
||
var p = _videoQuality; if (!p) return;
|
||
if (!Array.isArray(p.rejects)) p.rejects = [];
|
||
var i = p.rejects.indexOf(k);
|
||
if (i === -1) p.rejects.push(k); else p.rejects.splice(i, 1);
|
||
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-tier-rows');
|
||
if (!sec) return;
|
||
var card = sec.closest('.settings-group');
|
||
if (!card || card._vqWired) return;
|
||
card._vqWired = true;
|
||
card.addEventListener('click', function (e) {
|
||
var tm = e.target.closest('[data-vq-tier-move]');
|
||
if (tm) { moveTier(tm.getAttribute('data-vq-tier-move'), parseInt(tm.getAttribute('data-dir'), 10)); return; }
|
||
var rj = e.target.closest('[data-vq-reject]');
|
||
if (rj) { toggleReject(rj.getAttribute('data-vq-reject')); return; }
|
||
if (!_videoQuality) return;
|
||
var cd = e.target.closest('[data-vq-codec]');
|
||
if (cd) { _videoQuality.prefer_codec = cd.getAttribute('data-vq-codec'); renderQuality(); saveQuality(true); return; }
|
||
var hd = e.target.closest('[data-vq-hdr]');
|
||
if (hd) { _videoQuality.prefer_hdr = hd.getAttribute('data-vq-hdr'); renderQuality(); saveQuality(true); return; }
|
||
var au = e.target.closest('[data-vq-audio]');
|
||
if (au) { _videoQuality.prefer_audio = au.getAttribute('data-vq-audio'); renderQuality(); saveQuality(true); return; }
|
||
});
|
||
card.addEventListener('change', function (e) {
|
||
if (!_videoQuality) return;
|
||
var tt = e.target.closest('[data-vq-tier-toggle]');
|
||
if (tt) {
|
||
var key = tt.getAttribute('data-vq-tier-toggle');
|
||
var arr = _videoQuality.tiers || [];
|
||
for (var n = 0; n < arr.length; n++) { if (arr[n].key === key) { arr[n].enabled = tt.checked; break; } }
|
||
renderQuality(); saveQuality(true); return;
|
||
}
|
||
if (e.target.id === 'vq-cutoff') { _videoQuality.cutoff_resolution = e.target.value; saveQuality(true); return; }
|
||
if (e.target.id === 'vq-prefer-repack') { _videoQuality.prefer_repack = e.target.checked; saveQuality(true); return; }
|
||
if (e.target.id === 'vq-movie-size') { _videoQuality.max_movie_gb = parseInt(e.target.value, 10) || 0; saveQuality(true); return; }
|
||
if (e.target.id === 'vq-episode-size') { _videoQuality.max_episode_gb = parseInt(e.target.value, 10) || 0; saveQuality(true); return; }
|
||
});
|
||
card.addEventListener('input', function (e) {
|
||
if (e.target.id === 'vq-movie-size') { _vqSizeLabel('vq-movie-label', parseInt(e.target.value, 10) || 0); return; }
|
||
if (e.target.id === 'vq-episode-size') { _vqSizeLabel('vq-episode-label', parseInt(e.target.value, 10) || 0); return; }
|
||
});
|
||
}
|
||
|
||
// ── YouTube quality (separate, smaller yt-dlp profile) ────────────────────
|
||
function loadYtQuality() {
|
||
fetch(YT_QUALITY_URL, { headers: { 'Accept': 'application/json' } })
|
||
.then(function (r) { return r.ok ? r.json() : null; })
|
||
.then(function (d) { if (d) { _videoYtQuality = d; renderYtQuality(); } })
|
||
.catch(function () { /* ignore */ });
|
||
}
|
||
|
||
function renderYtQuality() {
|
||
var p = _videoYtQuality;
|
||
if (!p) return;
|
||
var res = document.getElementById('yq-resolution'); if (res) res.value = p.max_resolution || '1080p';
|
||
_vqSeg('yq-codec', 'data-yq-codec', p.video_codec);
|
||
_vqSeg('yq-container', 'data-yq-container', p.container);
|
||
var fps = document.getElementById('yq-60fps'); if (fps) fps.checked = !!p.prefer_60fps;
|
||
var hdr = document.getElementById('yq-hdr'); if (hdr) hdr.checked = !!p.allow_hdr;
|
||
}
|
||
|
||
function saveYtQuality(silent) {
|
||
if (!_videoYtQuality) return Promise.resolve();
|
||
return fetch(YT_QUALITY_URL, {
|
||
method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
|
||
body: JSON.stringify(_videoYtQuality)
|
||
}).then(function (r) { return r.ok ? r.json() : null; })
|
||
.then(function (d) { if (d) _videoYtQuality = d; if (!silent) toast('YouTube quality saved', 'success'); })
|
||
.catch(function () { /* ignore */ });
|
||
}
|
||
|
||
function wireYtQuality() {
|
||
var seg = document.getElementById('yq-codec');
|
||
if (!seg) return;
|
||
var card = seg.closest('.settings-group');
|
||
if (!card || card._yqWired) return;
|
||
card._yqWired = true;
|
||
card.addEventListener('click', function (e) {
|
||
if (!_videoYtQuality) return;
|
||
var cd = e.target.closest('[data-yq-codec]');
|
||
if (cd) { _videoYtQuality.video_codec = cd.getAttribute('data-yq-codec'); renderYtQuality(); saveYtQuality(true); return; }
|
||
var ct = e.target.closest('[data-yq-container]');
|
||
if (ct) { _videoYtQuality.container = ct.getAttribute('data-yq-container'); renderYtQuality(); saveYtQuality(true); return; }
|
||
});
|
||
card.addEventListener('change', function (e) {
|
||
if (!_videoYtQuality) return;
|
||
if (e.target.id === 'yq-resolution') { _videoYtQuality.max_resolution = e.target.value; saveYtQuality(true); return; }
|
||
if (e.target.id === 'yq-60fps') { _videoYtQuality.prefer_60fps = e.target.checked; saveYtQuality(true); return; }
|
||
if (e.target.id === 'yq-hdr') { _videoYtQuality.allow_hdr = e.target.checked; saveYtQuality(true); return; }
|
||
});
|
||
}
|
||
|
||
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/<svc>/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'); });
|
||
}
|
||
|
||
// ── Library Organization (naming templates + post-process toggles) ───────
|
||
var ORG_URL = '/api/video/organization';
|
||
var _videoOrg = null;
|
||
// Sample values for the live preview (mirrors what the importer feeds the engine).
|
||
var _ORG_MOVIE_EG = { title: 'The Matrix', titlefirst: 'T', year: '1999', quality: 'Bluray-1080p',
|
||
resolution: '1080p', source: 'Bluray', codec: 'HEVC', edition: '', tmdbid: '603', imdbid: 'tt0133093' };
|
||
var _ORG_EP_EG = { series: 'Breaking Bad', season: '01', seasonraw: '1', episode: '01', episodetitle: 'Pilot',
|
||
year: '2008', quality: 'WEBDL-1080p', resolution: '1080p', source: 'WEBDL', codec: 'H264', tvdbid: '81189' };
|
||
var _ORG_YT_EG = { channel: 'Veritasium', title: 'How Electricity Actually Works', year: '2024',
|
||
date: '2024-03-15', month: '03', day: '15', videoid: 'oI_X2cMHNe0' };
|
||
|
||
function _orgSanitize(v) {
|
||
return String(v == null ? '' : v).replace(/[\\/:*?"<>|\x00-\x1f]/g, '')
|
||
.replace(/\s+/g, ' ').trim().replace(/[ .]+$/, '');
|
||
}
|
||
// Client-side mirror of core/video/organization.render_template + _tidy_component
|
||
// (kept in lockstep) so the preview matches what actually lands on disk.
|
||
function _orgRender(tmpl, vals) {
|
||
var keys = Object.keys(vals).sort(function (a, b) { return b.length - a.length; });
|
||
var out = String(tmpl || '');
|
||
keys.forEach(function (k) { out = out.split('${' + k + '}').join(_orgSanitize(vals[k])); });
|
||
keys.forEach(function (k) { out = out.split('$' + k).join(_orgSanitize(vals[k])); });
|
||
return out.split('/').map(function (p) {
|
||
p = p.replace(/\s+-\s+(?=(\s|$))/g, ' ').replace(/\(\s*\)/g, '').replace(/\[\s*\]/g, '');
|
||
p = p.replace(/\s+/g, ' ').trim().replace(/^-+|-+$/g, '').trim().replace(/[ .]+$/, '');
|
||
return p;
|
||
}).filter(function (p) { return p !== ''; }).join('/');
|
||
}
|
||
function renderOrgPreview() {
|
||
var mt = document.getElementById('vo-movie-template');
|
||
var et = document.getElementById('vo-episode-template');
|
||
var yt = document.getElementById('vo-youtube-template');
|
||
var mp = document.getElementById('vo-movie-preview');
|
||
var ep = document.getElementById('vo-episode-preview');
|
||
var yp = document.getElementById('vo-youtube-preview');
|
||
if (mp && mt) mp.textContent = _orgRender(mt.value || mt.placeholder, _ORG_MOVIE_EG) + '.mkv';
|
||
if (ep && et) ep.textContent = _orgRender(et.value || et.placeholder, _ORG_EP_EG) + '.mkv';
|
||
if (yp && yt) yp.textContent = _orgRender(yt.value || yt.placeholder, _ORG_YT_EG) + '.mp4';
|
||
}
|
||
function fillOrg() {
|
||
if (!_videoOrg) return;
|
||
var set = function (id, v) { var el = document.getElementById(id); if (el) el.value = v; };
|
||
var chk = function (id, v) { var el = document.getElementById(id); if (el) el.checked = !!v; };
|
||
set('vo-movie-template', _videoOrg.movie_template || '');
|
||
set('vo-episode-template', _videoOrg.episode_template || '');
|
||
set('vo-youtube-template', _videoOrg.youtube_template || '');
|
||
set('vo-transfer-mode', _videoOrg.transfer_mode || 'copy');
|
||
chk('vo-verify', _videoOrg.verify_with_ffprobe);
|
||
chk('vo-replace', _videoOrg.replace_existing);
|
||
chk('vo-subs', _videoOrg.carry_subtitles);
|
||
chk('vo-artwork', _videoOrg.save_artwork);
|
||
chk('vo-nfo', _videoOrg.write_nfo);
|
||
chk('vo-subs-dl', _videoOrg.download_subtitles);
|
||
set('vo-sub-langs', _videoOrg.subtitle_langs || 'en');
|
||
renderOrgPreview();
|
||
}
|
||
function loadOrganization() {
|
||
fetch(ORG_URL, { headers: { 'Accept': 'application/json' } })
|
||
.then(function (r) { return r.ok ? r.json() : null; })
|
||
.then(function (d) { if (d) { _videoOrg = d; fillOrg(); } })
|
||
.catch(function () { /* ignore */ });
|
||
}
|
||
function collectOrg() {
|
||
var val = function (id) { var el = document.getElementById(id); return el ? el.value : ''; };
|
||
var on = function (id) { var el = document.getElementById(id); return !!(el && el.checked); };
|
||
return {
|
||
movie_template: val('vo-movie-template'),
|
||
episode_template: val('vo-episode-template'),
|
||
youtube_template: val('vo-youtube-template'),
|
||
transfer_mode: val('vo-transfer-mode'),
|
||
verify_with_ffprobe: on('vo-verify'),
|
||
replace_existing: on('vo-replace'),
|
||
carry_subtitles: on('vo-subs'),
|
||
save_artwork: on('vo-artwork'),
|
||
write_nfo: on('vo-nfo'),
|
||
download_subtitles: on('vo-subs-dl'),
|
||
subtitle_langs: val('vo-sub-langs')
|
||
};
|
||
}
|
||
function saveOrganization(silent) {
|
||
return fetch(ORG_URL, {
|
||
method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
|
||
body: JSON.stringify(collectOrg())
|
||
}).then(function (r) { return r.ok ? r.json() : null; })
|
||
.then(function (d) { if (d) { _videoOrg = d; } if (!silent) toast('Library organization saved', 'success'); })
|
||
.catch(function () { /* ignore */ });
|
||
}
|
||
function wireOrganization() {
|
||
var anchor = document.getElementById('vo-movie-template');
|
||
if (!anchor) return;
|
||
var card = anchor.closest('.settings-group');
|
||
if (!card || card._voWired) return;
|
||
card._voWired = true;
|
||
['vo-movie-template', 'vo-episode-template', 'vo-youtube-template'].forEach(function (id) {
|
||
var el = document.getElementById(id);
|
||
if (!el) return;
|
||
el.addEventListener('input', renderOrgPreview); // live preview while typing
|
||
el.addEventListener('change', function () { saveOrganization(false); });
|
||
});
|
||
['vo-transfer-mode', 'vo-verify', 'vo-replace', 'vo-subs', 'vo-artwork', 'vo-nfo',
|
||
'vo-subs-dl', 'vo-sub-langs'].forEach(function (id) {
|
||
var el = document.getElementById(id);
|
||
if (el) el.addEventListener('change', function () { saveOrganization(false); });
|
||
});
|
||
var reset = document.getElementById('vo-reset');
|
||
if (reset) reset.addEventListener('click', function () {
|
||
// POST blank templates + standard toggles; the backend normalises to the
|
||
// Radarr/Sonarr defaults and echoes them back.
|
||
fetch(ORG_URL, {
|
||
method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
|
||
body: JSON.stringify({ movie_template: '', episode_template: '', youtube_template: '',
|
||
transfer_mode: 'copy', verify_with_ffprobe: true, replace_existing: true,
|
||
carry_subtitles: true, save_artwork: false, write_nfo: false,
|
||
download_subtitles: false, subtitle_langs: 'en' })
|
||
}).then(function (r) { return r.ok ? r.json() : null; })
|
||
.then(function (d) { if (d) { _videoOrg = d; fillOrg(); toast('Reset to the standard layout', 'success'); } });
|
||
});
|
||
}
|
||
|
||
function onPageShown(e) {
|
||
if (e && e.detail !== PAGE_ID) return;
|
||
loadServer();
|
||
loadConn();
|
||
load();
|
||
loadKeys();
|
||
loadDownloads();
|
||
wireDownloads();
|
||
loadQuality();
|
||
wireQuality();
|
||
loadYtQuality();
|
||
wireYtQuality();
|
||
loadSlskd();
|
||
wireSlskd();
|
||
loadOrganization();
|
||
wireOrganization();
|
||
}
|
||
|
||
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), saveYtQuality(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();
|
||
}
|
||
})();
|