From b9c83a50fa96c81b302dbcb29cea3ca7c4bc6034 Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Thu, 19 Mar 2026 11:47:37 -0700 Subject: [PATCH] Add Soulseek peer queue filtering and configurable download timeout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add max_peer_queue setting to skip peers with long queues (soft filter with fallback to unfiltered if all results removed) - Add download_timeout setting replacing hardcoded 10-minute limit - Include quality_score (peer health: upload speed, free slots, queue length) in result ranking — was calculated but never used in sort key - New UI controls in Soulseek settings section --- config/settings.py | 4 +++- core/matching_engine.py | 25 +++++++++++++++++++------ web_server.py | 11 +++++++---- webui/index.html | 18 ++++++++++++++++++ webui/static/script.js | 6 +++++- 5 files changed, 52 insertions(+), 12 deletions(-) diff --git a/config/settings.py b/config/settings.py index f7059b19..29acb1e2 100644 --- a/config/settings.py +++ b/config/settings.py @@ -360,7 +360,9 @@ class ConfigManager: "slskd_url": "", "api_key": "", "download_path": "./downloads", - "transfer_path": "./Transfer" + "transfer_path": "./Transfer", + "max_peer_queue": 0, + "download_timeout": 600 }, "download_source": { "mode": "soulseek", # Options: "soulseek", "youtube", "tidal", "qobuz", "hifi", "hybrid" diff --git a/core/matching_engine.py b/core/matching_engine.py index 905a1d7a..ac5837c3 100644 --- a/core/matching_engine.py +++ b/core/matching_engine.py @@ -867,32 +867,45 @@ class MusicMatchingEngine: return adjusted_confidence, version_type - def find_best_slskd_matches_enhanced(self, spotify_track: SpotifyTrack, slskd_results: List[TrackResult]) -> List[TrackResult]: + def find_best_slskd_matches_enhanced(self, spotify_track: SpotifyTrack, slskd_results: List[TrackResult], + max_peer_queue: int = 0) -> List[TrackResult]: """ Enhanced version of find_best_slskd_matches with version-aware scoring. Returns candidates sorted by adjusted confidence (preferring originals). + + Args: + max_peer_queue: Skip peers with queue longer than this (0 = no limit) """ if not slskd_results: return [] + # Apply queue filter if configured + if max_peer_queue > 0: + filtered = [r for r in slskd_results if r.queue_length <= max_peer_queue] + # Fall back to unfiltered if everything got removed (rare files) + if filtered: + slskd_results = filtered + scored_results = [] for slskd_track in slskd_results: # Use enhanced confidence calculation confidence, version_type = self.calculate_slskd_match_confidence_enhanced(spotify_track, slskd_track) - + # Store the adjusted confidence and version info slskd_track.confidence = confidence slskd_track.version_type = getattr(slskd_track, 'version_type', 'original') scored_results.append(slskd_track) - # Sort by confidence score (descending), then by version preference, then by size + # Sort by confidence, version preference, peer quality, then file size def sort_key(r): # Primary: confidence score # Secondary: prefer originals (original=0, others=penalty value for tie-breaking) version_priority = 0.0 if r.version_type == 'original' else getattr(r, 'version_penalty', 0.1) - # Tertiary: file size - return (r.confidence, -version_priority, r.size) - + # Tertiary: peer quality (upload speed, queue, free slots) + peer_quality = r.quality_score + # Quaternary: file size + return (r.confidence, -version_priority, peer_quality, r.size) + sorted_results = sorted(scored_results, key=sort_key, reverse=True) # Filter out very low-confidence results diff --git a/web_server.py b/web_server.py index fa32cc4b..1c11803c 100644 --- a/web_server.py +++ b/web_server.py @@ -6993,7 +6993,8 @@ def stream_enhanced_search_track(): logger.info(f"✅ Found {len(tracks_result)} results for query: '{query}'") # Use matching engine to find best match - best_matches = matching_engine.find_best_slskd_matches_enhanced(temp_track, tracks_result) + _max_q = config_manager.get('soulseek.max_peer_queue', 0) or 0 + best_matches = matching_engine.find_best_slskd_matches_enhanced(temp_track, tracks_result, max_peer_queue=_max_q) if best_matches: # Get the first (best) result @@ -20683,7 +20684,8 @@ def get_valid_candidates(results, spotify_track, query): if not results: return [] # Uses the existing, powerful matching engine for scoring - initial_candidates = matching_engine.find_best_slskd_matches_enhanced(spotify_track, results) + _max_q = config_manager.get('soulseek.max_peer_queue', 0) or 0 + initial_candidates = matching_engine.find_best_slskd_matches_enhanced(spotify_track, results, max_peer_queue=_max_q) if not initial_candidates: return [] @@ -23318,8 +23320,9 @@ def _build_batch_status_data(batch_id, batch, live_transfers_lookup): task_start_time = task.get('status_change_time', current_time) task_age = current_time - task_start_time - # If task has been running for more than 10 minutes, check if file completed - if task_age > 600 and task['status'] in ['downloading', 'queued', 'searching']: + # If task has been running too long, check if file completed + _dl_timeout = config_manager.get('soulseek.download_timeout', 600) or 600 + if task_age > _dl_timeout and task['status'] in ['downloading', 'queued', 'searching']: stuck_state = task['status'] task_filename = task.get('filename') or (task.get('track_info') or {}).get('filename') diff --git a/webui/index.html b/webui/index.html index 5a5fb157..c35c7397 100644 --- a/webui/index.html +++ b/webui/index.html @@ -3934,6 +3934,24 @@ Ignore search results from peers with upload speed below this threshold +
+ + + Skip peers with a queue longer than this (prefers peers with shorter queues regardless) +
+
+ + + Abandon stuck downloads after this many minutes +
diff --git a/webui/static/script.js b/webui/static/script.js index ce33955f..f4360a17 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -4916,6 +4916,8 @@ async function loadSettingsData() { document.getElementById('soulseek-search-timeout').value = settings.soulseek?.search_timeout || 60; document.getElementById('soulseek-search-timeout-buffer').value = settings.soulseek?.search_timeout_buffer || 15; document.getElementById('soulseek-min-peer-speed').value = settings.soulseek?.min_peer_upload_speed || 0; + document.getElementById('soulseek-max-peer-queue').value = settings.soulseek?.max_peer_queue || 0; + document.getElementById('soulseek-download-timeout').value = Math.round((settings.soulseek?.download_timeout || 600) / 60); // Populate ListenBrainz settings document.getElementById('listenbrainz-base-url').value = settings.listenbrainz?.base_url || ''; @@ -5826,7 +5828,9 @@ async function saveSettings(quiet = false) { 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, - min_peer_upload_speed: parseInt(document.getElementById('soulseek-min-peer-speed').value) || 0 + min_peer_upload_speed: parseInt(document.getElementById('soulseek-min-peer-speed').value) || 0, + max_peer_queue: parseInt(document.getElementById('soulseek-max-peer-queue').value) || 0, + download_timeout: (parseInt(document.getElementById('soulseek-download-timeout').value) || 10) * 60 }, listenbrainz: { base_url: document.getElementById('listenbrainz-base-url').value,