Commit graph

1 commit

Author SHA1 Message Date
Broque Thomas
e20994e1c7 Manual picks: stream results, don't auto-retry, fix stuck-at-0%
Three follow-on fixes to the manual-search candidates modal once people
started actually using it:

1. NDJSON streaming. Manual search waited for every source to return
   before showing anything. Now streams one event per source as each
   completes — header line, source_results per source, done terminator.
   Frontend appends rows incrementally via response.body.getReader().

2. Manual picks no longer auto-retry on failure. New _user_manual_pick
   flag set on the task in /download-candidate. Both monitor retry
   paths (not-in-live-transfers stuck + Errored state) bail on the
   flag. Surfaces the failure to the user instead of silently picking
   a different candidate via fresh search.

3. Non-Soulseek manual picks (youtube/tidal/qobuz/hifi/deezer/
   soundcloud/lidarr) no longer stuck at "downloading 0%" forever. The
   live_transfers IF branch now marks manual-pick tasks failed
   directly when the engine reports Errored, instead of deferring to
   the monitor (which bails on manual picks). Engine fallback in else
   branch covers the rare race where the orchestrator's pre-populated
   transfer lookup is missing the entry.

Plus a deadlock fix discovered along the way: the new failure path
synchronously called on_download_completed while holding tasks_lock,
which itself re-acquires the same Lock — non-reentrant
threading.Lock self-deadlocked the polling thread. While wedged, every
other endpoint that needed the lock (including /candidates → other
failed rows couldn't open modals) hung waiting. Moved completion
callbacks onto a daemon thread so the lock releases first.

Plus failed/not_found/cancelled rows are now ALWAYS clickable (not
just when the auto-search cached candidates) — the modal carries the
manual search bar, which is the user's recourse for empty results.

Plus manual download worker now runs on a dedicated thread instead of
competing with the batch's 3-worker missing_download_executor pool —
saturated batches no longer queue manual picks indefinitely.

All scoped to manual picks via the _user_manual_pick flag — auto
attempt flow byte-identical to before. Engine fallback gated on the
flag too so auto attempts in the else branch keep the original
do-nothing behavior (safety valve handles the stuck-forever case).

Also dropped _handle_failed_download from web_server.py — defined
but had no callers (dead code).

17 new unit tests pin the gate behavior:
- engine fallback: Errored/Cancelled/Succeeded/InProgress transitions,
  manual-pick gate, terminal-state skip, soulseek skip, missing
  download_id skip, engine returning None, orchestrator exception
- monitor: manual-pick skips not-in-live-transfers retry + Errored
  retry
- IF-branch end-to-end: Errored marks failed, "Completed, Errored"
  hits failure branch, auto attempts defer to monitor

Manual-search endpoint tests rewritten for NDJSON: 11 cases (validation,
single-source dispatch, parallel "all" dispatch, one-event-per-source
streaming shape, unconfigured-source skip + reject, header metadata,
per-source exception isolation).

Full suite 2259 passed, 1 skipped.
2026-05-08 15:12:58 -07:00