Commit Graph
4 Commits
  • Stop the candidate ladder from hammering slskd through a stall
    A live request for "Michael Jackson - Chicago" exercised both new paths
    correctly -- the blocked artist name fell back to the bare title and found real
    Xscape rips, and the ladder tried five peers instead of dying on the first --
    and then failed anyway, with all five reporting a 500 from slskd.
    
    slskd's own log shows why, and it is not the peers. Every timeout in the
    preceding hour landed inside the single minute muse was enqueueing, and in the
    same window slskd also failed to resolve three OTHER users' searches. Each
    enqueue blocks about five seconds waiting on a peer message connection, so five
    back-to-back attempts is twenty-five seconds of sustained pressure on one
    subsystem. The ladder turned one bad moment into five spent candidates and a
    dead request, and made slskd worse for everyone else while doing it.
    
    Attempts are now spaced by MUSE_ATTEMPT_BACKOFF. Failures are classified: "File
    not shared" is about a file and still advances at once, while a timeout is about
    slskd and, after MUSE_SYSTEMIC_LIMIT of them in a row, stops the walk and leaves
    the untried candidates on the row. A retry resumes that ladder instead of
    re-running a search whose answer has not changed.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
  • Count the peers actually tried, not the attempt counter
    A job carried over from before the candidate ladder has attempts=0 while
    carrying a real failure in `tried`, so the summary rendered as "tried 0 peers:
    1x no bytes in 20m" -- a count of zero next to the evidence contradicting it.
    The live instance produced exactly that when the stall check finally cleared
    its long-stuck job.
    
    Counts distinct usernames in `tried` instead. The two agree for anything this
    version enqueued; the list is the one that is right in both cases.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
  • Age a stalled transfer from created_at when started_at is missing
    The stall clock read started_at and returned zero when it was absent, which
    exempted every row written before that column existed -- precisely the jobs
    that have been stuck the longest. The live instance had one: a `downloading`
    job from before the upgrade, its transfer parked in Queued, Remotely, which
    the new check would have skipped forever.
    
    Falls back to created_at, which for a job still in `downloading` is the same
    question asked a slightly different way.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
  • 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>