diff --git a/src/apps/officer-web/Screens/Dashboard/Jobs/DownloadJobDetail.tsx b/src/apps/officer-web/Screens/Dashboard/Jobs/DownloadJobDetail.tsx index eb8eaf50..4ac074f2 100644 --- a/src/apps/officer-web/Screens/Dashboard/Jobs/DownloadJobDetail.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Jobs/DownloadJobDetail.tsx @@ -7,11 +7,11 @@ import { toast } from 'sonner'; // Detail view for a `download` job — a compact two-phase progress readout (metadata → download), polled // from the job's persisted progress (the executor emits counter snapshots, not per-item events). -type Counts = { done: number; failed: number; total: number }; type DownloadProgress = { - phase: 'expanding' | 'metadata' | 'download' | 'done'; - meta: Counts; - dl: Counts; + phase: 'expanding' | 'download' | 'done'; + done: number; + failed: number; + total: number; current?: string; }; type DownloadJob = { @@ -54,46 +54,24 @@ const statusBadge = (status: string) => { } }; -// A labelled progress bar: fill = processed/total; the caption states saved vs skipped/failed. -const PhaseBar = ({ - label, - c, - active, - savedLabel, - failedLabel, -}: { - label: string; - c: Counts; - active: boolean; - savedLabel: string; - failedLabel: string; -}) => { - const processed = c.done + c.failed; - const pct = c.total ? (processed / c.total) * 100 : 0; +// The download progress bar: fill = processed/total; caption states saved vs skipped. +const DownloadBar = ({ p }: { p: DownloadProgress }) => { + const processed = p.done + p.failed; + const pct = p.total ? (processed / p.total) * 100 : 0; return (
- - {label} - + Downloaded - {processed}/{c.total || '—'} + {processed}/{p.total || '—'}
-
+
- - {c.done} {savedLabel} - - {c.failed > 0 && ( - - {c.failed} {failedLabel} - - )} + {p.done} saved + {p.failed > 0 && {p.failed} skipped}
); @@ -161,13 +139,7 @@ export const DownloadJobDetail = () => { const p = job.progress; const isAudio = job.inputs?.format !== 'video'; const phaseLabel = - !p || p.phase === 'expanding' - ? 'Preparing…' - : p.phase === 'metadata' - ? 'Fetching metadata' - : p.phase === 'download' - ? 'Downloading' - : 'Done'; + !p || p.phase === 'expanding' ? 'Expanding playlist…' : p.phase === 'download' ? 'Downloading' : 'Done'; return (
@@ -205,20 +177,7 @@ export const DownloadJobDetail = () => {
{phaseLabel}
- - + {running && p?.phase === 'download' && p.current && (
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}