From bacc528ba1260e6211cde8e5b33dcfe45e4fd49e Mon Sep 17 00:00:00 2001 From: BoulderBadgeDad Date: Fri, 26 Jun 2026 09:10:15 -0700 Subject: [PATCH] =?UTF-8?q?video=20search:=20stop=20slskd=20searches=20whe?= =?UTF-8?q?n=20done=20=E2=86=92=20fixes=20movie-search=20flooding?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- core/video/download_monitor.py | 22 ++++++++++++++-------- core/video/slskd_search.py | 14 ++++++++++++++ tests/test_video_retry.py | 25 +++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 8 deletions(-) diff --git a/core/video/download_monitor.py b/core/video/download_monitor.py index 0b83c6cd..038d8462 100644 --- a/core/video/download_monitor.py +++ b/core/video/download_monitor.py @@ -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: diff --git a/core/video/slskd_search.py b/core/video/slskd_search.py index d569f97f..8524a0ef 100644 --- a/core/video/slskd_search.py +++ b/core/video/slskd_search.py @@ -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"] diff --git a/tests/test_video_retry.py b/tests/test_video_retry.py index 6f3f4ca3..6cad2f07 100644 --- a/tests/test_video_retry.py +++ b/tests/test_video_retry.py @@ -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