fix(worker): rank Soulseek peers by estimated transfer time, not raw speed
Peer selection ranked (free-slot, advertised-speed, short-queue), ignoring file size. But Soulseek FLAC rips of one album vary ~2x in size (a 24-bit/bloated rip vs a standard CD rip) and Lyra scores them the SAME quality class (the slskd search exposes no bit-depth, so every FLAC is class 2). So a peer advertising high speed but serving 40MB/track files would out-rank a smaller standard rip that actually finishes sooner — then blow past the 60-min per-peer backstop, time out, fall through, and restart at 0% (Linkin Park "Hybrid Theory" looping for hours). Rank by estimated transfer time instead: album bytes ÷ advertised speed, after the free-slot check. Prefers whichever peer FINISHES first — fast peers and/or smaller standard-edition rips — with no quality loss (all class 2). Equal-size case still reduces to speed order, so existing ranking behaviour is preserved. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -72,14 +72,18 @@ def _keep_matching(responses: list, needle: str) -> list:
|
|||||||
|
|
||||||
def _parse_search_responses(responses: list) -> list[dict]:
|
def _parse_search_responses(responses: list) -> list[dict]:
|
||||||
"""Turn slskd search responses into album candidates (one per peer+directory), ordered so the
|
"""Turn slskd search responses into album candidates (one per peer+directory), ordered so the
|
||||||
peers most likely to deliver quickly come first: a free upload slot, then higher upload speed,
|
peers that will FINISH first come first: a free upload slot, then the shortest estimated
|
||||||
then a shorter queue. The pipeline tries candidates in this order, so ranking fast/free peers
|
transfer time (album bytes ÷ the peer's advertised speed), then a shorter queue. Ranking on
|
||||||
first (over slow or queued ones) is what makes the Soulseek fall-through actually converge.
|
estimated time — not raw speed — matters because Soulseek FLAC rips of the same album vary
|
||||||
Pure — no I/O — so it is unit-tested offline."""
|
~2x in size (a 24-bit/bloated rip vs a standard CD rip), and Lyra scores them the SAME quality
|
||||||
|
class (the search exposes no bit-depth), so a peer advertising high speed but serving huge
|
||||||
|
files can lose a race — and blow past the download backstop — against a peer with a smaller
|
||||||
|
standard rip. The pipeline tries candidates in this order, so this is what makes the Soulseek
|
||||||
|
fall-through converge on a copy that actually completes. Pure — no I/O — unit-tested offline."""
|
||||||
scored: list[tuple] = []
|
scored: list[tuple] = []
|
||||||
for resp in responses:
|
for resp in responses:
|
||||||
username = resp.get("username", "")
|
username = resp.get("username", "")
|
||||||
has_slot = 1 if resp.get("hasFreeUploadSlot") else 0
|
has_slot = 0 if resp.get("hasFreeUploadSlot") else 1 # 0 sorts first (free slot = starts now)
|
||||||
speed = int(resp.get("uploadSpeed") or 0)
|
speed = int(resp.get("uploadSpeed") or 0)
|
||||||
queue = int(resp.get("queueLength") or 0)
|
queue = int(resp.get("queueLength") or 0)
|
||||||
by_dir: dict[str, list] = {}
|
by_dir: dict[str, list] = {}
|
||||||
@@ -94,6 +98,10 @@ def _parse_search_responses(responses: list) -> list[dict]:
|
|||||||
for directory, dfiles in by_dir.items():
|
for directory, dfiles in by_dir.items():
|
||||||
if not dfiles:
|
if not dfiles:
|
||||||
continue
|
continue
|
||||||
|
# Estimated seconds to pull this album from this peer at its advertised rate. Unknown/
|
||||||
|
# zero speed → a low nominal (1 B/s) so total size still orders those peers last.
|
||||||
|
total_bytes = sum(int(f["size"] or 0) for f in dfiles)
|
||||||
|
est_seconds = total_bytes / (speed if speed > 0 else 1)
|
||||||
lossless = all(f["ext"] in _LOSSLESS_EXT for f in dfiles)
|
lossless = all(f["ext"] in _LOSSLESS_EXT for f in dfiles)
|
||||||
dirname = _basename(directory)
|
dirname = _basename(directory)
|
||||||
if " - " in dirname:
|
if " - " in dirname:
|
||||||
@@ -111,8 +119,9 @@ def _parse_search_responses(responses: list) -> list[dict]:
|
|||||||
"format": "FLAC" if lossless else "MP3",
|
"format": "FLAC" if lossless else "MP3",
|
||||||
"bitrate": None,
|
"bitrate": None,
|
||||||
}
|
}
|
||||||
scored.append((has_slot, speed, -queue, candidate))
|
scored.append((has_slot, est_seconds, queue, candidate))
|
||||||
scored.sort(key=lambda t: (t[0], t[1], t[2]), reverse=True) # free + fast + short-queue first
|
# ascending: free slot first, then shortest estimated transfer, then shortest queue
|
||||||
|
scored.sort(key=lambda t: (t[0], t[1], t[2]))
|
||||||
return [c for *_rest, c in scored]
|
return [c for *_rest, c in scored]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -110,6 +110,21 @@ def test_parse_ranks_free_and_fast_peers_first():
|
|||||||
assert peers == ["fast", "slow", "queued"] # free+fast first; no-slot peer last despite speed
|
assert peers == ["fast", "slow", "queued"] # free+fast first; no-slot peer last despite speed
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_prefers_shorter_transfer_over_raw_speed():
|
||||||
|
# The Hybrid Theory case: a peer advertising higher speed but serving a ~2x-larger rip (24-bit
|
||||||
|
# / bloated FLAC) should lose to a peer with lower speed but a smaller standard rip that
|
||||||
|
# finishes sooner — both are the same quality class to Lyra, so faster-to-finish wins.
|
||||||
|
responses = [
|
||||||
|
{"username": "bloated", "hasFreeUploadSlot": True, "uploadSpeed": 20000, "queueLength": 0,
|
||||||
|
"files": [{"filename": rf"x\LP - Album\{i:02}.flac", "size": 40_000_000} for i in range(12)]},
|
||||||
|
{"username": "lean", "hasFreeUploadSlot": True, "uploadSpeed": 15000, "queueLength": 0,
|
||||||
|
"files": [{"filename": rf"y\LP - Album\{i:02}.flac", "size": 24_000_000} for i in range(12)]},
|
||||||
|
]
|
||||||
|
# bloated est = 480MB/20000 = 24000s; lean est = 288MB/15000 = 19200s → lean finishes first
|
||||||
|
peers = [json.loads(c["source_ref"])["username"] for c in _parse_search_responses(responses)]
|
||||||
|
assert peers[0] == "lean"
|
||||||
|
|
||||||
|
|
||||||
def test_eta_seconds_from_measured_rate():
|
def test_eta_seconds_from_measured_rate():
|
||||||
assert _eta_seconds(1000, 250, 250.0) == 3 # 750 bytes left / 250 B/s
|
assert _eta_seconds(1000, 250, 250.0) == 3 # 750 bytes left / 250 B/s
|
||||||
assert _eta_seconds(1000, 0, 100.0) == 10
|
assert _eta_seconds(1000, 0, 100.0) == 10
|
||||||
|
|||||||
Reference in New Issue
Block a user