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:
1 parent
0dca779f59
commit
171e117f7c
47 files changed
+2939
-805
No files matched your search
@@ -148,8 +148,9 @@ automatically. Every job shows what it *resolved to*, so a bad auto-pick is
|
||||
visible in the list rather than quietly landing the wrong song. Lines that
|
||||
match nothing are reported back as skipped instead of failing the batch.
|
||||
|
||||
Failed jobs get a **Retry** button. Most Soulseek failures are the peer going
|
||||
offline, not a bad request, so retrying later usually just works.
|
||||
Failed jobs get a **Retry** button, but far fewer jobs reach that state now:
|
||||
a request that loses its peer falls through to the next candidate on its own.
|
||||
See "When a peer says no" below.
|
||||
|
||||
### Everything goes through a bounded queue
|
||||
|
||||
@@ -179,10 +180,83 @@ is needed to catch `Come As You Are (Live)`, but substring matching would also
|
||||
penalise `Deliver Me` and `Olive`. And a term only counts against a result when
|
||||
you didn't type it, so `oasis live forever` and `nirvana unplugged` still work.
|
||||
|
||||
**Picking the file (Soulseek).** Format preference first, then whether the peer
|
||||
can actually serve you (free upload slot, short queue), then filename
|
||||
plausibility, then bitrate. A marginally better file behind 200 queued users
|
||||
loses to a good-enough one available now.
|
||||
**Picking the file (Soulseek).** Whether the peer can actually serve you comes
|
||||
first — a free upload slot and a short queue — then format preference, then
|
||||
filename plausibility, then bitrate. A marginally better file behind 200 queued
|
||||
users loses to a good-enough one available now.
|
||||
|
||||
That order is deliberate and was once the other way round, which was a bug
|
||||
worth naming: format outranked availability, so a FLAC behind a five-thousand
|
||||
deep queue always beat an MP3 that was free to download immediately. Whether
|
||||
the peer can serve you is the single most predictive signal for whether the
|
||||
job ever finishes, so nothing outranks it.
|
||||
|
||||
## Searching for a name the file doesn't use
|
||||
|
||||
Two things go wrong between "what Deezer calls this track" and "what a stranger
|
||||
named the file", and both used to end the request.
|
||||
|
||||
**The Soulseek server filters some artist names.** This is measurable: on a
|
||||
healthy, logged-in client, `Michael Jackson Billie Jean` and bare
|
||||
`Michael Jackson` both return **zero** responses, while `Billie Jean` returns
|
||||
over a thousand files. The same is true of The Weeknd, Kanye West and Frank
|
||||
Ocean. Sending one query of `artist title` — which is all muse used to do —
|
||||
means every request for a filtered artist fails reporting "searched 0 files",
|
||||
indistinguishable from a track nobody shares.
|
||||
|
||||
So muse tries a **ladder** of queries, loosest last, and stops at the first
|
||||
rung that yields enough candidates:
|
||||
|
||||
```
|
||||
Michael Jackson Billie Jean 0 responses
|
||||
Billie Jean 251 responses, 1083 files
|
||||
```
|
||||
|
||||
The rungs drop, in order: a parenthetical suffix (`Novacane (Explicit Version)`),
|
||||
a `feat.` credit, a leading article, everything after a `/` in a split title,
|
||||
and finally the artist itself. Since most requests are answered by the first
|
||||
query, the extra searches cost nothing in the common case — `MUSE_MAX_QUERIES`
|
||||
caps how far it will go.
|
||||
|
||||
Dropping the artist from the *query* does not mean ignoring it. The artist is
|
||||
then matched against the whole **folder path**, which is where a well-organised
|
||||
share puts it anyway:
|
||||
|
||||
```
|
||||
anjya music/Michael Jackson/1982 - Thriller [flac]/06 - Billie Jean.flac
|
||||
^^^^^^^^^^^^^^^^ artist found here
|
||||
```
|
||||
|
||||
**Names differ in punctuation and accents.** Normalisation folds accents away
|
||||
before comparing, so `Beyoncé` matches a file called `Beyonce`, `Björk` matches
|
||||
`Bjork`, and `Don't Stop` matches `Dont Stop`. Apostrophes are removed rather
|
||||
than turned into a space; `&` still separates, so `R&B` stays two tokens
|
||||
instead of collapsing into the unrelated word `rb`. Track numbers and release
|
||||
tags (`02 - `, `[320kbps]`) are stripped before the comparison, and a
|
||||
reordered filename (`Creep - Radiohead`) scores the same as `Radiohead - Creep`.
|
||||
|
||||
The title has to stand on its own: a matching artist cannot carry a filename
|
||||
whose title is wrong. Without that floor, a strong artist match let a
|
||||
*different song by the right artist* through.
|
||||
|
||||
## When a peer says no
|
||||
|
||||
`Transfer rejected: File not shared.` is the commonest failure on Soulseek —
|
||||
the peer's share index is stale and the file it advertised is gone. It is a
|
||||
condition of one *file*, not of the request, and the right response is to take
|
||||
the next candidate.
|
||||
|
||||
muse keeps the whole ranked list (`MUSE_CANDIDATES`, default 8) on the job row
|
||||
rather than only the winner, so when a transfer is rejected, times out, errors,
|
||||
or slskd cannot open a connection to the peer at all, the next candidate is
|
||||
enqueued instead of the request failing. The list is ordered to put a different
|
||||
peer next, so one offline user cannot burn three attempts in a row. Only when
|
||||
the list is exhausted — or `MUSE_MAX_ATTEMPTS` is reached — does the job fail,
|
||||
and it then reports every peer it tried and what each one said.
|
||||
|
||||
A download that sits with no bytes for `MUSE_STALL_MINUTES` is abandoned the
|
||||
same way, as is one slskd has forgotten about. Both used to leave the job in
|
||||
`downloading` forever, with a cheerful pulsing dot and no log line.
|
||||
|
||||
## Review: when muse isn't sure
|
||||
|
||||
@@ -470,7 +544,14 @@ stat -c "%h" "/data/media/music/<Artist>/<Album> (<Year>)/<NN> - <Title>.flac"
|
||||
| `MUSE_LIBRARY_DIR` | `/data/media/music` | where the library lives |
|
||||
| `MUSE_DB` | `/config/muse.db` | job history |
|
||||
| `MUSE_FORMATS` | `flac,mp3` | ordered preference; anything else is rejected |
|
||||
| `MUSE_MIN_MATCH` | `0.45` | filename-to-title similarity floor |
|
||||
| `MUSE_MIN_MATCH` | `0.45` | filename-to-track similarity floor |
|
||||
| `MUSE_TITLE_FLOOR` | `0.5` | the title alone must clear this before the artist counts |
|
||||
| `MUSE_CANDIDATES` | `8` | ranked candidates kept per request, for fallback |
|
||||
| `MUSE_MAX_ATTEMPTS` | `5` | peers tried before a request is given up on |
|
||||
| `MUSE_STALL_MINUTES` | `20` | a transfer moving no bytes for this long is abandoned |
|
||||
| `MUSE_MAX_QUERIES` | `4` | rungs of the search ladder to try |
|
||||
| `MUSE_MIN_CANDIDATES` | `3` | below this, escalate to the next query |
|
||||
| `MUSE_SEARCH_RETRIES` | `3` | attempts at issuing a search before failing |
|
||||
| `MUSE_AUTO_CONFIDENCE` | `0.95` | at or above this, import without asking |
|
||||
| `MUSE_REVIEW_CONFIDENCE` | `0.70` | below this, held but probably wrong |
|
||||
| `MUSE_WORKERS` | `3` | concurrent Soulseek searches |
|
||||
|
||||
Reference in new issue
Block a user