From 1b3c18e4e62d30a9668c49e1a42709ce65d821e0 Mon Sep 17 00:00:00 2001 From: BoulderBadgeDad Date: Fri, 26 Jun 2026 10:08:58 -0700 Subject: [PATCH] =?UTF-8?q?slskd=20search:=20supply=20our=20own=20search?= =?UTF-8?q?=20id=20=E2=86=92=20fixes=20'search=20didn't=20run'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- core/video/slskd_search.py | 38 ++++++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/core/video/slskd_search.py b/core/video/slskd_search.py index 8524a0ef..86e04c28 100644 --- a/core/video/slskd_search.py +++ b/core/video/slskd_search.py @@ -30,7 +30,10 @@ def _conn(): from config.settings import config_manager base = str(config_manager.get("soulseek.slskd_url", "") or "").rstrip("/") key = config_manager.get("soulseek.api_key", "") or "" - return base, ({"X-API-Key": key} if key else {}) + headers = {"Accept": "application/json"} # make slskd answer in JSON, not whatever default + if key: + headers["X-API-Key"] = key + return base, headers def build_query(scope: str, title: Any, *, year: Any = None, season: Any = None, @@ -128,23 +131,38 @@ def search_timeout_ms() -> int: def start_search(query: str) -> dict: """Kick off a slskd search (don't wait). Returns {configured[, id][, error]}. - The caller polls ``poll_responses(id)`` until satisfied (like the music side).""" + The caller polls ``poll_responses(id)`` until satisfied (like the music side). + + We generate the search id OURSELVES and pass it to slskd — it honors a client-supplied + ``id`` — so we never depend on parsing it back out of the POST response. Some slskd builds + return the id in a Location header / a non-dict body, which made us think the search + 'didn't run' even though slskd created it (the bug behind the fast 'no results').""" + import uuid base, headers = _conn() if not base: return {"configured": False} - payload = {"searchText": query, "timeout": search_timeout_ms(), "filterResponses": True, - "minimumResponseFileCount": 1, "minimumPeerUploadSpeed": _min_speed_bytes()} + search_id = str(uuid.uuid4()) + payload = {"id": search_id, "searchText": query, "timeout": search_timeout_ms(), + "filterResponses": True, "minimumResponseFileCount": 1, + "minimumPeerUploadSpeed": _min_speed_bytes()} try: r = requests.post(base + "/api/v0/searches", json=payload, headers=headers, timeout=15) r.raise_for_status() - data = r.json() except Exception as e: # noqa: BLE001 - surface any slskd/network failure to the UI return {"configured": True, "error": str(e)} - sid = data.get("id") if isinstance(data, dict) else ( - data[0].get("id") if isinstance(data, list) and data and isinstance(data[0], dict) else None) - if not sid: - return {"configured": True, "error": "slskd returned no search id"} - return {"configured": True, "id": sid} + # Prefer the id slskd echoes back if present; otherwise the one we supplied (it's honored). + sid = None + try: + data = r.json() + if isinstance(data, dict): + sid = data.get("id") + elif isinstance(data, str) and data.strip(): + sid = data.strip().strip('"') + elif isinstance(data, list) and data and isinstance(data[0], dict): + sid = data[0].get("id") + except Exception: # noqa: BLE001, S110 - non-JSON / empty body → fall back to our id + pass + return {"configured": True, "id": sid or search_id} def stop_search(search_id) -> None: