diff --git a/config/settings.py b/config/settings.py index 29acb1e2..6b59d565 100644 --- a/config/settings.py +++ b/config/settings.py @@ -366,8 +366,9 @@ class ConfigManager: }, "download_source": { "mode": "soulseek", # Options: "soulseek", "youtube", "tidal", "qobuz", "hifi", "hybrid" - "hybrid_primary": "soulseek", # Which source to try first in hybrid mode - "hybrid_secondary": "youtube", # Fallback source if primary finds nothing + "hybrid_primary": "soulseek", # Legacy: primary source for hybrid mode + "hybrid_secondary": "youtube", # Legacy: fallback source for hybrid mode + "hybrid_order": [], # Ordered list of sources for hybrid mode (overrides primary/secondary) }, "tidal_download": { "quality": "lossless", # Options: "low", "high", "lossless", "hires" diff --git a/core/download_orchestrator.py b/core/download_orchestrator.py index afe475d8..e205d891 100644 --- a/core/download_orchestrator.py +++ b/core/download_orchestrator.py @@ -46,16 +46,21 @@ class DownloadOrchestrator: self.mode = config_manager.get('download_source.mode', 'soulseek') self.hybrid_primary = config_manager.get('download_source.hybrid_primary', 'soulseek') self.hybrid_secondary = config_manager.get('download_source.hybrid_secondary', 'youtube') + self.hybrid_order = config_manager.get('download_source.hybrid_order', []) logger.info(f"🎛️ Download Orchestrator initialized - Mode: {self.mode}") if self.mode == 'hybrid': - logger.info(f" Primary: {self.hybrid_primary}, Fallback: {self.hybrid_secondary}") + if self.hybrid_order: + logger.info(f" Source priority: {' → '.join(self.hybrid_order)}") + else: + logger.info(f" Primary: {self.hybrid_primary}, Fallback: {self.hybrid_secondary}") def reload_settings(self): """Reload settings from config (call after settings change)""" self.mode = config_manager.get('download_source.mode', 'soulseek') self.hybrid_primary = config_manager.get('download_source.hybrid_primary', 'soulseek') self.hybrid_secondary = config_manager.get('download_source.hybrid_secondary', 'youtube') + self.hybrid_order = config_manager.get('download_source.hybrid_order', []) # Reload underlying client configs (SLSKD URL, API key, etc.) self.soulseek._setup_client() @@ -80,7 +85,9 @@ class DownloadOrchestrator: elif self.mode == 'hifi': return self.hifi.is_configured() elif self.mode == 'hybrid': - return self.soulseek.is_configured() or self.youtube.is_configured() or self.tidal.is_configured() or self.qobuz.is_configured() or self.hifi.is_configured() + clients = {'soulseek': self.soulseek, 'youtube': self.youtube, 'tidal': self.tidal, 'qobuz': self.qobuz, 'hifi': self.hifi} + sources = self.hybrid_order if self.hybrid_order else [self.hybrid_primary, self.hybrid_secondary] + return any(clients[s].is_configured() for s in sources if s in clients) return False @@ -153,30 +160,39 @@ class DownloadOrchestrator: 'qobuz': self.qobuz, 'hifi': self.hifi, } - primary = self.hybrid_primary if self.hybrid_primary in clients else 'soulseek' - secondary = self.hybrid_secondary if self.hybrid_secondary in clients else 'soulseek' - # Ensure secondary differs from primary - if secondary == primary: - # Pick first available source that isn't primary - secondary = next((name for name in clients if name != primary), 'soulseek') + # Build ordered source list: prefer hybrid_order, fall back to legacy primary/secondary + if self.hybrid_order: + source_order = [s for s in self.hybrid_order if s in clients] + else: + primary = self.hybrid_primary if self.hybrid_primary in clients else 'soulseek' + secondary = self.hybrid_secondary if self.hybrid_secondary in clients else 'soulseek' + if secondary == primary: + secondary = next((name for name in clients if name != primary), 'soulseek') + source_order = [primary, secondary] - # Try primary source first - logger.info(f"🔍 Hybrid search - trying {primary} first: {query}") - tracks, albums = await clients[primary].search(query, timeout, progress_callback) - if tracks: - logger.info(f"✅ {primary} found {len(tracks)} tracks") - return (tracks, albums) + if not source_order: + source_order = ['soulseek'] - # Try secondary fallback - logger.info(f"🔄 {primary} found nothing, trying {secondary} fallback") - tracks, albums = await clients[secondary].search(query, timeout, progress_callback) - if tracks: - logger.info(f"✅ {secondary} found {len(tracks)} tracks") - return (tracks, albums) + logger.info(f"🔍 Hybrid search ({' → '.join(source_order)}): {query}") - # Nothing found from either source - logger.warning(f"❌ Hybrid search: both {primary} and {secondary} found nothing for: {query}") + # Try each source in priority order + for i, source_name in enumerate(source_order): + try: + if i == 0: + logger.info(f"🔍 Trying {source_name} (priority {i+1}): {query}") + else: + logger.info(f"🔄 Trying {source_name} (priority {i+1}): {query}") + + tracks, albums = await clients[source_name].search(query, timeout, progress_callback) + if tracks: + logger.info(f"✅ {source_name} found {len(tracks)} tracks") + return (tracks, albums) + except Exception as e: + logger.warning(f"⚠️ {source_name} search failed: {e}") + + # Nothing found from any source + logger.warning(f"❌ Hybrid search: all sources ({', '.join(source_order)}) found nothing for: {query}") return ([], []) # Fallback: empty results diff --git a/web_server.py b/web_server.py index 276d2b56..f2cd2b9a 100644 --- a/web_server.py +++ b/web_server.py @@ -6997,7 +6997,9 @@ def stream_enhanced_search_track(): search_queries = [] import re - if download_mode in ('youtube', 'tidal', 'qobuz', 'hifi') or (download_mode == 'hybrid' and config_manager.get('download_source.hybrid_primary') in ('youtube', 'tidal', 'qobuz', 'hifi')): + _hybrid_order = config_manager.get('download_source.hybrid_order', []) + _hybrid_first = _hybrid_order[0] if _hybrid_order else config_manager.get('download_source.hybrid_primary', 'soulseek') + if download_mode in ('youtube', 'tidal', 'qobuz', 'hifi') or (download_mode == 'hybrid' and _hybrid_first in ('youtube', 'tidal', 'qobuz', 'hifi')): # YouTube/Tidal mode: Include artist for better context # Primary query: Artist + Track if artist_name and track_name: @@ -21876,8 +21878,10 @@ def _run_full_missing_tracks_process(batch_id, playlist_id, tracks_json): preflight_source = None preflight_tracks = None dl_source_mode = config_manager.get('download_source.mode', 'soulseek') + _dl_hybrid_order = config_manager.get('download_source.hybrid_order', []) + _dl_hybrid_first = _dl_hybrid_order[0] if _dl_hybrid_order else config_manager.get('download_source.hybrid_primary', 'soulseek') soulseek_is_source = dl_source_mode == 'soulseek' or ( - dl_source_mode == 'hybrid' and config_manager.get('download_source.hybrid_primary', 'soulseek') == 'soulseek' + dl_source_mode == 'hybrid' and _dl_hybrid_first == 'soulseek' ) if batch_is_album and batch_album_context and batch_artist_context and soulseek_is_source: artist_name = batch_artist_context.get('name', '') diff --git a/webui/index.html b/webui/index.html index 8d3a0fe5..3e0c0f09 100644 --- a/webui/index.html +++ b/webui/index.html @@ -3875,33 +3875,15 @@