Restart failed saves through a guarded server retry

This commit is contained in:
Jonathan Sykes
2026-10-03 19:16:08 +08:00
parent 5485a3c2f4
commit f341b0c76e
7 changed files with 162 additions and 4 deletions

View File

@@ -437,7 +437,7 @@ async function resumableOpfsSave(videoId, qs, hevc, onProgress, signal) {
return { ok: false, error: 'the server copy kept changing — try again' };
}
async function opfsDownload(videoId, { mux = false, onProgress = null, signal } = {}) {
async function opfsDownload(videoId, { mux = false, retry = false, onProgress = null, signal } = {}) {
if (!window.OPFS || !window.OPFS.isSupported()) {
return { ok: false, error: 'OPFS not supported in this browser' };
}
@@ -452,6 +452,15 @@ async function opfsDownload(videoId, { mux = false, onProgress = null, signal }
const qs = params.toString();
const url = `/api/download/${encodeURIComponent(videoId)}${qs ? '?' + qs : ''}`;
if (retry) {
try {
const response = await fetch(`/api/download/${encodeURIComponent(videoId)}/retry`, { method: 'POST', cache: 'no-store', signal });
const result = await response.json();
if (!response.ok || !result.ok) return { ok: false, error: result.error || 'Could not restart this save.' };
} catch (error) {
return { ok: false, paused: true, error: error.name === 'AbortError' ? 'Save paused' : 'Connection lost' };
}
}
const resumed = await resumableOpfsSave(videoId, qs, params.get('hevc') === '1', onProgress, signal);
if (resumed) return resumed;
@@ -1791,7 +1800,7 @@ function recordDeviceFile(id, res) {
}
// Download a video into the permanent offline cache. Safe to call repeatedly.
async function preload(video, { quiet = false, mux = false } = {}) {
async function preload(video, { quiet = false, mux = false, retry = false } = {}) {
const id = video.id;
// Custom (edited) videos have no YouTube source to (re)download — their
// media is produced once by the editor. Never route them through the normal
@@ -1816,7 +1825,7 @@ async function preload(video, { quiet = false, mux = false } = {}) {
await SaveSlots.acquire(controller.signal);
slot = true;
SaveQueue.progress(id, null);
const res = await API.cacheDownload(id, { mux, signal: controller.signal, onProgress: (p) => SaveQueue.progress(id, p) });
const res = await API.cacheDownload(id, { mux, retry, signal: controller.signal, onProgress: (p) => SaveQueue.progress(id, p) });
if (res && res.paused) {
// The partial stays on the device; SaveQueue picks it up again.
if (!quiet) toast(`Paused “${video.title}” — it will continue when the connection is back`);
@@ -12560,7 +12569,7 @@ async function boot() {
updateBottomChrome();
if (window.Downloads) Downloads.configure({
jobs: () => SaveQueue.pending().map(v => ({ ...v, status: downloading.has(v.id) ? (SaveQueue.get(v.id)?.phase === 'waiting' ? 'queued' : 'active') : v.status || 'paused', progress: SaveQueue.get(v.id) })),
action: (id, action) => { if (action === 'pause') SaveQueue.pause(id); else if (action === 'cancel') SaveQueue.cancel(id); else { const v = SaveQueue.pending().find(v => v.id === id); if (v) preload(v, { quiet: true }); } },
action: (id, action) => { if (action === 'pause') SaveQueue.pause(id); else if (action === 'cancel') SaveQueue.cancel(id); else { const v = SaveQueue.pending().find(v => v.id === id); if (v) preload(v, { quiet: true, retry: action === 'retry' }); } },
playlists: () => data.playlists, slots: () => data.settings.parallelSaves || 4,
files: async () => { const result = []; if (!WEB || !navigator.storage?.getDirectory) return result;
const root = await navigator.storage.getDirectory();