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).