From 5d2913153009f23b41df51ed6ec1343f7c20570e Mon Sep 17 00:00:00 2001 From: Broque Thomas Date: Sun, 28 Dec 2025 21:02:01 -0800 Subject: [PATCH] Add configurable Soulseek search timeout and buffer Introduces 'search_timeout' and 'search_timeout_buffer' options to Soulseek settings in the config, backend, and web UI. The backend now uses these values to control search duration and polling, allowing users to fine-tune how long searches run and how long to wait for late results. --- config/config.example.json | 4 +++- core/soulseek_client.py | 32 ++++++++++++++++++++++++-------- webui/index.html | 10 ++++++++++ webui/static/script.js | 6 +++++- 4 files changed, 42 insertions(+), 10 deletions(-) diff --git a/config/config.example.json b/config/config.example.json index 2180f02c..692aaae9 100644 --- a/config/config.example.json +++ b/config/config.example.json @@ -30,7 +30,9 @@ "slskd_url": "http://host.docker.internal:5030", "api_key": "SoulseekAPIKey", "download_path": "/app/downloads", - "transfer_path": "/app/Transfer" + "transfer_path": "/app/Transfer", + "search_timeout": 60, + "search_timeout_buffer": 15 }, "logging": { "path": "logs/app.log", diff --git a/core/soulseek_client.py b/core/soulseek_client.py index 0fde5189..9c51f813 100644 --- a/core/soulseek_client.py +++ b/core/soulseek_client.py @@ -602,17 +602,22 @@ class SoulseekClient: return None - async def search(self, query: str, timeout: int = 60, progress_callback=None) -> tuple[List[TrackResult], List[AlbumResult]]: + async def search(self, query: str, timeout: int = None, progress_callback=None) -> tuple[List[TrackResult], List[AlbumResult]]: if not self.base_url: logger.error("Soulseek client not configured") return [], [] - + + # Get timeout from config if not specified + if timeout is None: + from config.settings import config_manager + timeout = config_manager.get('soulseek.search_timeout', 60) + # Apply rate limiting before search await self._wait_for_rate_limit() - + try: - logger.info(f"Starting search for: '{query}'") - + logger.info(f"Starting search for: '{query}' (slskd timeout: {timeout}s)") + search_data = { 'searchText': query, 'timeout': timeout * 1000, # slskd expects milliseconds @@ -645,13 +650,24 @@ class SoulseekClient: # Track this search as active self.active_searches[search_id] = True - + + # Get timeout buffer from config + from config.settings import config_manager + timeout_buffer = config_manager.get('soulseek.search_timeout_buffer', 15) + # Poll for results - process and emit results immediately when found all_responses = [] all_tracks = [] all_albums = [] - poll_interval = 1 # Check every 1.5 seconds for more responsive updates - max_polls = int(timeout / poll_interval) # 20 attempts over 30 seconds + poll_interval = 1 # Check every 1 second for responsive updates + + # IMPORTANT: Poll for LONGER than slskd searches to catch all results + # slskd timeout: how long slskd searches for + # polling timeout: how long WE wait for slskd to finish (with buffer) + polling_timeout = timeout + timeout_buffer + max_polls = int(polling_timeout / poll_interval) + + logger.info(f"Polling for up to {polling_timeout}s (slskd timeout: {timeout}s + buffer: {timeout_buffer}s)") for poll_count in range(max_polls): # Check if search was cancelled diff --git a/webui/index.html b/webui/index.html index e0915c41..84935967 100644 --- a/webui/index.html +++ b/webui/index.html @@ -2273,6 +2273,16 @@ +
+ + + How long to search for tracks (15-300 seconds) +
+
+ + + Extra time to wait for late results (5-60 seconds) +
diff --git a/webui/static/script.js b/webui/static/script.js index f743fb09..eff8eefd 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -1629,6 +1629,8 @@ async function loadSettingsData() { // Populate Soulseek settings document.getElementById('soulseek-url').value = settings.soulseek?.slskd_url || ''; document.getElementById('soulseek-api-key').value = settings.soulseek?.api_key || ''; + document.getElementById('soulseek-search-timeout').value = settings.soulseek?.search_timeout || 60; + document.getElementById('soulseek-search-timeout-buffer').value = settings.soulseek?.search_timeout_buffer || 15; // Populate ListenBrainz settings document.getElementById('listenbrainz-token').value = settings.listenbrainz?.token || ''; @@ -2004,7 +2006,9 @@ async function saveSettings() { slskd_url: document.getElementById('soulseek-url').value, api_key: document.getElementById('soulseek-api-key').value, download_path: document.getElementById('download-path').value, - transfer_path: document.getElementById('transfer-path').value + transfer_path: document.getElementById('transfer-path').value, + search_timeout: parseInt(document.getElementById('soulseek-search-timeout').value) || 60, + search_timeout_buffer: parseInt(document.getElementById('soulseek-search-timeout-buffer').value) || 15 }, listenbrainz: { token: document.getElementById('listenbrainz-token').value