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.
This commit is contained in:
BoulderBadgeDad 2026-06-26 09:10:15 -07:00
parent 6531a19c7a
commit bacc528ba1
3 changed files with 53 additions and 8 deletions

View file

@ -262,20 +262,26 @@ def _fail_or_retry(db, dl, error_msg) -> None:
def _search_for_retry(query, max_seconds=22):
"""A bounded blocking slskd search for the retry worker (shorter than the UI's)."""
from core.video.slskd_search import poll_search, start_search
"""A bounded blocking slskd search for the retry worker (shorter than the UI's).
Always STOPS the slskd search when done otherwise it keeps running its full timeout
(~60s) and, since the bounded auto-grab fires searches back-to-back, they pile up on
slskd. Stopping each one keeps concurrent slskd searches the worker pool size."""
from core.video.slskd_search import poll_search, start_search, stop_search
res = start_search(query)
sid = res.get("id")
if not sid:
return {"hits": [], "total_files": 0}
deadline = time.monotonic() + max_seconds
last = {"hits": [], "total_files": 0}
while time.monotonic() < deadline:
last = poll_search(sid)
if len(last.get("hits") or []) >= 12:
break
time.sleep(1.5)
return last
try:
while time.monotonic() < deadline:
last = poll_search(sid)
if len(last.get("hits") or []) >= 12:
break
time.sleep(1.5)
return last
finally:
stop_search(sid)
def _requery_worker(dl_id) -> None:

View file

@ -147,6 +147,20 @@ def start_search(query: str) -> dict:
return {"configured": True, "id": sid}
def stop_search(search_id) -> None:
"""Stop + forget a search on slskd once we're done polling it. slskd otherwise keeps
every search running its full ``search_timeout`` (default 60s), so the bounded automation
searches which finish fast when results come in pile up dozens-deep and swamp slskd.
Best-effort; a failed cleanup never matters."""
base, headers = _conn()
if not base or not search_id:
return
try:
requests.delete(base + "/api/v0/searches/%s" % search_id, headers=headers, timeout=10)
except Exception: # noqa: BLE001, S110 - cleanup is best-effort
pass
def poll_responses(search_id: str) -> list:
"""Current grouped video hits for an in-flight search (cheap; call repeatedly)."""
return poll_search(search_id)["hits"]

View file

@ -67,3 +67,28 @@ def test_merge_candidates_dedupes_against_tried():
out = merge_candidates(new, ["b.mkv"])
assert [c["filename"] for c in out] == ["a.mkv"] # b excluded (tried), dup a collapsed
assert out[0]["release_title"] == "Dune.2021.1080p"
# ── bounded search stops its slskd search (so they don't pile up) ─────────────
def test_search_for_retry_stops_the_slskd_search(monkeypatch):
"""The bounded auto-grab search MUST stop its slskd search when done — slskd otherwise
keeps each running ~60s, and back-to-back searches pile up and swamp it. Stopped even on
an early break (12+ hits arrive fast)."""
import core.video.slskd_search as ss
from core.video import download_monitor as dm
stopped = []
monkeypatch.setattr(ss, "start_search", lambda q: {"id": "S1"})
monkeypatch.setattr(ss, "poll_search", lambda sid: {"hits": list(range(12)), "total_files": 12})
monkeypatch.setattr(ss, "stop_search", lambda sid: stopped.append(sid))
res = dm._search_for_retry("Dune 2021")
assert len(res["hits"]) == 12 and stopped == ["S1"] # early-broke AND stopped
def test_search_for_retry_no_id_skips_stop(monkeypatch):
import core.video.slskd_search as ss
from core.video import download_monitor as dm
stopped = []
monkeypatch.setattr(ss, "start_search", lambda q: {"configured": True, "error": "boom"})
monkeypatch.setattr(ss, "stop_search", lambda sid: stopped.append(sid))
assert dm._search_for_retry("x") == {"hits": [], "total_files": 0}
assert stopped == [] # nothing started → nothing to stop