diff --git a/src/servers/api/tasks/execute-download.ts b/src/servers/api/tasks/execute-download.ts
index 1381cc70..2e1b3cd2 100644
--- a/src/servers/api/tasks/execute-download.ts
+++ b/src/servers/api/tasks/execute-download.ts
@@ -1,19 +1,17 @@
-import { reclipInfo, reclipPlaylist, reclipDownloadOne, type ReclipInfo } from '../../reclip-client';
+import { reclipPlaylist, reclipDownloadOne } from '../../reclip-client';
-// The download-job executor — pure scripting, no agent. Two phases:
-// 1. metadata — expand the playlist, fetch each item's info sequentially, keep the ones that resolve
-// (skip the errors: private/deleted/unavailable).
-// 2. download — download every survivor in the chosen format (audio/video); skip anything that fails.
-// 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) so the job manager marks it
-// stopped/failed; per-item errors are counted and skipped, never fatal.
+// 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.
-type Counts = { done: number; failed: number; total: number };
export type DownloadProgress = {
- phase: 'expanding' | 'metadata' | 'download' | 'done';
- meta: Counts;
- dl: Counts;
- current?: string; // title of the item currently downloading
+ 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 };
@@ -36,20 +34,13 @@ export async function executeDownload(params: ExecuteDownloadParams): Promise
{
const now = Date.now();
if (!force && now - lastEmit < EMIT_THROTTLE_MS) return;
lastEmit = now;
- params.emit({
- type: 'download:progress',
- progress: { ...progress, meta: { ...progress.meta }, dl: { ...progress.dl } },
- });
+ params.emit({ type: 'download:progress', progress: { ...progress } });
};
const checkAbort = () => {
if (params.abortSignal.aborted) throw new Error('aborted');
@@ -65,44 +56,20 @@ export async function executeDownload(params: ExecuteDownloadParams): Promise = [];
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 ?? '' });
- progress.meta.done++; // done = kept; failed = skipped (both phases read `(done+failed)/total`)
- } else {
- progress.meta.failed++;
- }
- emit();
- }
- emit(true);
-
- // ── Phase 2: download survivors ──
- progress.phase = 'download';
- progress.dl.total = valid.length;
- emit(true);
- for (const item of valid) {
- checkAbort();
- progress.current = item.title || item.url;
+ progress.current = u;
emit(true);
try {
- await reclipDownloadOne({
- url: item.url,
- destDir: absDir,
- audioOnly,
- title: item.title,
- signal: params.abortSignal,
- });
- progress.dl.done++;
+ 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.dl.failed++;
+ progress.failed++;
}
emit();
}
diff --git a/src/servers/reclip-client.ts b/src/servers/reclip-client.ts
index a3e61f75..213a5394 100644
--- a/src/servers/reclip-client.ts
+++ b/src/servers/reclip-client.ts
@@ -45,7 +45,7 @@ type DownloadOpts = {
url: string;
destDir: string; // absolute directory to write the finished file into
audioOnly: boolean;
- title?: string; // for a title-based filename (ReClip names by job id otherwise)
+ title?: string; // optional override; omit and ReClip names the file from the video title itself
onPhase?: (phase: 'transferring') => void;
signal?: { aborted: boolean };
};
@@ -53,22 +53,17 @@ type DownloadOpts = {
/**
* Download ONE video/audio via ReClip and stream the finished file into `destDir`. Resolves with the
* saved filename; throws on any failure (unreachable / rejected / job error / timeout / abort). Respects
- * a cooperative `signal.aborted` between polls and while streaming.
+ * a cooperative `signal.aborted` between polls and while streaming. No metadata prefetch — ReClip derives
+ * the filename from the video title — so this is a single request per item.
*/
export async function reclipDownloadOne(opts: DownloadOpts): Promise {
const { url, destDir, audioOnly, signal } = opts;
const aborted = () => signal?.aborted === true;
- let title = opts.title ?? '';
- if (!title) {
- const info = await reclipInfo(url).catch(() => null);
- title = info?.title ?? '';
- }
-
const dlRes = await fetch(`${RECLIP_BASE}/api/download`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ url, format: audioOnly ? 'audio' : 'video', title }),
+ body: JSON.stringify({ url, format: audioOnly ? 'audio' : 'video', title: opts.title ?? '' }),
signal: AbortSignal.timeout(30_000),
}).catch(() => {
throw new Error(`Could not reach ReClip at ${RECLIP_BASE}`);
diff --git a/src/workspaces/officerdev/src/apps/FileBrowser/VideoDownloadPanel.tsx b/src/workspaces/officerdev/src/apps/FileBrowser/VideoDownloadPanel.tsx
index 2d2d228e..b8dfd66d 100644
--- a/src/workspaces/officerdev/src/apps/FileBrowser/VideoDownloadPanel.tsx
+++ b/src/workspaces/officerdev/src/apps/FileBrowser/VideoDownloadPanel.tsx
@@ -17,12 +17,12 @@ import {
} from 'lucide-react';
import { useFilesAPI, type VideoInfo } from '../../hooks/useFilesAPI';
-// Compact progress shape mirrored from the download-job executor (done = successes, failed = skipped).
-type JobCounts = { done: number; failed: number; total: number };
+// Compact progress shape mirrored from the download-job executor (done = saved, failed = skipped).
type JobProgress = {
- phase: 'expanding' | 'metadata' | 'download' | 'done';
- meta: JobCounts;
- dl: JobCounts;
+ phase: 'expanding' | 'download' | 'done';
+ done: number;
+ failed: number;
+ total: number;
current?: string;
};
@@ -275,8 +275,8 @@ export const VideoDownloadPanel = () => {
if (cancelled || !j) return;
setJobStatus(j.status);
setJobProg(j.progress);
- if (j.progress && j.progress.dl.done > lastDlDone.current) {
- lastDlDone.current = j.progress.dl.done;
+ if (j.progress && j.progress.done > lastDlDone.current) {
+ lastDlDone.current = j.progress.done;
setRefreshSignal((n) => n + 1);
}
if (['completed', 'failed', 'stopped', 'interrupted'].includes(j.status)) clearInterval(timer);
@@ -555,8 +555,6 @@ export const VideoDownloadPanel = () => {
// ── Job progress view ──
-const jobPct = (c: JobCounts) => (c.total ? ((c.done + c.failed) / c.total) * 100 : 0);
-
const JobView = ({
prog,
status,
@@ -572,39 +570,11 @@ const JobView = ({
}) => {
const terminal = ['completed', 'failed', 'stopped', 'interrupted'].includes(status);
const phaseLabel =
- !prog || prog.phase === 'expanding'
- ? 'Preparing…'
- : prog.phase === 'metadata'
- ? 'Fetching metadata'
- : prog.phase === 'download'
- ? 'Downloading'
- : 'Done';
- const Bar = ({ label, c, saved, failed }: { label: string; c: JobCounts; saved: string; failed: string }) => (
-
-
- {label}
-
- {c.done + c.failed}/{c.total || '—'}
-
-
-
-
-
- {c.done} {saved}
-
- {c.failed > 0 && (
-
- {c.failed} {failed}
-
- )}
-
-
- );
+ !prog || prog.phase === 'expanding' ? 'Expanding playlist…' : prog.phase === 'download' ? 'Downloading' : 'Done';
+ const done = prog?.done ?? 0;
+ const failed = prog?.failed ?? 0;
+ const total = prog?.total ?? 0;
+ const pct = total ? ((done + failed) / total) * 100 : 0;
return (
@@ -622,8 +592,21 @@ const JobView = ({
{audio ? 'Audio' : 'Video'}
-
-
+
+
+ Downloaded
+
+ {done + failed}/{total || '—'}
+
+
+
+
+ {done} saved
+ {failed > 0 && {failed} skipped}
+
+
{!terminal && prog?.phase === 'download' && prog.current && (
{prog.current}