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>
This commit is contained in:
2026-07-28 18:35:10 +00:00
co-authored by Claude Opus 4.8
parent bc1b799a27
commit c55ec5884b
4 changed files with 66 additions and 162 deletions
@@ -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 }) => (
<div className="flex flex-col gap-1">
<div className="flex justify-between text-xs">
<span className="font-medium text-duck-dark">{label}</span>
<span className="tabular-nums text-duck-dark/50">
{c.done + c.failed}/{c.total || '—'}
</span>
</div>
<div className="h-2 overflow-hidden rounded-full bg-duck-dark/10">
<div
className="h-full rounded-full bg-duck-teal transition-all duration-300"
style={{ width: `${jobPct(c)}%` }}
/>
</div>
<div className="flex gap-3 text-[10px] text-duck-dark/50">
<span className="text-duck-teal">
{c.done} {saved}
</span>
{c.failed > 0 && (
<span className="text-red-500/80">
{c.failed} {failed}
</span>
)}
</div>
</div>
);
!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 (
<div className="flex flex-col gap-4">
<div className="flex items-center gap-2 text-sm">
@@ -622,8 +592,21 @@ const JobView = ({
</span>
<span className="ml-auto text-xs text-duck-dark/40">{audio ? 'Audio' : 'Video'}</span>
</div>
<Bar label="Metadata" c={prog?.meta ?? { done: 0, failed: 0, total: 0 }} saved="found" failed="skipped" />
<Bar label="Download" c={prog?.dl ?? { done: 0, failed: 0, total: 0 }} saved="saved" failed="failed" />
<div className="flex flex-col gap-1">
<div className="flex justify-between text-xs">
<span className="font-medium text-duck-dark">Downloaded</span>
<span className="tabular-nums text-duck-dark/50">
{done + failed}/{total || '—'}
</span>
</div>
<div className="h-2.5 overflow-hidden rounded-full bg-duck-dark/10">
<div className="h-full rounded-full bg-duck-teal transition-all duration-300" style={{ width: `${pct}%` }} />
</div>
<div className="flex gap-3 text-[10px] text-duck-dark/50">
<span className="text-duck-teal">{done} saved</span>
{failed > 0 && <span className="text-red-500/80">{failed} skipped</span>}
</div>
</div>
{!terminal && prog?.phase === 'download' && prog.current && (
<p className="truncate text-xs text-duck-dark/50" title={prog.current}>
{prog.current}