From 7c43ff229118b266a19766385edc08a8adc49a9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Tue, 28 Jul 2026 19:26:16 +0000 Subject: [PATCH] download job: write each item's description as a sidecar .txt next to the media MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The metadata phase already fetches ReClip's description (ReClip now forwards it in /api/info). Carry it into the download phase and write it to a text file with the same base name as the media — "Song Name.mp3" → "Song Name.txt". reclipDownloadOne gains an onFilename callback that fires the moment the final filename is known (before the file transfers), so the executor writes the sidecar in parallel with the download stream, and the exact name guarantees they pair up. Empty descriptions write nothing; the write is best-effort (never fails a download). Verified: correct base name + .txt, exact content, and no sidecar for an empty description. Co-Authored-By: Claude Opus 4.8 --- src/servers/api/tasks/execute-download.ts | 22 ++++++++++++++++++++-- src/servers/reclip-client.ts | 3 +++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/servers/api/tasks/execute-download.ts b/src/servers/api/tasks/execute-download.ts index 3f26f255..74b2071b 100644 --- a/src/servers/api/tasks/execute-download.ts +++ b/src/servers/api/tasks/execute-download.ts @@ -1,5 +1,20 @@ +import { writeFile } from 'node:fs/promises'; +import { join } from 'node:path'; import { reclipInfo, reclipPlaylist, reclipDownloadOne, type ReclipInfo } from '../../reclip-client'; +// Write a per-item description sidecar next to its media file — same base name, `.txt` (e.g. +// "Song Name.mp3" → "Song Name.txt"). Best-effort; skipped when the description is empty. +async function writeDescriptionSidecar(dir: string, mediaFilename: string, description: string): Promise { + const desc = description.trim(); + if (!desc) return; + const base = mediaFilename.replace(/\.[^./\\]+$/, ''); // strip the media extension only + try { + await writeFile(join(dir, `${base}.txt`), desc, 'utf8'); + } catch { + /* best-effort — a missing sidecar shouldn't fail the download */ + } +} + // 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 @@ -80,12 +95,12 @@ export async function executeDownload(params: ExecuteDownloadParams): Promise = []; + const valid: Array<{ url: string; title: string; description: string }> = []; for (const u of urls) { checkAbort(); const info = await reclipInfo(u).catch((): ReclipInfo => ({ error: 'fetch failed' })); if (info && !info.error) { - valid.push({ url: u, title: info.title ?? '' }); + valid.push({ url: u, title: info.title ?? '', description: info.description ?? '' }); progress.meta.done++; } else { progress.meta.failed++; @@ -109,6 +124,9 @@ export async function executeDownload(params: ExecuteDownloadParams): Promise void writeDescriptionSidecar(absDir, filename, item.description), }); progress.dl.done++; } catch { diff --git a/src/servers/reclip-client.ts b/src/servers/reclip-client.ts index 213a5394..1135ca2a 100644 --- a/src/servers/reclip-client.ts +++ b/src/servers/reclip-client.ts @@ -10,6 +10,7 @@ const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); export type ReclipInfo = { title?: string; + description?: string; thumbnail?: string; duration?: number; uploader?: string; @@ -47,6 +48,7 @@ type DownloadOpts = { audioOnly: boolean; title?: string; // optional override; omit and ReClip names the file from the video title itself onPhase?: (phase: 'transferring') => void; + onFilename?: (filename: string) => void; // fired as soon as the final filename is known (before streaming) signal?: { aborted: boolean }; }; @@ -91,6 +93,7 @@ export async function reclipDownloadOne(opts: DownloadOpts): Promise { } } + opts.onFilename?.(filename); // exact name known — the caller can write a sidecar while we stream opts.onPhase?.('transferring'); // Stream the finished file into the destination folder (filename is title-sanitized by ReClip).