/*
* SoulSync โ Video Library page (isolated).
*
* Reuses the music library's look + paging model: server-side search / AโZ /
* sort / owned-wanted filter / pagination (75 per page) via /api/video/library,
* rendering one page of music-style .library-artist-card cards (with a poster
* proxy + resolution badge). Cards are NOT clickable yet. IIFE, no globals.
*/
(function () {
'use strict';
var PAGE_ID = 'video-library';
var LIBRARY_URL = '/api/video/library';
var LETTERS = 'abcdefghijklmnopqrstuvwxyz'.split('');
var state = { tab: 'movies', search: '', letter: 'all', sort: 'title',
status: 'all', page: 1, limit: 75, loaded: false };
var searchTimer = null;
function $(sel) { return document.querySelector(sel); }
function esc(s) {
return String(s == null ? '' : s)
.replace(/&/g, '&').replace(//g, '>')
.replace(/"/g, '"').replace(/'/g, ''');
}
function resLabel(res) {
if (!res) return '';
res = String(res).toLowerCase();
if (res.indexOf('2160') > -1 || res === '4k') return '4K';
if (res.indexOf('1080') > -1) return '1080p';
if (res.indexOf('720') > -1) return '720p';
if (res.indexOf('480') > -1 || res.indexOf('576') > -1) return 'SD';
return res.toUpperCase();
}
function buildAlphabet() {
var host = $('[data-video-lib-alphabet]');
if (!host || host.childNodes.length) return;
var defs = [['all', 'All']].concat(
LETTERS.map(function (l) { return [l, l.toUpperCase()]; }), [['#', '#']]);
for (var i = 0; i < defs.length; i++) {
var b = document.createElement('button');
b.className = 'alphabet-btn' + (defs[i][0] === 'all' ? ' active' : '');
b.setAttribute('data-letter', defs[i][0]);
b.textContent = defs[i][1];
(function (letter, btn) {
btn.addEventListener('click', function () {
state.letter = letter;
var all = host.querySelectorAll('.alphabet-btn');
for (var j = 0; j < all.length; j++) all[j].classList.toggle('active', all[j] === btn);
reload();
});
})(defs[i][0], b);
host.appendChild(b);
}
}
function cardHTML(it, kind) {
var fallback = kind === 'movie' ? '๐ฌ' : '๐บ';
var img = it.has_poster
? '

'
: '';
// Overlay control group (eye/get), injected inside the positioned poster
// box: airing show -> eye + get; ended show / movie -> get; (people elsewhere).
var ctrl = window.VideoGet ? VideoGet.cardButton({
kind: kind, tmdbId: it.tmdb_id, libraryId: it.id, title: it.title,
poster: it.has_poster ? ('/api/video/poster/' + kind + '/' + it.id) : '',
status: it.status, source: 'library'
}) : '';
if (ctrl) img = img.replace(/<\/div>$/, ctrl + '');
var badge = '';
if (kind === 'movie') {
var rl = resLabel(it.resolution);
if (rl) badge = '' + rl + '
';
}
var meta = [];
if (it.year) meta.push(String(it.year));
if (kind === 'movie') meta.push(it.has_file ? 'Owned' : 'Wanted');
else meta.push((it.owned_count || 0) + '/' + (it.episode_count || 0) + ' eps');
// A REAL link (like the music artist cards) so reload / new-tab / Back all
// work; the click handler intercepts plain left-clicks into SPA nav.
var href = '/video-detail/library/' + kind + '/' + it.id;
var hook = ' href="' + href + '" data-video-card-open="' + kind + '" data-video-card-id="' + it.id + '"';
return '' + img + badge +
'' +
'
' + esc(it.title) + '
' +
'
' +
esc(meta.join(' ยท ')) + '
';
}
function showLoading(on) {
var l = $('[data-video-lib-loading]');
if (l) l.classList.toggle('hidden', !on);
}
function renderItems(items, kind) {
var grid = $('[data-video-lib-grid]');
var empty = $('[data-video-lib-empty]');
if (grid) grid.innerHTML = items.map(function (it) { return cardHTML(it, kind); }).join('');
if (empty) empty.classList.toggle('hidden', items.length > 0);
// #watchlist: paint the follow eye on shows already on the watchlist.
if (grid && kind === 'show' && window.VideoWatchlist) VideoWatchlist.hydrate(grid);
}
function updatePagination(p) {
var box = $('[data-video-lib-pagination]');
var prev = $('[data-video-lib-prev]');
var next = $('[data-video-lib-next]');
var info = $('[data-video-lib-pageinfo]');
if (!box) return;
if (!p) { box.classList.add('hidden'); return; }
if (prev) prev.disabled = !p.has_prev;
if (next) next.disabled = !p.has_next;
if (info) info.textContent = 'Page ' + p.page + ' of ' + p.total_pages;
box.classList.toggle('hidden', p.total_pages <= 1);
}
function setCount(n) {
var c = $('[data-video-lib-count]');
var lbl = $('[data-video-lib-count-label]');
if (c) c.textContent = n;
if (lbl) lbl.textContent = state.tab === 'movies' ? 'Movies' : 'Shows';
}
function load() {
state.loaded = true;
showLoading(true);
var apiKind = state.tab === 'movies' ? 'movies' : 'shows'; // query param (plural)
var cardKind = state.tab === 'movies' ? 'movie' : 'show'; // card + poster URL (singular)
var params = new URLSearchParams({
kind: apiKind, search: state.search, letter: state.letter, sort: state.sort,
status: state.status, page: state.page, limit: state.limit });
fetch(LIBRARY_URL + '?' + params.toString(), { headers: { 'Accept': 'application/json' } })
.then(function (r) { return r.ok ? r.json() : null; })
.then(function (d) {
showLoading(false);
if (!d || d.error) { renderItems([], cardKind); updatePagination(null); setCount(0); return; }
renderItems(d.items || [], cardKind);
updatePagination(d.pagination);
setCount(d.pagination ? d.pagination.total_count : 0);
})
.catch(function () { showLoading(false); renderItems([], cardKind); updatePagination(null); setCount(0); });
}
function reload() { state.page = 1; load(); }
function wire() {
// Card click โ drill into the detail page (event-delegated on the grid).
var grid = $('[data-video-lib-grid]');
if (grid) {
grid.addEventListener('click', function (e) {
// Let modified clicks (new tab / window) use the real href.
if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
var card = e.target.closest('[data-video-card-open]');
if (!card || !grid.contains(card)) return;
e.preventDefault();
document.dispatchEvent(new CustomEvent('soulsync:video-open-detail', {
detail: { kind: card.getAttribute('data-video-card-open'),
id: parseInt(card.getAttribute('data-video-card-id'), 10), source: 'library' },
}));
});
}
var tabs = document.querySelectorAll('[data-video-lib-tab]');
for (var i = 0; i < tabs.length; i++) {
(function (tab) {
tab.addEventListener('click', function () {
state.tab = tab.getAttribute('data-video-lib-tab');
var all = document.querySelectorAll('[data-video-lib-tab]');
for (var j = 0; j < all.length; j++) all[j].classList.toggle('active', all[j] === tab);
var s = $('[data-video-lib-search]');
if (s) s.placeholder = 'Search ' + (state.tab === 'movies' ? 'movies' : 'shows') + '...';
reload();
});
})(tabs[i]);
}
var search = $('[data-video-lib-search]');
if (search) {
search.addEventListener('input', function () {
state.search = search.value || '';
if (searchTimer) clearTimeout(searchTimer);
searchTimer = setTimeout(reload, 300);
});
}
var sort = $('[data-video-lib-sort]');
if (sort) sort.addEventListener('change', function () { state.sort = sort.value; reload(); });
var status = $('[data-video-lib-status]');
if (status) status.addEventListener('change', function () { state.status = status.value; reload(); });
var prev = $('[data-video-lib-prev]');
if (prev) prev.addEventListener('click', function () {
if (state.page > 1) { state.page--; load(); }
});
var next = $('[data-video-lib-next]');
if (next) next.addEventListener('click', function () { state.page++; load(); });
}
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();
}
function init() {
buildAlphabet();
wire();
document.addEventListener('soulsync:video-page-shown', onPageShown);
document.addEventListener('soulsync:video-scan-done', onScanDone);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();