Fix Hydrabase search types, ID routing, and plugin passthrough

- Use correct server request types: 'tracks', 'albums', 'artists',
  'artist.albums', 'album.tracks' (were singular, caused timeouts)
- Normalize artists to strings (server may send dicts)
- Use native plugin IDs (iTunes/Spotify) instead of soul_id for
  album/artist/track IDs so downstream endpoints can resolve them
- Carry soul_id and plugin_id in external_urls for routing
- Pass plugin param from frontend to server for correct client routing
  (iTunes vs Deezer vs Spotify) with isdigit() fallback
- Route source=hydrabase to iTunes client for artist images
- Include external_urls in enhanced search API response
- Reduce WebSocket timeout from 15s to 8s
pull/253/head
Broque Thomas 2 months ago
parent a4f0745547
commit ee3500242e

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

@ -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 = []

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

Loading…
Cancel
Save