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:
@@ -159,29 +159,35 @@ def _set_state(conn: psycopg.Connection, job_id: str, state: str, stage: str) ->
|
||||
conn.commit()
|
||||
|
||||
|
||||
def _set_download_progress(conn: psycopg.Connection, job_id: str, frac: float) -> None:
|
||||
def _set_download_progress(
|
||||
conn: psycopg.Connection, job_id: str, frac: float, eta_seconds: int | None = None
|
||||
) -> None:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute('UPDATE "Job" SET "downloadProgress" = %s WHERE id = %s', (frac, job_id))
|
||||
cur.execute(
|
||||
'UPDATE "Job" SET "downloadProgress" = %s, "downloadEtaSeconds" = %s WHERE id = %s',
|
||||
(frac, eta_seconds, job_id),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
|
||||
def _make_on_progress(conn: psycopg.Connection, job_id: str, reports: "threading.Event"):
|
||||
"""Build the on_progress(pct) callback threaded into adapter.download. It marks the
|
||||
adapter as reporting real byte-level progress (so the file-count poller yields) and
|
||||
throttle-writes Job.downloadProgress. Called synchronously from adapter.download on the
|
||||
pipeline's own thread, so it safely reuses `conn`."""
|
||||
"""Build the on_progress(pct, eta_seconds=None) callback threaded into adapter.download. It
|
||||
marks the adapter as reporting real byte-level progress (so the file-count poller yields) and
|
||||
throttle-writes Job.downloadProgress (+ the download ETA when the adapter measures one — slskd
|
||||
does; others pass None). Called synchronously from adapter.download on the pipeline's own
|
||||
thread, so it safely reuses `conn`."""
|
||||
state = {"frac": -1.0, "t": 0.0}
|
||||
|
||||
def on_progress(pct: float) -> None:
|
||||
def on_progress(pct: float, eta_seconds: int | None = None) -> None:
|
||||
reports.set()
|
||||
frac = 0.0 if pct < 0 else 1.0 if pct > 1 else float(pct)
|
||||
now = time.monotonic()
|
||||
# throttle DB writes: on a >=1% move or every 0.5s, not once per received byte
|
||||
if frac - state["frac"] >= 0.01 or (now - state["t"]) >= 0.5:
|
||||
# throttle DB writes: on a >=1% move, a new ETA, or every 0.5s — not once per received byte
|
||||
if frac - state["frac"] >= 0.01 or eta_seconds is not None or (now - state["t"]) >= 0.5:
|
||||
state["frac"] = frac
|
||||
state["t"] = now
|
||||
try:
|
||||
_set_download_progress(conn, job_id, frac)
|
||||
_set_download_progress(conn, job_id, frac, eta_seconds)
|
||||
except Exception as e: # a progress write must never fail the download
|
||||
print(f"pipeline: on_progress write failed: {e}", flush=True)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user