From b523c7d408b9f2e6693fb5a60ab876a68e42311a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Tue, 28 Jul 2026 19:34:35 +0000 Subject: [PATCH] download job: also write a reference sidecar for FAILED items MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A failed download has no media file, so its description was lost — no way to tell which video was missed. Now a failure writes a `.txt` (or `<videoId>.txt` when untitled) holding the URL + description, so every missed item leaves a recoverable reference. Always written (even with an empty description — the URL is the reference); skipped only on a deliberate Stop, not a genuine error. Verified: successful item → "<media base>.txt" (description); failed item → "<title>.txt" (url + description); spaces preserved in both. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --- src/servers/api/tasks/execute-download.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/servers/api/tasks/execute-download.ts b/src/servers/api/tasks/execute-download.ts index 74b2071b..dc4c7763 100644 --- a/src/servers/api/tasks/execute-download.ts +++ b/src/servers/api/tasks/execute-download.ts @@ -15,6 +15,26 @@ async function writeDescriptionSidecar(dir: string, mediaFilename: string, descr } } +const FS_ILLEGAL = /[/\\:*?"<>|\u0000-\u001f]/g; +const safeName = (s: string) => s.replace(FS_ILLEGAL, '_').replace(/\s+/g, ' ').trim().slice(0, 180); + +// On a FAILED download there's no media file to pair with — still drop a reference so the missed video is +// recoverable: a `<title>.txt` (or `<videoId>.txt` when untitled) holding the URL + the description. Always +// written (even with an empty description — the URL is the reference). +async function writeMissedSidecar( + dir: string, + item: { url: string; title: string; description: string }, +): Promise<void> { + const vid = item.url.match(/[?&]v=([\w-]+)/)?.[1]; + const name = safeName(item.title) || vid || 'missed'; + const content = [item.url, item.description.trim()].filter(Boolean).join('\n\n'); + try { + await writeFile(join(dir, `${name}.txt`), content, 'utf8'); + } catch { + /* best-effort */ + } +} + // The download-job executor — pure scripting, no agent. Two phases: // 1. metadata — fetch each item's info (title + validity); keep the ones that resolve, skip the errors // (private / deleted / unavailable). The title is required: ReClip names the output file @@ -132,6 +152,7 @@ export async function executeDownload(params: ExecuteDownloadParams): Promise<vo } catch { checkAbort(); // an abort surfaces as a throw here — re-check so it stops instead of counting as a skip progress.dl.failed++; + void writeMissedSidecar(absDir, item); // no media file — leave a reference to what we missed } emit(); }