Commit graph

7 commits

Author SHA1 Message Date
BoulderBadgeDad
12cec4c042 video search: availability-aware ranking (best-in-class, like music)
picks the most DOWNLOADABLE release, not just the biggest/fastest:
- peer_availability() mirrors the music side's scoring — free upload slot, tiered
  upload speed, graduated queue penalty.
- group_video_files now captures queueLength, picks the best PEER per release by
  (availability, then speed) instead of speed alone, and ranks hits by availability.
- _evaluate_hits sorts by (accepted, quality score, availability, size) so within a
  quality tier a free-slot/empty-queue source wins over one stuck behind a 1500-deep
  queue — fixes the downloads that sat at 0%.
queue/speed also surfaced on results for the UI. tested.
2026-06-26 10:54:56 -07:00
BoulderBadgeDad
e84d1bcde5 slskd search: throttle creation to avoid 429 storms
the real cause of the movie-search failures: slskd rate-limits search CREATION and
429s when exceeded — and the video path had NO throttle (the music side caps ~35/220s).
the thread-pool auto-grab fired searches in bursts (and each 429 returned instantly,
cascading into more), storming slskd into 429s; only a few slipped through.

add a shared throttle on start_search: min 2s between creations + a 35-per-220s window
cap (mirrors music) + a cooldown when a 429 IS hit (honors Retry-After) so it backs off
instead of hammering. pure reserve/cooldown logic tested.
2026-06-26 10:27:28 -07:00
BoulderBadgeDad
1b3c18e4e6 slskd search: supply our own search id → fixes 'search didn't run'
root cause of the fast 'no results' / 'search didn't run': slskd WAS creating the
searches (they showed up + returned results), but start_search couldn't read the
search id back out of the POST response, returned no id, and the caller bailed
instantly — then raced to the next, which is why slskd showed different titles than
the log.

fix: generate the search id client-side and pass it to slskd (it honors a supplied
id), so we never depend on parsing the response. still prefer slskd's echoed id if
present (dict/list/bare-string), fall back to ours. also send Accept: application/json
so slskd answers in json.
2026-06-26 10:09:31 -07:00
BoulderBadgeDad
bacc528ba1 video search: stop slskd searches when done → fixes movie-search flooding
bug: start_search tells slskd to keep searching its full timeout (~60s), but
_search_for_retry only polled then walked away — never stopping the search. for
movies (popular → 12+ hits in <1s → worker early-breaks → fires the next search
immediately) this piled up dozens of 60s-long slskd searches at once. tv episodes
return fewer hits → workers block the full 22s → searches issue slowly → no pileup
(why tv looked fine and movies flooded).

fix: stop_search(id) (DELETE /api/v0/searches/{id}); _search_for_retry stops its
search in a finally, even on early break. concurrent slskd searches now ≈ the
worker pool (3). retry worker benefits too. tested.
2026-06-26 09:10:15 -07:00
BoulderBadgeDad
5661ccbfd2 video search: poll the FULL slskd timeout (~60s) + explain audio-only results
The poll capped at 32s but slskd results trickle in over ~50s (the music side waits
the whole search_timeout), so slow searches like 'Project Hail Mary' returned 'none'
before results landed. Now:
- /search/start returns poll_ms (slskd search_timeout + 8s); the UI polls that long
  (capped 80s), streaming results as they arrive, stopping early only once results
  clearly plateau (≥20s + stable) or hit 25.
- /search/poll returns total_files; when 0 video releases but slskd DID return files,
  the panel says 'returned N files, but none are video — likely audio/other for this
  title' instead of a blank 'none' (Soulseek is audio-heavy; many movie titles are
  audiobooks there). slskd_search.poll_search() returns {hits, total_files}.
Tests green, ruff + balance clean.
2026-06-19 16:55:53 -07:00
BoulderBadgeDad
42c67a6d4d video search: stream slskd results (start + poll) — fixes 'no results'
The old search did a 4.5s slskd search + 8s wait; slskd responses trickle in over
10-30s, so the window closed before results arrived (you'd see them in slskd but the
panel said none). Now it works like the music side:
- slskd_search.start_search() (uses the shared soulseek.search_timeout) + poll_responses().
- POST /downloads/search/start (mock = immediate; soulseek = returns a search id) +
  GET /downloads/search/poll. Shared _evaluate_hits ranks each poll's hits.
- UI streams: starts the search, polls every 1.3s, renders results live with a
  'searching…' badge, stops when results plateau (4 stable polls) or ~32s. Live
  re-renders suppress per-card entrance so it doesn't blink.
Tests green, ruff clean.
2026-06-19 15:23:47 -07:00
BoulderBadgeDad
305020f61e video search: REAL slskd search for the Soulseek source (was mocked)
The Soulseek ⌕ now actually queries your slskd instance instead of fabricating
results. core/video/slskd_search.py (isolated): build_query(scope,…) → POST
/api/v0/searches (shared soulseek.* URL+key) → poll /responses (bounded ~8s,
early-exit at 25) → keep video files → group_video_files() folds them into one hit
per release folder with a peer count + the fastest available source. Same
{title,size_bytes,…} shape, so parse→evaluate→rank/cards are unchanged.

Endpoint routes source=='soulseek' to slskd (torrent/usenet stay mocked), surfaces
'slskd not configured' / network errors, carries username/peers/slots/filename
through for the cards + a future real Grab, and caps to 40. Pure helpers tested (4);
isolation guard + ruff clean.
2026-06-19 14:16:44 -07:00