download job: use the panel's exact url list (no server re-expansion)

A YouTube Mix/radio playlist (list=RD…) returns a different set of items on every
/api/playlist call (observed 779 / 1485 / 529 for the same URL). The job used to
re-expand the playlist server-side, so it would download a different list than the
count shown on the decision screen.

The panel now passes the already-expanded `urls[]` into the job, and the executor
uses them verbatim (falling back to expanding `url` only when no list is given).
The job downloads exactly what you decided on. Endpoint takes `urls[]` (stored as
inputs.urls) or `url`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-28 18:44:45 +00:00
co-authored by Claude Opus 4.8
parent c55ec5884b
commit 7d149bd4d1
3 changed files with 34 additions and 19 deletions
+10 -5
View File
@@ -72,21 +72,26 @@ pipelineJobsRouter.post('/', async (c) => {
});
// POST /download — enqueue a video/audio download job (ReClip). Runs in its own lane, needs no capability
// task. Body: { url (video or playlist), format:'audio'|'video', dir (target folder, home-relative),
// root?, label? }. The job expands + fetches metadata (phase 1) then downloads survivors (phase 2).
// task. Body: { urls[] (the exact list to download — preferred), OR url (expanded server-side),
// format:'audio'|'video', dir (target folder, home-relative), root?, label? }. Passing `urls` locks the
// job to the list the panel already expanded, so a Mix/radio playlist can't drift between fetch + job.
pipelineJobsRouter.post('/download', async (c) => {
const user = c.get('user');
const body = await c.req.json<{
url: string;
url?: string;
urls?: string[];
format?: 'audio' | 'video';
dir?: string;
root?: string;
label?: string;
}>();
if (!body.url) throw errors.BAD_REQUEST('url is required');
if (!body.url && !body.urls?.length) throw errors.BAD_REQUEST('url or urls is required');
const rootDir = getRootDir(user, body.root);
const absDir = resolveUserPath(rootDir, body.dir ?? '/'); // traversal-guarded
const format = body.format === 'video' ? 'video' : 'audio';
const inputs: Record<string, string> = body.urls?.length
? { urls: JSON.stringify(body.urls), format, absDir }
: { url: body.url!, format, absDir };
const { jobId, status } = await jobManager.enqueueJob(
{
userId: user.id,
@@ -95,7 +100,7 @@ pipelineJobsRouter.post('/download', async (c) => {
mode: 'download',
taskDirName: 'video-download',
taskName: body.label || (format === 'audio' ? 'Audio download' : 'Video download'),
inputs: { url: body.url, format, absDir },
inputs,
cwd: body.dir ?? '/',
config: {},
},