/* ── Downloads page ─────────────────────────────────────────── */ function escH(s) { return String(s).replace(/&/g,'&').replace(//g,'>'); } function fmtSpeed(b) { return b>1048576?(b/1048576).toFixed(1)+' MB/s':b>1024?(b/1024).toFixed(0)+' KB/s':b+' B/s'; } function fmtEta(s) { if (s < 0) return ''; if (s < 60) return s + 's'; if (s < 3600) return Math.floor(s/60) + 'm ' + (s%60) + 's'; return Math.floor(s/3600) + 'h ' + Math.floor((s%3600)/60) + 'm'; } function renderDownloads() { fetch(BASE + '/api/downloads', {cache: 'no-store'}) .then(function(r) { return r.json(); }) .then(function(dl) { var el = document.getElementById('dl-table'); if (!dl.length) { el.innerHTML = '

No downloads yet.

'; return; } var html = ''; dl.slice().reverse().forEach(function(d) { var pct = d.pct || 0, mb = (d.downloaded/1048576).toFixed(1); var cls = d.status==='done' ? 'done' : d.status.startsWith('error') ? 'err' : d.status==='downloading' ? 'dl' : ''; var thumb = d.cover_url ? '' : ''; html += ''; html += ''; }); html += '
NameStatusProgressSpeedETA
' + thumb + '' + escH(d.name) + '' + escH(d.status) + ''; if (d.status === 'downloading') html += '
 ' + pct + '% (' + mb + 'MB)'; else html += pct + '%'; html += '
' + (d.status==='downloading'&&d.speed_bps>0 ? fmtSpeed(d.speed_bps) : '—') + '' + (d.status==='downloading'&&d.eta_s>=0 ? fmtEta(d.eta_s) : '—') + ''; if (d.status==='downloading' || d.status==='queued') html += ''; html += '
'; el.innerHTML = html; }).catch(function() {}); } function cancelDl(id) { fetch(BASE + '/api/cancel', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({id:id})}) .then(function() { setTimeout(renderDownloads, 400); }); } function clearDl() { fetch(BASE + '/api/clear', {method:'POST'}).then(function() { setTimeout(renderDownloads, 400); }); } function clearCancelled() { fetch(BASE + '/api/clear-cancelled', {method:'POST'}).then(function() { setTimeout(renderDownloads, 400); }); } function retryInterrupted() { fetch(BASE + '/api/retry-interrupted', {method:'POST'}).then(function() { setTimeout(renderDownloads, 400); }); } function cleanPartials() { fetch(BASE + '/api/clean-partials', {method:'POST'}).then(function() { renderDownloads(); }); } renderDownloads(); setInterval(renderDownloads, 3000);