Recover blocked searches, fall through to the next peer, rebuild the UI

Measured against a live instance: 47 failed jobs against 33 imported. Three
independent causes, all of which ended the request.

The Soulseek server filters some artist names. "Michael Jackson Billie Jean"
and bare "Michael Jackson" both return zero responses; "Billie Jean" returns
1083 files. The Weeknd, Kanye West and Frank Ocean behave the same way. Since
pick_and_enqueue sent exactly one query of `artist title`, every request for a
filtered artist failed reporting "searched 0 files" -- 28 of 29 occurrences.
search_queries() now walks a ladder, loosest last, ending at the bare title,
and escalates only when a search underdelivers so the common case still costs
one search. Dropping the artist from the query does not mean ignoring it:
match_ratio() looks for it in the folder path, where an organised share puts it.

A rejected transfer ended the request. pick_and_enqueue kept a running maximum
and discarded every runner-up, so "Transfer rejected: File not shared." -- a
per-file condition where candidate two almost always works -- was terminal, as
were timeouts and slskd's 500 on an unreachable peer. The ranked list is now
persisted on the job row, ordered to put a different peer next, and
try_next_candidate() advances through it. A job fails only once the list is
exhausted, and then reports every peer it tried and what each one said.

norm() destroyed every non-ASCII character, so "Beyonce" never matched
Beyonce, nor Bjork Bjork, nor Dont Don't. It now folds accents and drops
apostrophes while leaving & as a separator.

Also: availability outranks format in score(), because a FLAC behind 5000
queued users used to beat an immediately available MP3; transfers that stall or
that slskd forgets are abandoned instead of sitting in `downloading` forever;
searches are deleted only once finished, since deleting one mid-flight makes
slskd throw an optimistic-concurrency error and lose the record; a failed
search POST is retried rather than failing the job on one blip.

The UI moves to a sidebar shell -- rail at tablet, bottom bar on phones -- with
neutral chrome and the sampled artwork colour confined to hero areas rather
than repainting the whole interface. Adds URL routing, a primitive set under
components/ui, and a request sheet showing the attempt ladder. Fixes
hover:bg-glass-hi, which was never a valid class, so track rows and the search
field had no hover or focus state at all; a reduced-motion path that could
strand elements at opacity 0; the login page's hardcoded palette; and the
missing 404 and error boundaries.

tests/test_acquisition.py covers this path against a faked slskd. It had none.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Aryan SinghandClaude Opus 5 committed 2026-09-02 11:33:52 -05:00
1 parent 0dca779f59
commit e1b7b69261
47 files changed
+2939 -805

No files matched your search

