download job: also write a reference sidecar for FAILED items

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 `<title>.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>
This commit is contained in:
2026-07-28 19:34:35 +00:00
co-authored by Claude Opus 4.8
parent 7c43ff2291
commit b523c7d408
+21
View File
@@ -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();
}