Files
ytplayer/server/download-retry.js
2026-10-03 19:16:08 +08:00

29 lines
1.7 KiB
JavaScript

// Explicit user retries bypass a failed cache row's backoff, not disk guards.
export function registerDownloadRetryRoute(app, { media, priority, clearHints = () => {}, now = Date.now, log = console }) {
const recent = new Map();
app.post('/api/download/:videoId/retry', async c => {
const id = (c.req.param('videoId') || '').trim();
const headers = { 'Cache-Control': 'no-store' };
if (!/^[A-Za-z0-9_-]{11}$/.test(id)) return c.json({ ok: false, error: 'invalid video id' }, 400, headers);
// Already-ready and active jobs are reusable, so Retry never deletes a
// playable copy or spawns a second downloader for the same song.
const status = await media.status(id);
if (status.status === 'ready' || status.status === 'queued' || ['downloading', 'validating'].includes(status.status)) {
clearHints(id);
return c.json({ ok: true, state: status.status === 'ready' ? 'ready' : 'working' }, 200, headers);
}
const time = now();
if (recent.has(id) && time - recent.get(id) < 15_000) {
return c.json({ ok: false, error: 'Please wait a few seconds before trying again.' }, 429, { ...headers, 'Retry-After': '15' });
}
recent.set(id, time);
if (recent.size > 2000) for (const [key, at] of recent) if (time - at >= 15_000) recent.delete(key);
if (recent.size > 2000) recent.delete(recent.keys().next().value);
clearHints(id);
// The existing cache owns the asynchronous job and deduplicates races.
// Its normal USB-volume, free-space, size and validation checks still run.
media.ensureCached(id, { priority, force: true }).catch(error => log.warn?.(`[download retry] ${id}: ${error.message}`));
return c.json({ ok: true, state: 'working' }, 202, headers);
});
}