feat(downloads): estimate a transcode's size so the progress bar moves
A transcode is produced as it is sent — chunked, with no Content-Length — and the worker reported progress 0.0 for its whole duration: an empty bar reading "0%" while the byte count climbed for an hour. That is the case every film whose audio must be re-encoded lands in. The backend already fetches the item to decide the audio policy, and that item carries what a prediction needs: the source's size (an `original` download copies the picture, so the output is the source give or take the audio track) and its runtime (a preset re-encodes at fixed rates, so the size is rate × runtime — from a preset table the URL builder now shares, so the two cannot drift). The prediction is made where the URL is resolved and persisted as the row's file_size. The worker uses it only when the response has no length; the server's figure always wins; an estimated bar is capped at 99% so a low prediction never shows a finished download still running; and the Completed event now carries the bytes actually written so the frontend stops persisting the row's file_size as the final size. The row renders three honest states: exact "42%", estimated "~42%" with "X / ~Y", or — with no total at all — an indeterminate band and the bytes so far, never "0%". The single-video button joins the series/season buttons on the enqueue path so all three resolve, and predict, in one place. DR-290, UT-252, UT-253, UT-254. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* What the progress bar on a download row should say.
|
||||
*
|
||||
* Extracted from `DownloadItem.svelte` so the three cases can be unit-tested:
|
||||
* an exact total (the server sent `Content-Length`), an estimated one (a
|
||||
* transcode sends none, so the backend predicted it from the source), and no
|
||||
* total at all — which used to render as an empty bar reading "0%" for the
|
||||
* whole of a transcode download. Progress with no denominator is not 0%; it
|
||||
* is unknown, and the bar says so.
|
||||
*
|
||||
* TRACES: UR-071 | DR-290 | UT-254
|
||||
*/
|
||||
|
||||
export interface ProgressSource {
|
||||
status: "pending" | "downloading" | "completed" | "failed" | "paused";
|
||||
/** 0..1 as the backend reports it; already capped short of 1 when estimated. */
|
||||
progress: number;
|
||||
bytesDownloaded: number;
|
||||
fileSize?: number;
|
||||
/** `fileSize` is the backend's prediction, not the server's word. */
|
||||
fileSizeEstimated?: boolean;
|
||||
}
|
||||
|
||||
export type ProgressKind = "exact" | "estimated" | "indeterminate";
|
||||
|
||||
export interface ProgressView {
|
||||
kind: ProgressKind;
|
||||
/** Whole percent for the bar width; `null` when there is nothing to measure against. */
|
||||
percent: number | null;
|
||||
/** "42%" / "~42%"; empty when indeterminate. */
|
||||
percentLabel: string;
|
||||
/** "512 B / 1.0 KB", "420 B / ~1000 B", or just the bytes so far. */
|
||||
label: string;
|
||||
}
|
||||
|
||||
export function formatBytes(bytes: number): string {
|
||||
if (bytes === 0) return "0 B";
|
||||
const k = 1024;
|
||||
const sizes = ["B", "KB", "MB", "GB"];
|
||||
const i = Math.min(Math.floor(Math.log(bytes) / Math.log(k)), sizes.length - 1);
|
||||
return `${(bytes / Math.pow(k, i)).toFixed(i === 0 ? 0 : 1)} ${sizes[i]}`;
|
||||
}
|
||||
|
||||
export function describeProgress(d: ProgressSource): ProgressView {
|
||||
const downloaded = formatBytes(d.bytesDownloaded);
|
||||
if (!d.fileSize) {
|
||||
return { kind: "indeterminate", percent: null, percentLabel: "", label: downloaded };
|
||||
}
|
||||
const percent = Math.round(Math.min(Math.max(d.progress, 0), 1) * 100);
|
||||
if (d.fileSizeEstimated) {
|
||||
return {
|
||||
kind: "estimated",
|
||||
percent,
|
||||
percentLabel: `~${percent}%`,
|
||||
label: `${downloaded} / ~${formatBytes(d.fileSize)}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
kind: "exact",
|
||||
percent,
|
||||
percentLabel: `${percent}%`,
|
||||
label: `${downloaded} / ${formatBytes(d.fileSize)}`,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user