Add Soulseek peer queue filtering and configurable download timeout

- 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
This commit is contained in:
Broque Thomas 2026-03-19 11:47:37 -07:00
parent f0270ce7a5
commit b9c83a50fa
5 changed files with 52 additions and 12 deletions

View file

@ -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"

View file

@ -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

View file

@ -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')

View file

@ -3934,6 +3934,24 @@
</select>
<small class="settings-hint">Ignore search results from peers with upload speed below this threshold</small>
</div>
<div class="form-group">
<label>Max Peer Queue Length:</label>
<select id="soulseek-max-peer-queue">
<option value="0">No limit</option>
<option value="5">5</option>
<option value="10">10</option>
<option value="20">20</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
<small class="settings-hint">Skip peers with a queue longer than this (prefers peers with shorter queues regardless)</small>
</div>
<div class="form-group">
<label>Download Timeout (minutes):</label>
<input type="number" id="soulseek-download-timeout" placeholder="10" min="2"
max="60" value="10">
<small class="settings-hint">Abandon stuck downloads after this many minutes</small>
</div>
<div class="form-actions" style="margin-top: 8px;">
<button class="test-button" onclick="testConnection('soulseek')">Test Soulseek Connection</button>
</div>

View file

@ -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,