' : '';
}
function drawerHTML(d, meta) {
var isYt = dlType(d.kind) === 'youtube', ctx = parseCtx(d);
var loading = meta === null;
meta = meta || {};
var back = '', head = '', lead = '', extra = '';
if (isYt) {
// big thumbnail + channel Β· duration Β· views Β· upload date, then the description
var thumb = meta.thumbnail_url || d.poster_url;
var yb = [];
if (ctx.channel || ctx.channel_title) yb.push(esc(ctx.channel || ctx.channel_title));
if (meta.duration) yb.push(esc(meta.duration));
if (meta.view_count) yb.push(fmtViews(meta.view_count) + ' views');
if (ctx.published_at) yb.push(esc(String(ctx.published_at).slice(0, 10)));
head = '
';
var btns = [];
if (d.media_id && !isYt) btns.push('');
var ytCh = meta.channel_id || ctx.channel_id;
if (isYt && ytCh) 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('') + '
' : '';
return back + '
' + head + lead + extra +
'
Download
' + facts + '
' +
actions + '
';
}
function metaURL(d) {
var t = dlType(d.kind);
if (t === 'youtube') return '/api/video/downloads/yt-meta/' + encodeURIComponent(d.media_id);
if (d.media_source === 'library') return null; // owned re-grab: media_id isn't a tmdb id
var url = '/api/video/downloads/meta/' + (t === 'movie' ? 'movie' : 'show') + '/' + encodeURIComponent(d.media_id);
if (d.kind === 'episode') {
var c = parseCtx(d);
if (c.season != null && c.episode != null) url += '?season=' + encodeURIComponent(c.season) + '&episode=' + encodeURIComponent(c.episode);
}
return url;
}
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; }
// kick off the lazy detail fetch (TMDB for movie/TV, cached metadata for youtube) so the
// first paint already shows 'Loadingβ¦' rather than 'no synopsis' flashing before content.
if (_meta[d.id] === undefined && d.media_id) {
var url = metaURL(d);
if (!url) { _meta[d.id] = {}; }
else {
_meta[d.id] = null;
getJSON(url).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]); }
});
}
}
dr.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 och = e.target.closest('[data-vdpg-open-channel]');
if (och) {
document.dispatchEvent(new CustomEvent('soulsync:video-open-detail', {
detail: { kind: 'channel', source: 'youtube', id: och.getAttribute('data-vdpg-open-channel') }
}));
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;
})();