diff --git a/apps/web/src/components/download-phase-metrics.tsx b/apps/web/src/components/download-phase-metrics.tsx deleted file mode 100644 index c68b415..0000000 --- a/apps/web/src/components/download-phase-metrics.tsx +++ /dev/null @@ -1,42 +0,0 @@ -type Props = { - tokenFetchMs: number | null; - ytdlpMs: number | null; - uploadMs: number | null; - totalMs: number | null; -}; - -type MetricRow = { - key: string; - label: string; - value: number; -}; - -function formatMs(value: number): string { - if (value >= 1000) return `${(value / 1000).toFixed(value >= 10_000 ? 1 : 2)}s`; - return `${Math.round(value)}ms`; -} - -function collectMetrics({ tokenFetchMs, ytdlpMs, uploadMs, totalMs }: Props): MetricRow[] { - const rows: MetricRow[] = []; - if (typeof tokenFetchMs === "number") - rows.push({ key: "token", label: "Token", value: tokenFetchMs }); - if (typeof ytdlpMs === "number") rows.push({ key: "fetch", label: "Fetch", value: ytdlpMs }); - if (typeof uploadMs === "number") rows.push({ key: "upload", label: "Upload", value: uploadMs }); - if (typeof totalMs === "number") rows.push({ key: "total", label: "Total", value: totalMs }); - return rows; -} - -export function DownloadPhaseMetrics({ tokenFetchMs, ytdlpMs, uploadMs, totalMs }: Props) { - const rows = collectMetrics({ tokenFetchMs, ytdlpMs, uploadMs, totalMs }); - if (rows.length === 0) return null; - return ( -
{row.label}
-{formatMs(row.value)}
-
- {label}
++ {message} +
+Selected: {resolvedLabel}
- )} - {visibleError && ( -- {visibleError} -
- )} - {!visibleError && ( -Selected: {resolvedLabel}
)} - > + ); } diff --git a/apps/web/src/hooks/use-downloader-job.ts b/apps/web/src/hooks/use-downloader-job.ts index 8663a26..6fae2bf 100644 --- a/apps/web/src/hooks/use-downloader-job.ts +++ b/apps/web/src/hooks/use-downloader-job.ts @@ -11,6 +11,7 @@ import type { DownloaderCreateJobRequest, DownloaderJobResponse, DownloaderJobStage, + DownloaderJobStatus, } from "../types/downloader"; const POLL_MS = 1_500; @@ -34,7 +35,7 @@ export function useDownloaderJob() { }); }, [jobId]); - const status = useQuery({ + const query = useQuery({ queryKey: ["downloader-job", jobId], enabled: typeof jobId === "string" && jobId.length > 0, queryFn: () => fetchDownloaderJob(jobId ?? ""), @@ -51,34 +52,35 @@ export function useDownloaderJob() { }, }); const job = useMemo(() => { - if (!eventJob) return status.data; - if (!status.data || status.data.id !== eventJob.id) return eventJob; - if (status.data.status === "done" || status.data.status === "failed") { + if (!eventJob) return query.data; + if (!query.data || query.data.id !== eventJob.id) return eventJob; + if (query.data.status === "done" || query.data.status === "failed") { return { ...eventJob, - ...status.data, - resolved: status.data.resolved ?? eventJob.resolved, - error: status.data.error ?? eventJob.error, - errorCode: status.data.errorCode ?? eventJob.errorCode, - tokenFetchMs: status.data.tokenFetchMs ?? eventJob.tokenFetchMs, - ytdlpMs: status.data.ytdlpMs ?? eventJob.ytdlpMs, - uploadMs: status.data.uploadMs ?? eventJob.uploadMs, - totalMs: status.data.totalMs ?? eventJob.totalMs, + ...query.data, + resolved: query.data.resolved ?? eventJob.resolved, + error: query.data.error ?? eventJob.error, + errorCode: query.data.errorCode ?? eventJob.errorCode, + tokenFetchMs: query.data.tokenFetchMs ?? eventJob.tokenFetchMs, + ytdlpMs: query.data.ytdlpMs ?? eventJob.ytdlpMs, + uploadMs: query.data.uploadMs ?? eventJob.uploadMs, + totalMs: query.data.totalMs ?? eventJob.totalMs, }; } return { - ...status.data, + ...query.data, ...eventJob, - resolved: eventJob.resolved ?? status.data.resolved, - error: eventJob.error ?? status.data.error, - errorCode: eventJob.errorCode ?? status.data.errorCode, + resolved: eventJob.resolved ?? query.data.resolved, + error: eventJob.error ?? query.data.error, + errorCode: eventJob.errorCode ?? query.data.errorCode, }; - }, [eventJob, status.data]); + }, [eventJob, query.data]); - const isQueued = create.isPending || job?.status === "queued"; - const isRunning = job?.status === "running"; - const isDone = job?.status === "done"; - const isFailed = job?.status === "failed"; + const status: DownloaderJobStatus | null = create.isPending ? "queued" : (job?.status ?? null); + const isQueued = status === "queued"; + const isRunning = status === "running"; + const isDone = status === "done"; + const isFailed = status === "failed"; const stage: DownloaderJobStage | null = job?.stage ?? null; const progressPercent = typeof job?.progressPercent === "number" ? job.progressPercent : null; const resolved = job?.resolved ?? null; @@ -90,11 +92,12 @@ export function useDownloaderJob() { const errorText = create.error instanceof Error ? create.error.message - : job?.error || (status.error instanceof Error ? status.error.message : null); + : job?.error || (query.error instanceof Error ? query.error.message : null); function start(payload: DownloaderCreateJobRequest) { setEventJob(null); setSseUnavailable(false); + create.reset(); create.mutate(payload); } @@ -114,6 +117,7 @@ export function useDownloaderJob() { openArtifact, reset, jobId, + status, stage, progressPercent, resolved, diff --git a/apps/web/src/lib/downloader-display.ts b/apps/web/src/lib/downloader-display.ts new file mode 100644 index 0000000..ca5d48a --- /dev/null +++ b/apps/web/src/lib/downloader-display.ts @@ -0,0 +1,89 @@ +import type { DownloaderJobStage, DownloaderJobStatus } from "../types/downloader"; + +export const DOWNLOADER_STEPS = ["Prepare", "Download", "Finalize"]; + +export function isCancelledDownloaderJob( + status: DownloaderJobStatus | null, + stage: DownloaderJobStage | null, + errorCode: string | null, +): boolean { + return (status === "failed" && errorCode === "cancelled") || stage === "cancelled"; +} + +export function isFailedDownloaderJob( + status: DownloaderJobStatus | null, + stage: DownloaderJobStage | null, + errorCode: string | null, +): boolean { + return ( + (status === "failed" || stage === "failed") && + !isCancelledDownloaderJob(status, stage, errorCode) + ); +} + +export function downloaderStageIndex( + status: DownloaderJobStatus | null, + stage: DownloaderJobStage | null, +): number { + if (status === "done" || stage === "done") return 3; + if (stage === "download" || stage === "downloading") return 1; + if (stage === "mux" || stage === "finalizing") return 2; + if (status === "running" || stage === "extract" || stage === "running") return 0; + return -1; +} + +export function downloaderStatusLabel( + status: DownloaderJobStatus | null, + stage: DownloaderJobStage | null, + errorCode: string | null, + forceWaiting: boolean, +): string { + if (forceWaiting) return "Opening file"; + if (isCancelledDownloaderJob(status, stage, errorCode)) return "Cancelled"; + if (isFailedDownloaderJob(status, stage, errorCode)) return "Failed"; + if (stage === "cached") return "Ready from cache"; + if (status === "done" || stage === "done") return "Ready"; + if (status === "queued" || stage === "queued") return "Queued"; + if (stage === "download" || stage === "downloading") return "Downloading"; + if (stage === "mux" || stage === "finalizing") return "Finalizing"; + return "Preparing download"; +} + +export function downloaderStatusMessage( + status: DownloaderJobStatus | null, + stage: DownloaderJobStage | null, + errorCode: string | null, + errorText: string | null, + forceWaiting: boolean, +): string { + if (forceWaiting) return "Handing the file to your browser."; + if (isCancelledDownloaderJob(status, stage, errorCode)) return "The download was cancelled."; + if (isFailedDownloaderJob(status, stage, errorCode)) + return errorText ?? "The download could not be completed."; + if (stage === "cached") return "Using the cached file."; + if (status === "done" || stage === "done") return "Your file is ready."; + if (status === "queued" || stage === "queued") return "Waiting for an available worker."; + if (stage === "download" || stage === "downloading") return "Downloading the selected media."; + if (stage === "mux" || stage === "finalizing") return "Combining the final file."; + return "Preparing streams and selecting the format."; +} + +export function downloaderProgressValue( + status: DownloaderJobStatus | null, + stage: DownloaderJobStage | null, + progressPercent: number | null, + forceWaiting: boolean, +): number { + if (forceWaiting || status === "done" || stage === "done") return 100; + if (typeof progressPercent === "number") return Math.min(100, Math.max(0, progressPercent)); + if (status === "running") return 12; + if (status === "queued") return 4; + return 0; +} + +export function shouldShowDownloaderProgress( + status: DownloaderJobStatus | null, + forceWaiting: boolean, +): boolean { + return forceWaiting || status === "queued" || status === "running" || status === "done"; +} diff --git a/apps/web/src/lib/downloader-events.ts b/apps/web/src/lib/downloader-events.ts index 1c17211..85c5b68 100644 --- a/apps/web/src/lib/downloader-events.ts +++ b/apps/web/src/lib/downloader-events.ts @@ -12,8 +12,11 @@ function toStringOrNull(value: unknown): string | null { function toStage(value: unknown): DownloaderJobResponse["stage"] { if (value === "queued") return "queued"; + if (value === "extract") return "extract"; if (value === "running") return "running"; + if (value === "download") return "download"; if (value === "downloading") return "downloading"; + if (value === "mux") return "mux"; if (value === "finalizing") return "finalizing"; if (value === "done") return "done"; if (value === "cached") return "cached"; @@ -94,6 +97,7 @@ export function subscribeDownloaderEvents( } }; eventSource.addEventListener("progress", onProgress); + eventSource.addEventListener("message", onProgress); eventSource.onerror = () => { if (closed) return; handlers.onError(); @@ -103,6 +107,7 @@ export function subscribeDownloaderEvents( return () => { closed = true; eventSource.removeEventListener("progress", onProgress); + eventSource.removeEventListener("message", onProgress); eventSource.close(); }; } diff --git a/apps/web/src/types/downloader.ts b/apps/web/src/types/downloader.ts index f24f2af..f774bd6 100644 --- a/apps/web/src/types/downloader.ts +++ b/apps/web/src/types/downloader.ts @@ -1,11 +1,14 @@ export type DownloaderMode = "video" | "audio"; -type DownloaderJobStatus = "queued" | "running" | "done" | "failed"; +export type DownloaderJobStatus = "queued" | "running" | "done" | "failed"; export type DownloaderJobStage = | "queued" + | "extract" | "running" + | "download" | "downloading" + | "mux" | "finalizing" | "done" | "cached"