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
This commit is contained in:
parent
a4f0745547
commit
ee3500242e
3 changed files with 123 additions and 31 deletions
|
|
@ -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…
Reference in a new issue