+266
View File
@@ -0,0 +1,266 @@
"""The acquisition ladder: query variants, candidate fallback, stall handling.
This is the path that had no coverage at all, and it is where every production
failure lived. A fake slskd stands in for the network so the branches that only
happen on a bad day -- a peer that rejects, a transfer slskd forgets, a search
the server filters -- can be exercised deterministically.
Run standalone: python tests/test_acquisition.py
"""
import asyncio, json, os, sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
os.environ.setdefault("MUSE_AUTH", "off")
os.environ.setdefault("MUSE_DB", "dev/acquisition.db")
os.environ.setdefault("MUSE_SEARCH_POLLS", "2")
for f in ("dev/acquisition.db", "dev/acquisition.db-wal", "dev/acquisition.db-shm"):
if os.path.exists(f):
os.remove(f)
import app # noqa: E402
ok = fail = 0
def check(name, cond, detail=""):
global ok, fail
if cond:
ok += 1
print(f" PASS {name}")
else:
fail += 1
print(f" FAIL {name} {detail}")
# Searches poll on a 2s cadence; nothing here is actually waiting on a network.
async def _instant(_):
return None
app.asyncio.sleep = _instant
app.init_db() # normally done in lifespan; there is no server here
# ── a slskd that behaves however the test needs ──────────────────────────
class Resp:
def __init__(self, data=None, status=200):
self._data, self.status = data, status
def raise_for_status(self):
if self.status >= 400:
raise RuntimeError(f"Server error '{self.status} Internal Server Error'")
def json(self):
return self._data
class FakeSlskd:
"""Records what was asked of it and answers from a script."""
def __init__(self, per_query=None, enqueue_status=None, transfers=None):
self.per_query = per_query or {} # query text -> list of responses
self.enqueue_status = enqueue_status or {} # username -> http status
self.transfers = transfers or []
self.searched = []
self.enqueued = []
self.deleted = []
self._last = None
def __call__(self, timeout=30.0):
return self
async def __aenter__(self):
return self
async def __aexit__(self, *a):
return False
async def post(self, url, json=None):
if url == "/api/v0/searches":
self._last = json["searchText"]
self.searched.append(self._last)
return Resp({})
user = url.rsplit("/", 1)[-1]
st = self.enqueue_status.get(user, 200)
self.enqueued.append(user)
return Resp({}, st)
async def get(self, url, params=None):
if url.startswith("/api/v0/searches/"):
responses = self.per_query.get(self._last, [])
return Resp({"state": "Completed, ResponseLimitReached",
"responses": responses,
"fileCount": sum(len(r["files"]) for r in responses)})
return Resp(self.transfers)
async def delete(self, url, params=None):
self.deleted.append(url)
return Resp({})
def peer(username, filename, *, slot=True, queue=0, size=5_000_000, bitrate=320):
return {"username": username, "hasFreeUploadSlot": slot,
"queueLength": queue, "uploadSpeed": 1_000_000,
"files": [{"filename": filename, "size": size, "bitRate": bitrate}]}
def make_job(job_id, artist="Michael Jackson", title="Billie Jean"):
with app.db() as c:
c.execute("INSERT OR REPLACE INTO jobs (id, state, artist, title, album)"
" VALUES (?,?,?,?,?)", (job_id, "searching", artist, title, None))
def job_row(job_id):
with app.db() as c:
return c.execute("SELECT * FROM jobs WHERE id=?", (job_id,)).fetchone()
def main() -> int:
print("\nnormalisation")
check("accents fold to ASCII", app.norm("Beyoncé") == "beyonce",
repr(app.norm("Beyoncé")))
check("umlauts fold", app.norm("Björk") == "bjork", repr(app.norm("Björk")))
check("slashed o folds", app.norm("margø") == "margo", repr(app.norm("margø")))
check("apostrophes vanish, not split",
app.norm("Don't Stop") == "dont stop", repr(app.norm("Don't Stop")))
check("ampersand still separates", app.norm("R&B") == "r b", repr(app.norm("R&B")))
print("\nquery ladder")
qs = app.search_queries("Michael Jackson", "Billie Jean")
check("falls back to the bare title", "Billie Jean" in qs, qs)
check("full query is still tried first", qs[0] == "Michael Jackson Billie Jean", qs)
qs = app.search_queries("Frank Ocean", "Novacane (Explicit Version)")
check("strips a parenthetical suffix", "Frank Ocean Novacane" in qs, qs)
qs = app.search_queries("Post Sex Nachos", "Pop Tarts (feat. MARIS)")
check("strips a featuring credit", "Post Sex Nachos Pop Tarts" in qs, qs)
qs = app.search_queries("The Weeknd", "House Of Balloons / Glass Table Girls")
check("splits a slashed title", "House Of Balloons" in qs, qs)
check("ladder is capped", len(app.search_queries("A", "B")) <= app.MAX_QUERIES)
print("\nscoring")
avail_mp3 = app.score({"filename": r"x\Radiohead - Creep.mp3", "size": 1,
"bitRate": 320},
{"username": "u", "hasFreeUploadSlot": True,
"queueLength": 0, "uploadSpeed": 0},
"Radiohead", "Creep")
queued_flac = app.score({"filename": r"x\Radiohead - Creep.flac", "size": 1,
"bitRate": 1000},
{"username": "v", "hasFreeUploadSlot": False,
"queueLength": 400, "uploadSpeed": 0},
"Radiohead", "Creep")
check("an available MP3 beats a FLAC behind 400 people",
avail_mp3 > queued_flac, f"{avail_mp3:.0f} vs {queued_flac:.0f}")
check("artist found in the folder, not the filename",
app.match_ratio(r"Radiohead\Pablo Honey\02 Creep.flac",
"Radiohead", "Creep") == 1.0)
check("a matching artist cannot rescue the wrong song",
app.match_ratio(r"Radiohead\Pablo Honey\04 Karma Police.flac",
"Radiohead", "Creep") == 0.0)
check("reordered filename still matches",
app.match_ratio(r"x\Creep - Radiohead.mp3", "Radiohead", "Creep") == 1.0)
print("\ncandidate ladder")
fake = FakeSlskd(per_query={
"Billie Jean": [peer("alice", r"m\Michael Jackson\Billie Jean.flac"),
peer("bob", r"n\Michael Jackson\Billie Jean.mp3"),
peer("carol", r"o\Michael Jackson\Billie Jean.flac")],
})
app.slskd_client = fake
make_job("j1")
asyncio.run(app.pick_and_enqueue("j1", "Michael Jackson", "Billie Jean"))
r = job_row("j1")
check("blocklisted artist recovered by the title rung",
"Billie Jean" in fake.searched, fake.searched)
check("job is downloading", r["state"] == "downloading", r["detail"])
check("runners-up were kept", len(json.loads(r["candidates"] or "[]")) == 2,
r["candidates"])
check("consecutive candidates are different peers",
len({c["username"] for c in json.loads(r["candidates"])}) == 2)
print("\nfallback on failure")
fake.enqueue_status = {}
asyncio.run(app.abandon(job_row("j1"), "Transfer rejected: File not shared."))
r = job_row("j1")
check("a rejection advances instead of failing",
r["state"] == "downloading", f"{r['state']} / {r['detail']}")
check("the failed peer is recorded",
"File not shared" in (r["tried"] or ""), r["tried"])
check("attempt counter advanced", r["attempts"] == 2, r["attempts"])
asyncio.run(app.abandon(job_row("j1"), "timed out"))
asyncio.run(app.abandon(job_row("j1"), "timed out"))
r = job_row("j1")
check("only fails once the ladder is exhausted", r["state"] == "failed", r["detail"])
check("failure names every attempt, not just the last",
"tried 3 peers" in (r["detail"] or ""), r["detail"])
print("\nattempt cap")
fake2 = FakeSlskd(per_query={"Creep": [
peer(f"u{i}", rf"x\Radiohead\Creep.flac") for i in range(8)]})
# every peer refuses to accept the transfer
fake2.enqueue_status = {f"u{i}": 500 for i in range(8)}
app.slskd_client = fake2
make_job("j2", "Radiohead", "Creep")
asyncio.run(app.pick_and_enqueue("j2", "Radiohead", "Creep"))
r = job_row("j2")
check("gives up at MAX_ATTEMPTS, not at the end of the list",
r["state"] == "failed" and r["attempts"] == app.MAX_ATTEMPTS,
f"state={r['state']} attempts={r['attempts']}")
check("unreachable peers are reported as such",
"could not reach peer" in (r["tried"] or ""), r["detail"])
print("\nempty results")
fake3 = FakeSlskd(per_query={})
app.slskd_client = fake3
make_job("j3", "Nobody", "Nothing")
asyncio.run(app.pick_and_enqueue("j3", "Nobody", "Nothing"))
r = job_row("j3")
check("says the search was empty, not that matching failed",
"Soulseek returned nothing" in (r["detail"] or ""), r["detail"])
check("names the queries it tried", "'Nothing'" in (r["detail"] or ""), r["detail"])
print("\ntransient slskd")
class Flaky(FakeSlskd):
def __init__(self):
super().__init__(per_query={"Creep": [peer("z", r"x\Creep.flac")]})
self.n = 0
async def post(self, url, json=None):
if url == "/api/v0/searches":
self.n += 1
if self.n < 3:
raise RuntimeError("") # httpx timeouts stringify to nothing
return await FakeSlskd.post(self, url, json=json)
flaky = Flaky()
app.slskd_client = flaky
make_job("j4", "Radiohead", "Creep")
asyncio.run(app.pick_and_enqueue("j4", "Radiohead", "Creep"))
check("a blip is retried rather than failing the job",
job_row("j4")["state"] == "downloading", job_row("j4")["detail"])
print("\nsearch hygiene")
check("finished searches are cleaned up", len(flaky.deleted) >= 1)
print("\nstall clock")
from datetime import datetime, timedelta, timezone
with app.db() as c:
c.execute("UPDATE jobs SET started_at=? WHERE id=?",
((datetime.now(timezone.utc) - timedelta(minutes=90)).isoformat(),
"j4"))
check("a 90-minute-old transfer reads as stalled",
app.stalled_for(job_row("j4")) > app.STALL_MINUTES,
app.stalled_for(job_row("j4")))
with app.db() as c:
c.execute("UPDATE jobs SET started_at=NULL WHERE id=?", ("j4",))
check("a job with no clock is not spuriously stalled",
app.stalled_for(job_row("j4")) == 0.0)
print(f"\n{ok} passed, {fail} failed")
return 1 if fail else 0
if __name__ == "__main__":
sys.exit(main())