Files
platform/src/servers/api/tasks/execute-download.ts
T
pastilhasandClaude Opus 4.8 c55ec5884b download job: collapse to one phase (no metadata prefetch)
The job's two phases were a misread — the "count" phase is the client-side
playlist expansion (for the inline-vs-job decision, already done in the panel).
The job itself is just one download request per item.

Dropped the in-job metadata pass entirely:
- reclip-client: reclipDownloadOne no longer prefetches /api/info for a title —
  ReClip names the file from the video title itself, so it's a single request
  per item.
- execute-download: one phase — expand the playlist, then /api/download each url,
  skip failures. Progress is a single { done, failed, total, current } counter
  (no meta/dl split); ~2× faster and downloads start right after expansion.
- UI (DownloadJobDetail + panel JobView): one "Downloaded" bar instead of two.

Verified: every item is attempted directly (no /api/info gate), skip-on-error
counts correct.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 18:35:10 +00:00

81 lines
2.8 KiB
TypeScript

import { reclipPlaylist, reclipDownloadOne } from '../../reclip-client';
// The download-job executor — pure scripting, no agent. One phase: expand the playlist, then one download
// request per item in the chosen format (audio/video), skipping anything that fails (private / deleted /
// download error). No metadata prefetch — ReClip names the file from the video title itself. Emits a
// compact `download:progress` snapshot (counters, not per-item events — a playlist can be thousands of
// items). Throws on abort or a fatal error (playlist expansion); per-item errors are counted + skipped.
export type DownloadProgress = {
phase: 'expanding' | 'download' | 'done';
done: number; // downloaded successfully
failed: number; // skipped (unavailable / download error)
total: number;
current?: string; // url of the item currently downloading
};
export type DownloadEvent = { type: 'download:progress'; progress: DownloadProgress };
export type ExecuteDownloadParams = {
jobId: string;
userId: number;
email: string;
username?: string | null;
inputs: Record<string, string>; // { url, format: 'audio'|'video', absDir }
cwd?: string;
abortSignal: { aborted: boolean };
emit: (event: DownloadEvent) => void;
};
const EMIT_THROTTLE_MS = 750;
export async function executeDownload(params: ExecuteDownloadParams): Promise<void> {
const { url, format, absDir } = params.inputs;
if (!url || !absDir) throw new Error('download job missing url or target directory');
const audioOnly = format !== 'video'; // default to audio
const progress: DownloadProgress = { phase: 'expanding', done: 0, failed: 0, total: 0 };
let lastEmit = 0;
const emit = (force = false) => {
const now = Date.now();
if (!force && now - lastEmit < EMIT_THROTTLE_MS) return;
lastEmit = now;
params.emit({ type: 'download:progress', progress: { ...progress } });
};
const checkAbort = () => {
if (params.abortSignal.aborted) throw new Error('aborted');
};
emit(true);
// ── Expand ──
let urls = [url];
if (url.includes('list=')) {
const pl = await reclipPlaylist(url);
if (pl.error) throw new Error(pl.error);
if (pl.urls?.length) urls = pl.urls;
}
// ── Download: one request per item, skip failures ──
progress.phase = 'download';
progress.total = urls.length;
emit(true);
for (const u of urls) {
checkAbort();
progress.current = u;
emit(true);
try {
await reclipDownloadOne({ url: u, destDir: absDir, audioOnly, signal: params.abortSignal });
progress.done++;
} catch {
checkAbort(); // an abort surfaces as a throw here — re-check so it stops instead of counting as a skip
progress.failed++;
}
emit();
}
progress.phase = 'done';
progress.current = undefined;
emit(true);
}