c49cee9a20
Show a live "~Xm left" countdown on the download progress bar. The slskd download loop already polls byte transfer every 3s; measure the actual throughput (EWMA- smoothed bytes/sec), compute seconds-remaining from the album's total bytes, and surface it. New nullable Job.downloadEtaSeconds (migration; web entrypoint runs prisma migrate deploy), threaded through the on_progress callback (optional 2nd arg, so other adapters/callers are unaffected — they report no ETA). API exposes it; queue.tsx renders etaLabel() after the track count. Null when unknown (no sample yet or a source that doesn't report bytes). Worker 330 tests, web 214 tests, both green; verified live-rendered as "42% · 6/14 tracks · ~4m left". Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
49 lines
2.0 KiB
TypeScript
49 lines
2.0 KiB
TypeScript
export type Kind = "idle" | "working" | "verify" | "done" | "attention";
|
|
|
|
// Worker Job.state -> a literal, legible label + severity kind, collapsed to three
|
|
// honest phases: Searching -> Downloading -> Finishing. The record-label metaphor
|
|
// stays in the chrome; these labels never obscure what's happening.
|
|
const STATE_MAP: Record<string, { label: string; kind: Kind }> = {
|
|
requested: { label: "Queued", kind: "idle" },
|
|
matching: { label: "Searching", kind: "working" },
|
|
matched: { label: "Searching", kind: "working" },
|
|
downloading: { label: "Downloading", kind: "working" },
|
|
tagging: { label: "Finishing", kind: "verify" },
|
|
imported: { label: "Pressed", kind: "done" },
|
|
needs_attention: { label: "Needs attention", kind: "attention" },
|
|
};
|
|
|
|
/** Compact elapsed label from an ISO/Date string to `now` (ms since epoch). */
|
|
export function timeAgo(value: string | Date | null | undefined, now: number): string {
|
|
if (!value) return "";
|
|
const t = new Date(value).getTime();
|
|
if (Number.isNaN(t)) return "";
|
|
const s = Math.max(0, Math.floor((now - t) / 1000));
|
|
if (s < 60) return "just now";
|
|
const m = Math.floor(s / 60);
|
|
if (m < 60) return `${m}m`;
|
|
const h = Math.floor(m / 60);
|
|
if (h < 24) return `${h}h`;
|
|
return `${Math.floor(h / 24)}d`;
|
|
}
|
|
|
|
/** Compact download ETA like "~4m left", "~45s left", "~1h 5m left". Null/≤0 → no label. */
|
|
export function etaLabel(seconds: number | null | undefined): string | null {
|
|
if (seconds == null || seconds <= 0) return null;
|
|
if (seconds < 60) return `~${seconds}s left`;
|
|
const m = Math.round(seconds / 60);
|
|
if (m < 60) return `~${m}m left`;
|
|
const h = Math.floor(m / 60);
|
|
return `~${h}h ${m % 60}m left`;
|
|
}
|
|
|
|
export function describeJob(state: string, stage: string): { label: string; kind: Kind } {
|
|
void stage; // stage no longer drives the label; kept for call-site compatibility
|
|
return (
|
|
STATE_MAP[state] ?? {
|
|
label: state ? state[0].toUpperCase() + state.slice(1) : "Unknown",
|
|
kind: "idle",
|
|
}
|
|
);
|
|
}
|