download job: write each item's description as a sidecar .txt next to the media
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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<void> {
|
||||
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<vo
|
||||
progress.phase = 'metadata';
|
||||
progress.meta.total = urls.length;
|
||||
emit(true);
|
||||
const valid: Array<{ url: string; title: string }> = [];
|
||||
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<vo
|
||||
audioOnly,
|
||||
title: item.title,
|
||||
signal: params.abortSignal,
|
||||
// Filename is known before the file transfers — write the description sidecar alongside it, in
|
||||
// parallel with the download stream. Same base name guarantees they pair up.
|
||||
onFilename: (filename) => void writeDescriptionSidecar(absDir, filename, item.description),
|
||||
});
|
||||
progress.dl.done++;
|
||||
} catch {
|
||||
|
||||
@@ -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<string> {
|
||||
}
|
||||
}
|
||||
|
||||
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).
|
||||
|
||||
Reference in New Issue
Block a user