1 Commits

Author SHA1 Message Date
Jonathan 67efdedb51 WIP: slskd download resume (idempotent enqueue) — INCOMPLETE, do not deploy
Adds the _needs_enqueue() helper (a file is (re)enqueued only if it has no
transfer yet or its last one ended Errored/Cancelled/Rejected). This is step 1
of making SlskdClient._download idempotent so a retry/hard-kill resumes instead
of re-downloading the whole album from scratch.

REMAINING (continue here):
1. Add SlskdClient._existing_states(username) -> {filename: state} (GET
   /api/v0/transfers/downloads/{username}, best-effort, {} on error).
2. In _download(): before the POST, compute
     existing = self._existing_states(username)
     to_enqueue = [f for f in files if _needs_enqueue(existing.get(f["filename"], ""))]
   POST only to_enqueue (skip the POST entirely when empty); poll loop unchanged.
   Effect: a completed prior copy is retrieved immediately; a partial one resumes.
3. Tests in test_slskd_cancel.py: resume-skips-completed (no POST, immediate
   retrieve) + resume-only-enqueues-missing (partial). Traced: the 3 existing
   download tests still pass (the extra pre-loop GET consumes poll0 and degrades
   gracefully) — but RUN the suite to confirm.
4. Merge to main, build+push worker image, deploy — but only AFTER the in-flight
   Drake "Scorpion" job (soulseekboy43) has imported, so the deploy's worker
   recreate doesn't interrupt it.

Deferred follow-up (bigger, needs candidate reconstruction): pipeline
peer-stickiness (reuse the persisted chosen candidate on retry instead of
re-searching) + cancel a reclaimed job's orphaned transfers at startup. That is
what fully stops the cross-peer duplicate the user saw.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 23:47:35 +02:00
+9
View File
@@ -33,6 +33,15 @@ def _max_polls(total_bytes: int) -> int:
return max(_MIN_XFER_POLLS, min(budget, _MAX_XFER_POLLS))
def _needs_enqueue(state: str) -> bool:
"""Whether a file must be (re)enqueued on slskd. True if it has no transfer yet or its last
transfer ended badly (Errored/Cancelled/Rejected); a file already Completed/InProgress/Queued is
left alone so a resumed download continues it instead of restarting from zero. Pure — unit-tested."""
if not state:
return True
return any(bad in state for bad in ("Errored", "Cancelled", "Rejected"))
def _basename(path: str) -> str:
return path.rsplit("\\", 1)[-1].rsplit("/", 1)[-1]