';
// big actions
var btns = [];
if (d.media_id && !isYt) btns.push('');
if (isYt && d.media_id) btns.push('Open on YouTube');
if (isActive(d.status)) btns.push('');
else if (isFail(d.status)) btns.push('');
var actions = btns.length ? '
' + btns.join('') + '
' : '';
var syn = loading ? '
Loadingβ¦
'
: (overview ? '
' + esc(overview) + '
'
: (isYt ? '' : '
No synopsis available.
'));
return back + '
' +
'
' + titleHTML + metaLine + tagline + '
' +
syn + (genres ? '
' + esc(genres) + '
' : '') + watchHTML +
castHTML + '
Download
' + facts + '
' +
actions + '
';
}
function renderDrawer(el, d) {
var dr = el.querySelector('[data-f="drawer"]'); if (!dr) return;
var open = !!_expanded[d.id];
el.classList.toggle('vdpg-card-open', open);
dr.hidden = !open;
if (!open) { dr.innerHTML = ''; return; }
dr.innerHTML = drawerHTML(d, _meta[d.id]);
// lazily fetch TMDB detail for movie/TV (skip youtube + owned library re-grabs)
if (_meta[d.id] === undefined && d.media_id && dlType(d.kind) !== 'youtube' && d.media_source !== 'library') {
_meta[d.id] = null;
var k = dlType(d.kind) === 'movie' ? 'movie' : 'show';
getJSON('/api/video/downloads/meta/' + k + '/' + encodeURIComponent(d.media_id)).then(function (m) {
_meta[d.id] = m || {};
if (_expanded[d.id]) { var dr2 = el.querySelector('[data-f="drawer"]'); if (dr2) dr2.innerHTML = drawerHTML(d, _meta[d.id]); }
});
}
}
function render(list) {
var host = document.querySelector('[data-vdpg-list]'); if (!host) return;
list = list || [];
var empty = host.querySelector('[data-vdpg-empty]');
var counts = { all: list.length, active: 0, completed: 0, failed: 0 };
list.forEach(function (d) {
if (isActive(d.status)) counts.active++;
else if (d.status === 'completed') counts.completed++;
else counts.failed++;
});
setDownloadsBadge(counts.active); // sidebar live count (this page's poll keeps it fresh)
var cancelAll = document.querySelector('[data-vdpg-cancel-all]'); if (cancelAll) cancelAll.style.display = counts.active ? '' : 'none';
var clearBtn = document.querySelector('[data-vdpg-clear]'); if (clearBtn) clearBtn.style.display = (counts.completed + counts.failed) ? '' : 'none';
var sub = document.querySelector('[data-vdpg-sub]');
if (sub) {
var parts = [];
if (counts.active) parts.push(counts.active + ' active');
if (counts.completed) parts.push(counts.completed + ' done');
if (counts.failed) parts.push(counts.failed + ' failed');
sub.textContent = parts.join(' Β· ');
}
var seen = {}, shown = 0;
list.forEach(function (d) {
seen[d.id] = true;
var el = _cards[d.id] || (_cards[d.id] = makeCard(d));
patchCard(el, d);
var vis = matches(d.status);
el.style.display = vis ? '' : 'none';
if (vis) shown++;
host.appendChild(el); // keep order = server order (active first); no re-anim
});
Object.keys(_cards).forEach(function (id) {
if (!seen[id]) { var e = _cards[id]; if (e && e.parentNode) e.parentNode.removeChild(e); delete _cards[id]; }
});
if (empty) {
host.appendChild(empty); // keep the empty element last
empty.style.display = shown === 0 ? '' : 'none';
empty.textContent = !list.length ? "No downloads yet. Hit Grab on a search result and it'll show up here."
: 'Nothing ' + (_filter === 'all' ? 'here' : _filter) + ' right now.';
}
}
function setFilter(f) {
_filter = f;
Array.prototype.forEach.call(document.querySelectorAll('[data-vdpg-filter]'), function (b) {
b.classList.toggle('active', b.getAttribute('data-vdpg-filter') === f);
});
getJSON(URL_ACTIVE).then(function (d) { if (d) render(d.downloads || []); });
}
function anyActive() { return !!document.querySelector('.adl-row.adl-row-active, .adl-row.adl-row-queued'); }
// Only poll while the Downloads page is actually on screen β not in the background
// and not after switching to the music side (where the page-change event never fires).
function _onPage() {
return document.body.getAttribute('data-side') === 'video' &&
!!document.querySelector('[data-video-subpage="video-downloads"]:not([hidden])');
}
function poll() {
if (!_onPage()) { stop(); return; }
getJSON(URL_ACTIVE).then(function (d) { if (d) render(d.downloads || []); schedule(); });
}
function schedule() { if (_timer) clearTimeout(_timer); _timer = setTimeout(poll, anyActive() ? 2000 : 6000); }
function start() { wire(); if (_timer) clearTimeout(_timer); poll(); }
function stop() { if (_timer) { clearTimeout(_timer); _timer = null; } }
function wire() {
if (_wired) return; _wired = true;
var clearBtn = document.querySelector('[data-vdpg-clear]');
if (clearBtn) clearBtn.addEventListener('click', function () {
postJSON(URL_CLEAR, {}).then(function () { toast('Cleared finished downloads', 'success'); poll(); });
});
var cancelAll = document.querySelector('[data-vdpg-cancel-all]');
if (cancelAll) cancelAll.addEventListener('click', function () {
getJSON(URL_ACTIVE).then(function (d) {
var ids = ((d && d.downloads) || []).filter(function (x) { return isActive(x.status); }).map(function (x) { return x.id; });
Promise.all(ids.map(function (id) { return postJSON(URL_CANCEL, { id: id }); }))
.then(function () { toast('Cancelled ' + ids.length + ' download' + (ids.length === 1 ? '' : 's'), 'info'); poll(); });
});
});
var pills = document.querySelector('[data-vdpg-pills]');
if (pills) pills.addEventListener('click', function (e) {
var b = e.target.closest('[data-vdpg-filter]'); if (b) setFilter(b.getAttribute('data-vdpg-filter'));
});
var list = document.querySelector('[data-vdpg-list]');
if (list) list.addEventListener('click', function (e) {
var op = e.target.closest('[data-vdpg-open]');
if (op) {
var kind = op.getAttribute('data-kind') === 'movie' ? 'movie' : 'show';
var id = op.getAttribute('data-vdpg-open');
document.dispatchEvent(new CustomEvent('soulsync:video-open-detail', {
detail: { kind: kind, id: parseInt(id, 10) || id, source: op.getAttribute('data-source') || 'library' }
}));
return;
}
var cp = e.target.closest('[data-vdpg-copy]');
if (cp) {
var path = cp.getAttribute('data-vdpg-copy');
if (navigator.clipboard) navigator.clipboard.writeText(path).then(function () { toast('Path copied', 'success'); }, function () {});
else toast('Copy not supported here', 'info');
return;
}
var c = e.target.closest('[data-vdpg-cancel]');
if (c) { c.disabled = true; c.classList.add('adl-row-cancel-pending'); postJSON(URL_CANCEL, { id: +c.getAttribute('data-vdpg-cancel') }).then(function () { poll(); }); return; }
var r = e.target.closest('[data-vdpg-retry]');
if (r) { r.disabled = true; postJSON(URL_RETRY, { id: +r.getAttribute('data-vdpg-retry') }).then(function (res) {
if (res && res.ok) toast('Retrying', 'info'); else toast((res && res.error) || 'Retry failed', 'error'); poll(); }); return; }
// click anywhere on the row (but not the drawer body or a control) β toggle the detail drawer
if (e.target.closest('[data-f="drawer"]') || e.target.closest('button, a')) return;
var card = e.target.closest('.adl-row[data-dl-id]');
if (card && card._d) {
var cid = card.getAttribute('data-dl-id');
_expanded[cid] = !_expanded[cid];
renderDrawer(card, card._d);
}
});
}
document.addEventListener('soulsync:video-page-shown', function (e) {
if (e.detail === 'video-downloads') start(); else stop();
});
document.addEventListener('soulsync:video-download-started', function () {
if (document.querySelector('[data-video-subpage="video-downloads"]:not([hidden])')) setTimeout(poll, 350);
});
// Keep the sidebar Downloads badge live even when you're NOT on the page (the on-page
// poll already refreshes it, so skip the fetch then). Only runs on the video side.
var _badgeTimer = null;
function badgePoll() {
var onVideo = document.body.getAttribute('data-side') === 'video';
if (onVideo && !_onPage() && !document.hidden) {
getJSON(URL_ACTIVE).then(function (d) {
if (d) setDownloadsBadge((d.downloads || []).filter(function (x) { return isActive(x.status); }).length);
scheduleBadgePoll();
});
} else { scheduleBadgePoll(); }
}
function scheduleBadgePoll() { if (_badgeTimer) clearTimeout(_badgeTimer); _badgeTimer = setTimeout(badgePoll, 8000); }
document.addEventListener('soulsync:video-wishlist-changed', function () { setTimeout(badgePoll, 200); });
scheduleBadgePoll();
window._vdpgAnyActive = anyActive;
})();