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:
parent
6531a19c7a
commit
bacc528ba1
3 changed files with 53 additions and 8 deletions
|
|
@ -262,20 +262,26 @@ def _fail_or_retry(db, dl, error_msg) -> None:
|
||||||
|
|
||||||
|
|
||||||
def _search_for_retry(query, max_seconds=22):
|
def _search_for_retry(query, max_seconds=22):
|
||||||
"""A bounded blocking slskd search for the retry worker (shorter than the UI's)."""
|
"""A bounded blocking slskd search for the retry worker (shorter than the UI's).
|
||||||
from core.video.slskd_search import poll_search, start_search
|
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)
|
res = start_search(query)
|
||||||
sid = res.get("id")
|
sid = res.get("id")
|
||||||
if not sid:
|
if not sid:
|
||||||
return {"hits": [], "total_files": 0}
|
return {"hits": [], "total_files": 0}
|
||||||
deadline = time.monotonic() + max_seconds
|
deadline = time.monotonic() + max_seconds
|
||||||
last = {"hits": [], "total_files": 0}
|
last = {"hits": [], "total_files": 0}
|
||||||
while time.monotonic() < deadline:
|
try:
|
||||||
last = poll_search(sid)
|
while time.monotonic() < deadline:
|
||||||
if len(last.get("hits") or []) >= 12:
|
last = poll_search(sid)
|
||||||
break
|
if len(last.get("hits") or []) >= 12:
|
||||||
time.sleep(1.5)
|
break
|
||||||
return last
|
time.sleep(1.5)
|
||||||
|
return last
|
||||||
|
finally:
|
||||||
|
stop_search(sid)
|
||||||
|
|
||||||
|
|
||||||
def _requery_worker(dl_id) -> None:
|
def _requery_worker(dl_id) -> None:
|
||||||
|
|
|
||||||
|
|
@ -147,6 +147,20 @@ def start_search(query: str) -> dict:
|
||||||
return {"configured": True, "id": sid}
|
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:
|
def poll_responses(search_id: str) -> list:
|
||||||
"""Current grouped video hits for an in-flight search (cheap; call repeatedly)."""
|
"""Current grouped video hits for an in-flight search (cheap; call repeatedly)."""
|
||||||
return poll_search(search_id)["hits"]
|
return poll_search(search_id)["hits"]
|
||||||
|
|
|
||||||
|
|
@ -67,3 +67,28 @@ def test_merge_candidates_dedupes_against_tried():
|
||||||
out = merge_candidates(new, ["b.mkv"])
|
out = merge_candidates(new, ["b.mkv"])
|
||||||
assert [c["filename"] for c in out] == ["a.mkv"] # b excluded (tried), dup a collapsed
|
assert [c["filename"] for c in out] == ["a.mkv"] # b excluded (tried), dup a collapsed
|
||||||
assert out[0]["release_title"] == "Dune.2021.1080p"
|
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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue