Soulseek returned ' + totalFiles + ' file' + (totalFiles === 1 ? '' : 's') +
', but none are video releases — likely audio/other for this title. Try a different title or source.
';
} else {
resultsEl.innerHTML = '
No matching releases found.
';
}
return;
}
resultsEl._rows = rows; resultsEl._search = params; // for the Grab button
resultsEl.classList.toggle('vdl-res-noanim', !!live); // live re-renders → no per-card blink
var okN = rows.filter(function (r) { return r.accepted; }).length;
var badge = !live ? 'demo data'
: done ? '● live'
: 'searching…';
resultsEl.innerHTML =
'
' +
rows.map(resultCardHTML).join('');
}
// Start a search; for Soulseek, stream results in (poll like the music side —
// results trickle in over ~30s, so a single short wait misses them).
function searchInto(container, resultsEl, params, triggerRows) {
if (!resultsEl) return;
triggerRows = (triggerRows || []).filter(Boolean);
if (resultsEl._poll) { clearTimeout(resultsEl._poll); resultsEl._poll = null; }
_setScanning(triggerRows, true);
resultsEl.hidden = false;
resultsEl.classList.remove('vdl-res-noanim');
resultsEl.innerHTML = '
';
}
// ── TV show download view ─────────────────────────────────────────────────
// A season→episode picker (everything you're missing pre-ticked), each season
// and episode searchable inline across your sources, plus a bulk "Search N
// selected". Searches are stubs (faux-scan) — same motion the engine will drive.
function isoToday() {
var n = new Date();
return n.getFullYear() + '-' + ('0' + (n.getMonth() + 1)).slice(-2) + '-' + ('0' + n.getDate()).slice(-2);
}
function epState(e, today) {
if (e && e.owned) return 'owned';
if (e && e.air_date && e.air_date > today) return 'upcoming';
return 'missing';
}
function renderShow(container, opts) {
var d = opts.detail || {};
var maxSeason = 0;
(d.seasons || []).forEach(function (s) { if ((s.season_number || 0) > maxSeason) maxSeason = s.season_number; });
var st = container._dl = {
sel: new Set(), today: isoToday(),
tvId: opts.tvId || d.tmdb_id || null, source: opts.source || 'library',
sources: ['soulseek'], epMeta: {},
title: d.title || opts.title || '', maxSeason: maxSeason,
mediaId: opts.id, mediaSource: opts.source, poster: opts.poster || null, year: d.year || null
};
container.innerHTML =
'
Quality target
' +
'
Loading…
' +
'
' +
'' +
'Loading episodes…' +
'' +
'
' +
'' +
'';
if (!container._dlShowWired) {
container._dlShowWired = true;
container.addEventListener('click', onShowClick);
container.addEventListener('change', onShowChange);
}
getJSON('/api/video/downloads/quality').then(function (p) { if (container.isConnected && p) renderTarget(container, p, false); });
getJSON('/api/video/downloads/config').then(function (c) { if (container.isConnected) st.sources = sourcesFromConfig(c); });
buildSeasons(container, d, st);
}
function buildSeasons(container, d, st) {
var host = container.querySelector('[data-vdl-seasons]'); if (!host) return;
var seasons = (d.seasons || []).slice();
if (!seasons.length) { host.innerHTML = '
No season information available.
'; updateShowBar(container); return; }
host.innerHTML = seasons.map(function (s) { return seasonShellHTML(s); }).join('');
seasons.forEach(function (s) {
var card = host.querySelector('.vdl-season[data-vdl-season="' + s.season_number + '"]');
var eps = s.episodes || [];
if (eps.length) { fillSeason(container, card, s.season_number, eps, st); }
else if ((s.episode_total || 0) > 0 && st.tvId) { fetchSeason(container, card, s.season_number, st); }
else { var b = card.querySelector('.vdl-season-eps'); if (b) b.innerHTML = '
No episodes.
'; }
});
updateShowBar(container);
}
function seasonShellHTML(s) {
var sn = s.season_number;
var total = (s.episodes && s.episodes.length) || s.episode_total || 0;
return '
';
}
function onShowClick(e) {
var container = e.currentTarget; var st = container._dl; if (!st) return;
var grab = e.target.closest('[data-vdl-grab]');
if (grab) { doGrab(grab); return; }
// Episode-scope search (a source row inside an expanded episode → its own panel).
var srch = e.target.closest('[data-vdl-search]');
if (srch) {
var epEl = srch.closest('.vdl-ep'); if (!epEl) return;
var parts = (epEl.getAttribute('data-vdl-ep') || '').split('_');
var s = srch.getAttribute('data-vdl-search');
var block = srch.closest('[data-vdl-src-block]');
searchInto(container, block.querySelector('[data-vdl-results-for="' + s + '"]'),
{ scope: 'episode', title: st.title, season: +parts[0], episode: +parts[1], source: s },
[srch.closest('.vdl-src')]);
return;
}
// Season-scope search → season PACK.
var ss = e.target.closest('[data-vdl-season-search]');
if (ss) {
var sn = ss.getAttribute('data-vdl-season-search');
var sc = container.querySelector('.vdl-season[data-vdl-season="' + sn + '"]');
if (sc) sc.classList.add('vdl-season--open');
searchInto(container, sc && sc.querySelector('[data-vdl-season-results]'),
{ scope: 'season', title: st.title, season: +sn }, [ss]);
return;
}
// Whole-show search → complete-series pack.
if (e.target.closest('[data-vdl-search-show]')) {
searchInto(container, container.querySelector('[data-vdl-show-results]'),
{ scope: 'series', title: st.title, season_end: st.maxSeason || 5 },
[e.target.closest('[data-vdl-search-show]')]);
return;
}
var sh = e.target.closest('[data-vdl-season-toggle]');
if (sh && !e.target.closest('.vdl-season-cb') && !e.target.closest('[data-vdl-season-search]')) {
sh.closest('.vdl-season').classList.toggle('vdl-season--open'); return;
}
var eh = e.target.closest('[data-vdl-ep-toggle]');
if (eh && !e.target.closest('.vdl-ep-cell')) { toggleEp(container, eh.closest('.vdl-ep')); }
}
function onShowChange(e) {
var container = e.currentTarget; var st = container._dl; if (!st) return;
var ec = e.target.closest('[data-vdl-ep-cb]');
if (ec) {
var k = ec.getAttribute('data-vdl-ep-cb');
if (ec.checked) st.sel.add(k); else st.sel.delete(k);
syncSeason(container, k.split('_')[0]); updateShowBar(container); return;
}
var sa = e.target.closest('[data-vdl-season-all]');
if (sa) { setSeasonSel(container, sa.getAttribute('data-vdl-season-all'), sa.checked); updateShowBar(container); return; }
if (e.target.closest('[data-vdl-all]')) { setAllSel(container, e.target.checked); updateShowBar(container); }
}
function toggleEp(container, epEl) {
if (!epEl) return;
var open = !epEl.classList.contains('vdl-ep--open');
epEl.classList.toggle('vdl-ep--open', open);
if (open && !epEl.getAttribute('data-srcbuilt')) buildEpSearch(container, epEl);
}
function buildEpSearch(container, epEl) {
epEl.setAttribute('data-srcbuilt', '1');
var panel = epEl.querySelector('[data-vdl-ep-search]'); if (!panel) return;
var srcs = (container._dl.sources || []).filter(function (s) { return SRC_META[s]; });
if (!srcs.length) { panel.innerHTML = '
';
}
function setSeasonSel(container, sn, on) {
var st = container._dl;
var cbs = container.querySelectorAll('.vdl-season[data-vdl-season="' + sn + '"] .vdl-ep-cb');
for (var i = 0; i < cbs.length; i++) {
cbs[i].checked = on;
var k = cbs[i].getAttribute('data-vdl-ep-cb');
if (on) st.sel.add(k); else st.sel.delete(k);
}
syncSeason(container, sn);
}
function setAllSel(container, on) {
var cards = container.querySelectorAll('.vdl-season');
for (var i = 0; i < cards.length; i++) setSeasonSel(container, cards[i].getAttribute('data-vdl-season'), on);
}
function syncSeason(container, sn) {
var card = container.querySelector('.vdl-season[data-vdl-season="' + sn + '"]'); if (!card) return;
var all = card.querySelector('[data-vdl-season-all]'); if (!all) return;
var cbs = card.querySelectorAll('.vdl-ep-cb'), checked = 0;
for (var i = 0; i < cbs.length; i++) if (cbs[i].checked) checked++;
all.checked = cbs.length > 0 && checked === cbs.length;
all.indeterminate = checked > 0 && checked < cbs.length;
all.disabled = cbs.length === 0;
}
function updateShowBar(container) {
var st = container._dl; if (!st) return;
var sum = container.querySelector('[data-vdl-summary]');
if (sum) {
var seasons = container.querySelectorAll('.vdl-season').length;
var owned = 0, missing = 0, total = 0;
for (var k in st.epMeta) { total++; if (st.epMeta[k].state === 'owned') owned++; else if (st.epMeta[k].state === 'missing') missing++; }
sum.textContent = seasons + ' season' + (seasons === 1 ? '' : 's') + ' · ' + total + ' episodes · ' +
owned + ' in library · ' + missing + ' missing';
}
var master = container.querySelector('[data-vdl-all]');
if (master) {
var all = container.querySelectorAll('.vdl-ep-cb'), c = 0;
for (var i = 0; i < all.length; i++) if (all[i].checked) c++;
master.checked = all.length > 0 && c === all.length;
master.indeterminate = c > 0 && c < all.length;
}
}
window.VideoDownload = { render: render };
})();