';
}
function _movieSearch(container, block, auto, onSettled) {
var o = container._opts || {};
var s = block.getAttribute('data-vdl-src-block');
var resultsEl = block.querySelector('[data-vdl-results-for="' + s + '"]');
var statusRow = block.querySelector('.vdl-src');
// In per-source auto mode, grab this source's best when it settles; an
// optional onSettled lets a coordinator (header Auto) wait for every source.
var onDone = (auto || onSettled) ? function () {
if (auto) _autoPick(resultsEl, statusRow);
if (onSettled) onSettled(resultsEl);
} : null;
searchInto(container, resultsEl,
{ scope: 'movie', title: o.title || '', year: o.year || null, source: s },
[statusRow], onDone);
}
// Header "Auto": search EVERY source, then download the single best release
// across all of them (NOT one per source). Each source's results still show so
// the pick is transparent; the winner gets the auto ring + live tracker.
function _autoBest(container) {
var blocks = Array.prototype.slice.call(container.querySelectorAll('[data-vdl-src-block]'));
if (!blocks.length) return;
var hb = container.querySelector('[data-vdl-auto-best]');
if (hb) hb.disabled = true;
var pending = blocks.length, done = false;
blocks.forEach(function (block) {
_movieSearch(container, block, false, function () {
if (done) return;
if (--pending > 0) return;
done = true;
_grabBestAcross(container, blocks, hb);
});
});
}
function _grabBestAcross(container, blocks, hb) {
var best = null, bestPanel = null, bestIdx = -1;
blocks.forEach(function (block) {
var s = block.getAttribute('data-vdl-src-block');
var panel = block.querySelector('[data-vdl-results-for="' + s + '"]');
var rows = (panel && panel._rows) || [];
for (var i = 0; i < rows.length; i++) {
var r = rows[i];
if (!(r.accepted && r.username)) continue; // grabbable (Soulseek) + meets profile
var avail = r.peers || r.seeders || 0;
var bAvail = best ? (best.peers || best.seeders || 0) : -1;
if (!best || (r.score || 0) > (best.score || 0) ||
((r.score || 0) === (best.score || 0) && avail > bAvail)) {
best = r; bestPanel = panel; bestIdx = i;
}
}
});
if (hb) hb.disabled = false;
if (!best) { toast('Auto: no release met your quality profile on any source', 'error'); return; }
var card = bestPanel.querySelector('[data-vdl-card="' + bestIdx + '"]');
if (card) {
card.classList.add('vdl-res--auto');
if (card.scrollIntoView) card.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
}
var gbtn = card && card.querySelector('[data-vdl-grab]');
if (gbtn) { gbtn.disabled = true; gbtn.classList.add('vdl-res-grab--busy'); }
toast('Auto-picked best across sources: ' + (best.quality_label || best.title || 'release'), 'info');
sendGrab(buildGrabPayload(bestPanel, best)).then(function (res) {
if (res && res.ok) {
toast('Sent to Downloads', 'success');
beginTracking(card, res.id);
document.dispatchEvent(new CustomEvent('soulsync:video-download-started'));
} else {
if (gbtn) { gbtn.disabled = false; gbtn.classList.remove('vdl-res-grab--busy'); }
toast((res && res.error) || 'Auto: couldn’t start the download', 'error');
}
});
}
function onClick(e) {
var container = e.currentTarget;
var grab = e.target.closest('[data-vdl-grab]');
if (grab) { doGrab(grab); return; }
if (e.target.closest('[data-vdl-auto-best]')) { _autoBest(container); return; }
var ab = e.target.closest('[data-vdl-auto]');
if (ab) { _movieSearch(container, ab.closest('[data-vdl-src-block]'), true); return; }
var sb = e.target.closest('[data-vdl-search]');
if (sb) { _movieSearch(container, sb.closest('[data-vdl-src-block]')); return; }
}
// Render the download view into `container`. Re-callable (resets each time).
function render(container, opts) {
if (!container) return;
opts = opts || {};
if (opts.kind === 'show') { renderShow(container, opts); return; }
container._opts = opts;
container.innerHTML = contentHTML();
if (!container._vdlWired) { container._vdlWired = true; container.addEventListener('click', onClick); }
var isYt = !!opts.isYt;
getJSON(isYt ? '/api/video/downloads/youtube-quality' : '/api/video/downloads/quality')
.then(function (p) { if (container.isConnected && p) renderTarget(container, p, isYt); });
if (isYt) {
renderSources(container, ['youtube']);
} else {
getJSON('/api/video/downloads/config').then(function (c) {
if (container.isConnected) renderSources(container, sourcesFromConfig(c));
});
if (opts.file) renderOwned(container, opts.file);
}
// Resume tracking: if this title already has a download in flight (e.g. the
// user grabbed it, closed the modal, and re-opened), show a live banner.
watchActiveDownload(container, opts);
}
// Poll for an active/just-finished download of THIS title (by media identity) and
// surface a live banner at the top of the view — so re-opening the modal knows a
// download is already running. Suppressed while a result card is already tracking
// it inline (fresh-grab case), to avoid a duplicate indicator.
function watchActiveDownload(container, opts) {
var box = container.querySelector('[data-vdl-active]'); if (!box) return;
var mediaId = (opts.id != null ? opts.id : opts.mediaId);
if (mediaId == null) { box.hidden = true; return; }
var mediaSource = opts.source || opts.mediaSource || 'library';
if (container._activeT) { clearTimeout(container._activeT); container._activeT = null; }
(function tick() {
if (!container.isConnected) return; // modal closed → stop
getJSON('/api/video/downloads/status?media_id=' + encodeURIComponent(mediaId) +
'&media_source=' + encodeURIComponent(mediaSource)).then(function (d) {
if (!container.isConnected) return;
var dl = d && d.download;
var inlineTracker = !!container.querySelector('[data-vdl-track]'); // a card is already tracking
renderActiveBanner(box, (dl && !inlineTracker) ? dl : null);
var active = dl && ['downloading', 'queued', 'searching'].indexOf(dl.status) > -1;
if (active) container._activeT = setTimeout(tick, 1800);
});
})();
}
function renderActiveBanner(box, dl) {
var show = dl && ['downloading', 'queued', 'searching', 'completed', 'failed'].indexOf(dl.status) > -1;
if (!show) { box.hidden = true; box.innerHTML = ''; box._wired = false; return; }
box.hidden = false;
var st = dl.status, pct = Math.max(0, Math.min(100, dl.progress || 0));
if (st === 'completed') pct = 100;
box.className = 'vdl-active vdl-active--' + (st === 'completed' ? 'done' : (st === 'failed' ? 'fail' : 'active'));
var label = st === 'completed' ? 'Downloaded' : st === 'failed' ? 'Download failed'
: st === 'searching' ? 'Finding a release…' : st === 'queued' ? 'Queued' : 'Downloading';
var ic = st === 'completed' ? '✓' : st === 'failed' ? '✕' : '⤓';
var pctTxt = (st === 'downloading' || st === 'queued') ? pct + '%' : '';
box.innerHTML =
'' +
'
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, onDone) {
if (!resultsEl) return;
triggerRows = (triggerRows || []).filter(Boolean);
if (resultsEl._poll) { clearTimeout(resultsEl._poll); resultsEl._poll = null; }
resultsEl._rows = null; // drop any prior search's rows so Auto can't grab a stale hit
_setScanning(triggerRows, true);
resultsEl.hidden = false;
resultsEl.classList.remove('vdl-res-noanim');
resultsEl.innerHTML = '
';
}
// States the result-card tracker shows while a grabbed release downloads.
var TRACK_LABEL = { downloading: 'Downloading', queued: 'Queued',
searching: 'Finding another release…', completed: 'Downloaded', failed: 'Failed', cancelled: 'Cancelled' };
var TRACK_DONE = { completed: 1, failed: 1, cancelled: 1 };
// Close the modal (if any) and jump to the Downloads page.
function gotoDownloads() {
if (window.VideoGet && VideoGet.close) VideoGet.close();
document.dispatchEvent(new CustomEvent('soulsync:video-navigate', { detail: 'video-downloads' }));
}
// After a grab, turn the chosen card into a live tracker: a progress bar that
// follows the real download + a button that jumps to the Downloads page. Polls
// /downloads/status?id= until the download reaches a terminal state.
function beginTracking(card, dlId) {
if (!card) return;
card.classList.add('vdl-res--grabbed');
var gb = card.querySelector('[data-vdl-grab]'); if (gb) gb.remove();
var main = card.querySelector('.vdl-res-main') || card;
var foot = card.querySelector('[data-vdl-track]');
if (!foot) {
foot = document.createElement('div');
foot.className = 'vdl-res-track vdl-res-track--active';
foot.setAttribute('data-vdl-track', '');
foot.innerHTML =
'
' +
'Starting…' +
'' +
'' +
'
' +
'
';
if (main.nextSibling) card.insertBefore(foot, main.nextSibling); else card.appendChild(foot);
var go = foot.querySelector('[data-vdl-track-go]');
if (go) go.addEventListener('click', gotoDownloads);
}
_trackPoll(card, foot, dlId);
}
function _trackPoll(card, foot, dlId) {
if (foot._t) { clearTimeout(foot._t); foot._t = null; }
function tick() {
if (!card.isConnected) return; // modal closed → stop
getJSON('/api/video/downloads/status?id=' + encodeURIComponent(dlId)).then(function (d) {
if (!card.isConnected) return;
var dl = d && d.download;
if (!dl) { foot._t = setTimeout(tick, 2000); return; }
var st = dl.status;
var pct = Math.max(0, Math.min(100, dl.progress || 0));
if (st === 'completed') pct = 100;
var active = !(st in TRACK_DONE);
foot.className = 'vdl-res-track vdl-res-track--' +
(st === 'completed' ? 'done' : (st === 'failed' || st === 'cancelled' ? 'fail' : 'active'));
var fill = foot.querySelector('[data-vdl-track-fill]'); if (fill) fill.style.width = pct + '%';
var pctEl = foot.querySelector('[data-vdl-track-pct]');
if (pctEl) pctEl.textContent = (st === 'downloading' || st === 'queued') ? pct + '%' : '';
var stEl = foot.querySelector('[data-vdl-track-state]');
if (stEl) {
var spin = (st === 'downloading' || st === 'queued' || st === 'searching')
? '' : '';
var ic = st === 'completed' ? '✓ ' : (st === 'failed' || st === 'cancelled' ? '✕ ' : '');
stEl.innerHTML = spin + ic + esc(TRACK_LABEL[st] || 'Downloading');
}
if (active) foot._t = setTimeout(tick, 1700);
});
}
tick();
}
// ── 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 Manual search (a source row inside an expanded episode).
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;
}
// Episode-scope Auto (search this source, then auto-grab the best) — mirrors the
// movie per-source Auto, scoped to the episode.
var au = e.target.closest('[data-vdl-auto]');
if (au) {
var epA = au.closest('.vdl-ep'); if (!epA) return;
var pa = (epA.getAttribute('data-vdl-ep') || '').split('_');
var sa = au.getAttribute('data-vdl-auto');
var resA = au.closest('[data-vdl-src-block]').querySelector('[data-vdl-results-for="' + sa + '"]');
var rowA = au.closest('.vdl-src');
searchInto(container, resA,
{ scope: 'episode', title: st.title, season: +pa[0], episode: +pa[1], source: sa },
[rowA], function () { _autoPick(resA, rowA); });
return;
}
// Grab whole season — auto-grab every MISSING episode individually (episode-level).
var sg = e.target.closest('[data-vdl-season-grab]');
if (sg) { grabSeason(container, st, +sg.getAttribute('data-vdl-season-grab')); 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 = '
';
}
// ── per-episode live tracking ──────────────────────────────────────────────
// Every episode ROW carries its own live download status (Searching → Downloading
// % → Downloaded / Failed), so season grabs aren't headless and the modal shows
// exactly what each episode is doing — matching the inline movie tracker.
function _epEl(container, sn, en) {
return container.querySelector('.vdl-ep[data-vdl-ep="' + sn + '_' + en + '"]');
}
function epStatusRender(epEl, dl) {
var stEl = epEl.querySelector('[data-vdl-ep-status]'); if (!stEl) return;
var st = dl.status, pct = Math.max(0, Math.min(100, dl.progress || 0));
if (st === 'completed') pct = 100;
epEl.classList.remove('vdl-ep--dl-active', 'vdl-ep--dl-done', 'vdl-ep--dl-fail');
if (st === 'completed') {
epEl.classList.add('vdl-ep--dl-done');
stEl.innerHTML = '✓ Downloaded';
} else if (st === 'failed' || st === 'cancelled') {
epEl.classList.add('vdl-ep--dl-fail');
stEl.innerHTML = '✗ ' + esc(TRACK_LABEL[st] || 'Failed') + '';
} else {
epEl.classList.add('vdl-ep--dl-active');
var label = st === 'downloading' ? ('Downloading ' + pct + '%')
: (st === 'searching' ? 'Searching…' : (TRACK_LABEL[st] || 'Queued'));
stEl.innerHTML = '' +
'' + esc(label) + '';
}
}
function epSearching(epEl) {
if (!epEl) return;
epEl.classList.remove('vdl-ep--dl-done', 'vdl-ep--dl-fail');
epEl.classList.add('vdl-ep--dl-active');
var s = epEl.querySelector('[data-vdl-ep-status]');
if (s) s.innerHTML = 'Searching…';
}
function epNoRelease(epEl) {
if (!epEl) return;
epEl.classList.remove('vdl-ep--dl-active');
var s = epEl.querySelector('[data-vdl-ep-status]');
if (s) s.innerHTML = 'No release found';
}
// Poll one episode's download by id and paint its row until it finishes. Robust to
// the modal closing (stops) and to a newer grab on the same row (id guard).
function epTrack(epEl, dlId) {
if (!epEl || dlId == null) return;
if (epEl._eptimer) { clearTimeout(epEl._eptimer); epEl._eptimer = null; }
epEl._epdl = dlId;
(function tick() {
if (!epEl.isConnected || epEl._epdl !== dlId) return;
getJSON('/api/video/downloads/status?id=' + encodeURIComponent(dlId)).then(function (d) {
if (!epEl.isConnected || epEl._epdl !== dlId) return;
var dl = d && d.download;
if (!dl) { epEl._eptimer = setTimeout(tick, 2200); return; }
epStatusRender(epEl, dl);
if (!(dl.status in TRACK_DONE)) epEl._eptimer = setTimeout(tick, 2000);
});
})();
}
// Pick the best accepted+grabbable hit in a results panel and grab it. Returns
// {ok, id} — the id is what the episode row tracks. (Same payload as a manual grab.)
function _pickAndGrab(panel) {
var rows = (panel && panel._rows) || [];
var best = null;
for (var i = 0; i < rows.length; i++) {
if (rows[i].accepted && rows[i].username) { best = rows[i]; break; }
}
if (!best) return Promise.resolve({ ok: false });
return sendGrab(buildGrabPayload(panel, best)).then(function (res) {
return (res && res.ok) ? { ok: true, id: res.id } : { ok: false };
});
}
// ── Grab whole season (episode level) ──────────────────────────────────────
// Run the per-episode auto-grab for every MISSING episode, throttled — each takes
// the same path a manual per-episode Auto would, and each ROW shows live status.
function ensureScratch(container) {
var s = container.querySelector('[data-vdl-grab-scratch]');
if (!s) {
s = document.createElement('div');
s.setAttribute('data-vdl-grab-scratch', '');
s.style.display = 'none';
container.appendChild(s);
}
return s;
}
function autoGrabEpisode(container, st, sn, en, src) {
// Resolves once the search SETTLES (throttle the searches, not the grabs); the
// row then tracks the live download itself.
return new Promise(function (resolve) {
var epEl = _epEl(container, sn, en);
epSearching(epEl);
var panel = document.createElement('div');
panel.className = 'vdl-results vdl-res-noanim';
ensureScratch(container).appendChild(panel);
searchInto(container, panel,
{ scope: 'episode', title: st.title, season: sn, episode: en, source: src },
[], function () {
_pickAndGrab(panel).then(function (r) {
if (r.ok) {
epTrack(epEl, r.id);
document.dispatchEvent(new CustomEvent('soulsync:video-download-started'));
} else {
epNoRelease(epEl);
}
resolve(r);
});
});
});
}
function grabSeason(container, st, sn) {
var src = (st.sources || []).filter(function (s) { return SRC_META[s]; })[0];
if (!src) { toast('No download source configured', 'error'); return; }
var eps = [];
for (var k in st.epMeta) {
if (k.indexOf(sn + '_') === 0 && st.epMeta[k].state === 'missing') eps.push(+k.split('_')[1]);
}
eps.sort(function (a, b) { return a - b; });
if (!eps.length) { toast('No missing episodes in this season', 'info'); return; }
// make sure the season is open so the user sees the rows light up
var card = container.querySelector('.vdl-season[data-vdl-season="' + sn + '"]');
if (card) card.classList.add('vdl-season--open');
eps.forEach(function (en) { epSearching(_epEl(container, sn, en)); }); // immediate feedback
var btn = container.querySelector('[data-vdl-season-grab="' + sn + '"]');
if (btn) { btn.disabled = true; btn.textContent = 'Grabbing…'; }
toast('Grabbing ' + eps.length + ' episode' + (eps.length > 1 ? 's' : '') + ' — each row shows live status', 'info');
var idx = 0, active = 0, done = 0, MAX = 3;
function pump() {
while (active < MAX && idx < eps.length) {
active++;
autoGrabEpisode(container, st, sn, eps[idx++], src).then(function () {
active--; done++;
if (done >= eps.length) { if (btn) { btn.disabled = false; btn.textContent = 'Grab season'; } }
else pump();
});
}
}
pump();
}
// On (re)open, resume live tracking for any episodes of THIS show already in flight,
// so closing/reopening the modal never loses the per-episode progress.
function resumeEpisodeTracking(container, st) {
getJSON('/api/video/downloads/active').then(function (d) {
((d && d.downloads) || []).forEach(function (dl) {
var ctx = dl.search_ctx;
if (typeof ctx === 'string') { try { ctx = JSON.parse(ctx); } catch (e) { ctx = null; } }
ctx = ctx || {};
if (String(ctx.title || dl.title) !== String(st.title)) return;
if (ctx.season == null || ctx.episode == null) return;
var epEl = _epEl(container, ctx.season, ctx.episode);
if (epEl && epEl._epdl == null) { epStatusRender(epEl, dl); epTrack(epEl, dl.id); }
});
});
}
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 };
})();