replace hardcoded download job with download-media script capability

move video/audio downloads off the dedicated ReClip download lane onto the
generic script-job path:

- delete execute-download.ts, reclip-client.ts and the POST /jobs/download
  endpoint; drop the 'download' mode from the pipeline_jobs enum (legacy rows
  tolerated)
- execute-script.ts: strip the @@officer:progress@@ sentinel from the log,
  emit progress events, and isolate viewer/log writes (safeEmit/safeLog) so a
  broadcast or log throw can't wedge the stdout pump
- pipeline-job-manager.ts: persist latest progress; guard sendToViewer sends
- ScriptJobDetail: render the two progress bars; DownloadJobDetail kept for
  legacy history rows
- TaskRunnerModal: ScriptRunner descends into the triggered directory
- VideoDownloadPanel: rewire startJob to the script-job path

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-29 01:10:32 +00:00
co-authored by Claude Opus 4.8
parent b523c7d408
commit b2fb6f148c
10 changed files with 212 additions and 428 deletions
@@ -5,7 +5,6 @@ import { getPipelineJobsForUser, getPipelineJob } from 'officerdb';
import { getTaskByDirName } from './task-files';
import * as jobManager from './pipeline-job-manager';
import { jobLogPath } from './execute-script';
import { getRootDir, resolveUserPath } from '../file-browser/router';
export const pipelineJobsRouter = createRouter();
@@ -71,44 +70,6 @@ pipelineJobsRouter.post('/', async (c) => {
return c.json({ jobId, status });
});
// POST /download — enqueue a video/audio download job (ReClip). Runs in its own lane, needs no capability
// 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;
urls?: string[];
format?: 'audio' | 'video';
dir?: string;
root?: string;
label?: string;
}>();
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,
email: user.email,
username: user.username ?? '',
mode: 'download',
taskDirName: 'video-download',
taskName: body.label || (format === 'audio' ? 'Audio download' : 'Video download'),
inputs,
cwd: body.dir ?? '/',
config: {},
},
'queue',
);
return c.json({ jobId, status });
});
// GET /counts — header-badge summary { running, runningJobId, queued }. Before /:id so it isn't
// captured as an id.
pipelineJobsRouter.get('/counts', async (c) => {