feat: download ETA on the press bar from measured Soulseek throughput

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>
This commit is contained in:
Jonathan
2026-07-20 20:16:58 +02:00
parent db8b43b0e8
commit c49cee9a20
9 changed files with 113 additions and 24 deletions
+6 -8
View File
@@ -1,7 +1,7 @@
"use client";
import { useEffect, useState } from "react";
import { describeJob, timeAgo } from "./_ui/status";
import { describeJob, etaLabel, timeAgo } from "./_ui/status";
import { StatTiles } from "./_ui/stat-tiles";
import { SectionHeader } from "./_ui/section-header";
import { JobRow } from "./_ui/job-row";
@@ -46,6 +46,7 @@ type Row = {
candidateCount: number;
chosen: { source: string; format: string; trackCount: number } | null;
downloadProgress: number;
downloadEtaSeconds: number | null;
paused: boolean;
} | null;
};
@@ -62,6 +63,7 @@ function jobOf(r: Row) {
candidateCount: 0,
chosen: null,
downloadProgress: 0,
downloadEtaSeconds: null,
paused: false,
}
);
@@ -270,13 +272,9 @@ export function Queue() {
} else {
const total = j.chosen?.trackCount ?? 0;
const done = total ? Math.round((pct / 100) * total) : 0;
bar = (
<ProgressBar
kind="working"
percent={pct}
caption={total ? `${pct}% · ${done}/${total} tracks` : `${pct}%`}
/>
);
const base = total ? `${pct}% · ${done}/${total} tracks` : `${pct}%`;
const eta = etaLabel(j.downloadEtaSeconds);
bar = <ProgressBar kind="working" percent={pct} caption={eta ? `${base} · ${eta}` : base} />;
}
} else if (j.state === "tagging") {
bar = <ProgressBar kind="verify" indeterminate caption="finishing" />;