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 `
.txt` (or `.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 {
+ 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