diff --git a/core/hydrabase_client.py b/core/hydrabase_client.py index ed3000d7..7c724aa7 100644 --- a/core/hydrabase_client.py +++ b/core/hydrabase_client.py @@ -32,7 +32,7 @@ class HydrabaseClient: Same callable used by HydrabaseWorker. """ self.get_ws_and_lock = get_ws_and_lock - self.timeout = 15 # seconds + self.timeout = 8 # seconds self.last_peer_count = None self.last_peer_count_time = None @@ -121,19 +121,21 @@ class HydrabaseClient: # Response has our nonce — definitely ours if data.get('nonce') == nonce: results = self._extract_results(data) + logger.debug(f"Hydrabase matched nonce for ({request_type}, '{query}'): {len(results) if results else 0} results") return results if results is not None else [] # Response has results but no nonce (server doesn't echo nonces) if 'nonce' not in data: results = self._extract_results(data) if results is not None: + logger.debug(f"Hydrabase no-nonce results for ({request_type}, '{query}'): {len(results)} results") return results # Stats-only message with no nonce — skip and recv again logger.debug(f"Hydrabase draining non-result message for ({request_type}, '{query}')") continue # Has a nonce but not ours — stale response, skip it - logger.debug(f"Hydrabase draining stale nonce response for ({request_type}, '{query}')") + logger.debug(f"Hydrabase draining stale nonce {data.get('nonce')} (ours={nonce}) for ({request_type}, '{query}')") except Exception as e: logger.error(f"Hydrabase query failed ({request_type}, '{query}'): {e}") @@ -166,13 +168,18 @@ class HydrabaseClient: # ==================== Track Methods ==================== def search_tracks(self, query: str, limit: int = 20) -> List[Track]: - results = self._send_and_recv('track', query) + results = self._send_and_recv('tracks', query) if not results: return [] tracks = [] for item in results[:limit]: try: + ext_urls = dict(item.get('external_urls', {}) or {}) + if item.get('soul_id'): + ext_urls['hydrabase_soul_id'] = str(item['soul_id']) + if item.get('plugin_id'): + ext_urls['hydrabase_plugin'] = item['plugin_id'].lower() tracks.append(Track( id=str(item.get('id', '')), name=item.get('name', ''), @@ -181,7 +188,7 @@ class HydrabaseClient: duration_ms=item.get('duration_ms', 0), popularity=item.get('popularity', 0), preview_url=item.get('preview_url'), - external_urls=item.get('external_urls'), + external_urls=ext_urls, image_url=item.get('image_url'), release_date=self._normalize_release_date(item.get('release_date', '')) )) @@ -199,6 +206,11 @@ class HydrabaseClient: artists = [] for item in results[:limit]: try: + ext_urls = dict(item.get('external_urls', {}) or {}) + if item.get('soul_id'): + ext_urls['hydrabase_soul_id'] = str(item['soul_id']) + if item.get('plugin_id'): + ext_urls['hydrabase_plugin'] = item['plugin_id'].lower() artists.append(Artist( id=str(item.get('id', '')), name=item.get('name', ''), @@ -206,7 +218,7 @@ class HydrabaseClient: genres=item.get('genres', []), followers=item.get('followers', 0), image_url=item.get('image_url'), - external_urls=item.get('external_urls') + external_urls=ext_urls )) except Exception as e: logger.debug(f"Skipping malformed Hydrabase artist: {e}") @@ -215,22 +227,29 @@ class HydrabaseClient: # ==================== Album Methods ==================== def search_albums(self, query: str, limit: int = 20) -> List[Album]: - results = self._send_and_recv('album', query) + results = self._send_and_recv('albums', query) if not results: return [] albums = [] for item in results[:limit]: try: + # Use the plugin's native ID (iTunes/Spotify) so downstream + # endpoints can look it up. Carry soul_id in external_urls + # for Hydrabase-specific lookups (album.tracks). + ext_urls = dict(item.get('external_urls', {}) or {}) + soul_id = item.get('soul_id', '') + if soul_id: + ext_urls['hydrabase_soul_id'] = str(soul_id) albums.append(Album( - id=str(item.get('soul_id', item.get('id', ''))), + id=str(item.get('id', soul_id)), name=item.get('name', ''), artists=self._normalize_artists(item.get('artists', [])), release_date=self._normalize_release_date(item.get('release_date', '')), total_tracks=item.get('total_tracks', 0), album_type=item.get('album_type', 'album'), image_url=item.get('image_url'), - external_urls=item.get('external_urls') + external_urls=ext_urls )) except Exception as e: logger.debug(f"Skipping malformed Hydrabase album: {e}") @@ -240,22 +259,28 @@ class HydrabaseClient: def search_discography(self, artist_name: str, limit: int = 50) -> List[Album]: """Fetch an artist's discography (albums + singles) from Hydrabase.""" - results = self._send_and_recv('discography', artist_name) + results = self._send_and_recv('artist.albums', artist_name) + if not results: + results = self._send_and_recv('discography', artist_name) if not results: return [] albums = [] for item in results[:limit]: try: + ext_urls = dict(item.get('external_urls', {}) or {}) + soul_id = item.get('soul_id', '') + if soul_id: + ext_urls['hydrabase_soul_id'] = str(soul_id) albums.append(Album( - id=str(item.get('soul_id', item.get('id', ''))), + id=str(item.get('id', soul_id)), name=item.get('name', ''), artists=self._normalize_artists(item.get('artists', [])), release_date=self._normalize_release_date(item.get('release_date', '')), total_tracks=item.get('total_tracks', 0), album_type=item.get('album_type', 'album'), image_url=item.get('image_url'), - external_urls=item.get('external_urls') + external_urls=ext_urls )) except Exception as e: logger.debug(f"Skipping malformed Hydrabase discography album: {e}") @@ -272,8 +297,7 @@ class HydrabaseClient: """ results = self._send_and_recv('track.details', track_id) if not results: - # Fallback: search for the track ID directly - results = self._send_and_recv('track', track_id) + results = self._send_and_recv('tracks', track_id) if not results: return None @@ -320,7 +344,7 @@ class HydrabaseClient: """ results = self._send_and_recv('album.get', album_id) if not results: - results = self._send_and_recv('album', album_id) + results = self._send_and_recv('albums', album_id) if not results: return None @@ -516,15 +540,19 @@ class HydrabaseClient: item_type = item.get('album_type', 'album') if type_filter and item_type not in type_filter: continue + ext_urls = dict(item.get('external_urls', {}) or {}) + soul_id = item.get('soul_id', '') + if soul_id: + ext_urls['hydrabase_soul_id'] = str(soul_id) albums.append(Album( - id=str(item.get('soul_id', item.get('id', ''))), + id=str(item.get('id', soul_id)), name=item.get('name', ''), artists=self._normalize_artists(item.get('artists', [])), release_date=self._normalize_release_date(item.get('release_date', '')), total_tracks=item.get('total_tracks', 0), album_type=item_type, image_url=item.get('image_url'), - external_urls=item.get('external_urls'), + external_urls=ext_urls, )) except Exception as e: logger.debug(f"Skipping malformed Hydrabase artist album: {e}") diff --git a/web_server.py b/web_server.py index 89359f84..b6b22048 100644 --- a/web_server.py +++ b/web_server.py @@ -6982,8 +6982,8 @@ def enhanced_search(): if primary_source != "hydrabase": # Mirror to Hydrabase worker (fire-and-forget) if hydrabase_worker and dev_mode_enabled: - hydrabase_worker.enqueue(query, 'track') - hydrabase_worker.enqueue(query, 'album') + hydrabase_worker.enqueue(query, 'tracks') + hydrabase_worker.enqueue(query, 'albums') hydrabase_worker.enqueue(query, 'artists') # Search primary source synchronously — use is_spotify_authenticated() @@ -7051,7 +7051,8 @@ def _enhanced_search_source(query, client): artists.append({ "id": artist.id, "name": artist.name, - "image_url": artist.image_url + "image_url": artist.image_url, + "external_urls": artist.external_urls or {}, }) except Exception as e: logger.debug(f"Artist search failed for {type(client).__name__}: {e}") @@ -7067,10 +7068,11 @@ def _enhanced_search_source(query, client): "image_url": album.image_url, "release_date": album.release_date, "total_tracks": album.total_tracks, - "album_type": album.album_type + "album_type": album.album_type, + "external_urls": album.external_urls or {}, }) except Exception as e: - logger.debug(f"Album search failed for {type(client).__name__}: {e}") + logger.warning(f"Album search failed for {type(client).__name__}: {e}", exc_info=True) try: track_objs = client.search_tracks(query, limit=10) @@ -7083,10 +7085,11 @@ def _enhanced_search_source(query, client): "album": track.album, "duration_ms": track.duration_ms, "image_url": track.image_url, - "release_date": track.release_date + "release_date": track.release_date, + "external_urls": track.external_urls or {}, }) except Exception as e: - logger.debug(f"Track search failed for {type(client).__name__}: {e}") + logger.warning(f"Track search failed for {type(client).__name__}: {e}", exc_info=True) return {"artists": artists, "albums": albums, "tracks": tracks, "available": True} @@ -8936,6 +8939,19 @@ def get_artist_image(artist_id): client = _get_deezer_client() image_url = client._get_artist_image_from_albums(artist_id) return jsonify({"success": True, "image_url": image_url}) + elif source_override == 'hydrabase': + # Route to the plugin that sourced the data + plugin = request.args.get('plugin', '').lower() + if plugin == 'deezer': + client = _get_deezer_client() + image_url = client._get_artist_image_from_albums(artist_id) + elif plugin == 'itunes' or artist_id.isdigit(): + from core.itunes_client import iTunesClient + client = iTunesClient() + image_url = client._get_artist_image_from_albums(artist_id) + else: + image_url = None + return jsonify({"success": True, "image_url": image_url}) elif spotify_client and spotify_client.is_spotify_authenticated() and source_override != 'itunes': # Use Spotify directly artist_data = spotify_client.sp.artist(artist_id) @@ -8963,7 +8979,7 @@ def get_artist_discography(artist_id): # Mirror to Hydrabase P2P network if hydrabase_worker and dev_mode_enabled and artist_name: - hydrabase_worker.enqueue(artist_name, 'discography') + hydrabase_worker.enqueue(artist_name, 'artist.albums') # Determine which source to use spotify_available = spotify_client and spotify_client.is_spotify_authenticated() @@ -8995,12 +9011,31 @@ def get_artist_discography(artist_id): albums = deezer_cl.get_artist_albums(artist_id, album_type='album,single', limit=50) if albums: active_source = 'deezer' + elif source_override == 'hydrabase': + plugin = request.args.get('plugin', '').lower() + if plugin == 'deezer': + hb_cl = _get_deezer_client() + elif plugin == 'itunes' or artist_id.isdigit(): + from core.itunes_client import iTunesClient + hb_cl = iTunesClient() + else: + hb_cl = spotify_client + albums = hb_cl.get_artist_albums(artist_id, album_type='album,single', limit=50) + if albums: + active_source = plugin or 'hydrabase' # If direct ID lookup failed but we have artist name, search by name if not albums and artist_name: if source_override == 'itunes': from core.itunes_client import iTunesClient cl = iTunesClient() + elif source_override == 'hydrabase': + plugin = request.args.get('plugin', '').lower() + if plugin == 'deezer': + cl = _get_deezer_client() + else: + from core.itunes_client import iTunesClient + cl = iTunesClient() elif source_override == 'deezer': cl = _get_deezer_client() elif source_override == 'spotify' and spotify_available: @@ -9263,6 +9298,13 @@ def get_artist_album_tracks(artist_id, album_id): if source_override == 'itunes': from core.itunes_client import iTunesClient client = iTunesClient() + elif source_override == 'hydrabase': + plugin = request.args.get('plugin', '').lower() + if plugin == 'deezer': + client = _get_deezer_client() + elif plugin == 'itunes' or album_id.isdigit(): + from core.itunes_client import iTunesClient + client = iTunesClient() elif source_override == 'deezer': client = _get_deezer_client() @@ -25939,6 +25981,16 @@ def get_album_tracks(album_id): if source_override == 'itunes': from core.itunes_client import iTunesClient client = iTunesClient() + elif source_override == 'hydrabase': + # Hydrabase IDs originate from whichever plugin the peer runs. + # 'plugin' param is authoritative; fall back to ID format detection. + plugin = request.args.get('plugin', '').lower() + if plugin == 'itunes' or (not plugin and album_id.isdigit()): + from core.itunes_client import iTunesClient + client = iTunesClient() + elif plugin == 'deezer': + client = _get_deezer_client() + # else: spotify (default) elif source_override == 'deezer': client = _get_deezer_client() @@ -26082,7 +26134,7 @@ def search_spotify_tracks(): tracks = hydrabase_client.search_tracks(query, limit=limit) else: if hydrabase_worker and dev_mode_enabled: - hydrabase_worker.enqueue(query, 'track') + hydrabase_worker.enqueue(query, 'tracks') tracks = spotify_client.search_tracks(query, limit=limit) tracks_dict = [{ @@ -26128,7 +26180,7 @@ def search_itunes_tracks(): source = 'hydrabase' else: if hydrabase_worker and dev_mode_enabled: - hydrabase_worker.enqueue(query, 'track') + hydrabase_worker.enqueue(query, 'tracks') fallback_client = _get_metadata_fallback_client() tracks = fallback_client.search_tracks(query, limit=limit) source = _get_metadata_fallback_source() @@ -41789,7 +41841,7 @@ def import_search_albums(): albums = hydrabase_client.search_albums(query, limit=limit) else: if hydrabase_worker and dev_mode_enabled: - hydrabase_worker.enqueue(query, 'album') + hydrabase_worker.enqueue(query, 'albums') albums = spotify_client.search_albums(query, limit=limit) results = [] @@ -42148,7 +42200,7 @@ def import_search_tracks(): tracks = hydrabase_client.search_tracks(query, limit=limit) else: if hydrabase_worker and dev_mode_enabled: - hydrabase_worker.enqueue(query, 'track') + hydrabase_worker.enqueue(query, 'tracks') tracks = spotify_client.search_tracks(query, limit=limit) results = [] diff --git a/webui/static/script.js b/webui/static/script.js index c7f33915..67cb12da 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -7681,7 +7681,10 @@ function initializeSearchModeToggle() { await new Promise(resolve => setTimeout(resolve, 100)); // Load the artist details with source context - await selectArtistForDetail(artist, { source: sourceOverride }); + await selectArtistForDetail(artist, { + source: sourceOverride, + plugin: artist.external_urls?.hydrabase_plugin, + }); } }) ); @@ -8015,6 +8018,10 @@ function initializeSearchModeToggle() { if (_activeSearchSource && _activeSearchSource !== 'spotify') { albumParams.set('source', _activeSearchSource); } + // Pass Hydrabase plugin origin so server routes to correct client + if (album.external_urls?.hydrabase_plugin) { + albumParams.set('plugin', album.external_urls.hydrabase_plugin); + } const response = await fetch(`/api/spotify/album/${album.id}?${albumParams}`); if (!response.ok) { @@ -31699,6 +31706,7 @@ async function selectArtistForDetail(artist, options = {}) { artistsPageState.selectedArtist = artist; artistsPageState.currentView = 'detail'; artistsPageState.sourceOverride = options.source || null; + artistsPageState.pluginOverride = options.plugin || null; // Show detail state showArtistDetailState(); @@ -31707,7 +31715,7 @@ async function selectArtistForDetail(artist, options = {}) { updateArtistDetailHeader(artist); // Load discography (pass artist name for cross-source fallback) - await loadArtistDiscography(artist.id, artist.name, options.source); + await loadArtistDiscography(artist.id, artist.name, options.source, options.plugin); } /** @@ -31715,7 +31723,7 @@ async function selectArtistForDetail(artist, options = {}) { * @param {string} artistId - Artist ID (Spotify or iTunes format) * @param {string} [artistName] - Optional artist name for fallback searches */ -async function loadArtistDiscography(artistId, artistName = null, sourceOverride = null) { +async function loadArtistDiscography(artistId, artistName = null, sourceOverride = null, pluginOverride = null) { console.log(`💿 Loading discography for artist: ${artistId} (name: ${artistName}, source: ${sourceOverride || 'auto'})`); // Use source-prefixed cache key to avoid ID collisions between sources @@ -31746,6 +31754,7 @@ async function loadArtistDiscography(artistId, artistName = null, sourceOverride const params = new URLSearchParams(); if (artistName) params.set('artist_name', artistName); if (sourceOverride) params.set('source', sourceOverride); + if (pluginOverride) params.set('plugin', pluginOverride); if (params.toString()) url += `?${params.toString()}`; // Call the real API endpoint @@ -33196,6 +33205,9 @@ async function createArtistAlbumVirtualPlaylist(album, albumType) { if (artistsPageState.sourceOverride) { _aat1.set('source', artistsPageState.sourceOverride); } + if (artistsPageState.pluginOverride) { + _aat1.set('plugin', artistsPageState.pluginOverride); + } const response = await fetch(`/api/artist/${artist.id}/album/${album.id}/tracks?${_aat1}`); if (!response.ok) {