Hydrabase: improve parsing add discography/tracks
Improve Hydrabase response handling and add discography/album track helpers. core/hydrabase_client.py: extract peer counts from stats.connectedPeers, handle new "response" key and stats-only or unexpected response shapes (return empty instead of wrapping), and add search_discography() and get_album_tracks() to map Hydrabase results into Album/Track objects. web_server.py: avoid redundant Hydrabase round-trips by passing precomputed hydrabase_counts into the background comparison worker; prefer Hydrabase for artist discography and album track import when active (with Spotify fallback); and route album-context searches to Hydrabase when configured. These changes reduce duplicate network calls and improve robustness against varied Hydrabase payloads.
This commit is contained in:
parent
44297a210b
commit
f90d6567f7
3 changed files with 229 additions and 62 deletions
|
|
@ -70,21 +70,35 @@ class HydrabaseClient:
|
|||
|
||||
data = json.loads(raw)
|
||||
|
||||
# Check for peer_count in response (future stats messages)
|
||||
if isinstance(data, dict) and 'peer_count' in data:
|
||||
import time
|
||||
self.last_peer_count = data['peer_count']
|
||||
self.last_peer_count_time = time.time()
|
||||
# Extract stats if present (can appear alongside results)
|
||||
if isinstance(data, dict) and 'stats' in data:
|
||||
stats = data['stats']
|
||||
if isinstance(stats, dict) and 'connectedPeers' in stats:
|
||||
import time
|
||||
self.last_peer_count = stats['connectedPeers']
|
||||
self.last_peer_count_time = time.time()
|
||||
|
||||
# Handle various response shapes
|
||||
if isinstance(data, list):
|
||||
return data
|
||||
if isinstance(data, dict) and 'results' in data:
|
||||
return data['results']
|
||||
if isinstance(data, dict) and 'data' in data:
|
||||
result = data['data']
|
||||
return result if isinstance(result, list) else [result]
|
||||
return [data] if data else []
|
||||
if isinstance(data, dict):
|
||||
# Hydrabase returns results under "response" key
|
||||
if 'response' in data:
|
||||
resp = data['response']
|
||||
return resp if isinstance(resp, list) else [resp]
|
||||
if 'results' in data:
|
||||
return data['results']
|
||||
if 'data' in data:
|
||||
result = data['data']
|
||||
return result if isinstance(result, list) else [result]
|
||||
# Stats-only or empty response — no search results
|
||||
if 'stats' in data:
|
||||
logger.debug(f"Hydrabase stats-only response for ({request_type}, '{query}')")
|
||||
return []
|
||||
|
||||
# Unknown shape — return empty rather than wrapping garbage
|
||||
logger.debug(f"Hydrabase unexpected response shape for ({request_type}, '{query}'): {list(data.keys()) if isinstance(data, dict) else type(data).__name__}")
|
||||
return []
|
||||
except Exception as e:
|
||||
logger.error(f"Hydrabase query failed ({request_type}, '{query}'): {e}")
|
||||
return None
|
||||
|
|
@ -171,6 +185,56 @@ class HydrabaseClient:
|
|||
logger.debug(f"Skipping malformed Hydrabase album: {e}")
|
||||
return albums
|
||||
|
||||
# ==================== Discography Methods ====================
|
||||
|
||||
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)
|
||||
if not results:
|
||||
return []
|
||||
|
||||
albums = []
|
||||
for item in results[:limit]:
|
||||
try:
|
||||
albums.append(Album(
|
||||
id=str(item.get('id', '')),
|
||||
name=item.get('name', ''),
|
||||
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')
|
||||
))
|
||||
except Exception as e:
|
||||
logger.debug(f"Skipping malformed Hydrabase discography album: {e}")
|
||||
return albums
|
||||
|
||||
def get_album_tracks(self, album_query: str, limit: int = 50) -> List[Track]:
|
||||
"""Fetch tracks for an album from Hydrabase using the album_tracks type."""
|
||||
results = self._send_and_recv('album_tracks', album_query)
|
||||
if not results:
|
||||
return []
|
||||
|
||||
tracks = []
|
||||
for item in results[:limit]:
|
||||
try:
|
||||
tracks.append(Track(
|
||||
id=str(item.get('id', '')),
|
||||
name=item.get('name', ''),
|
||||
artists=item.get('artists', []),
|
||||
album=item.get('album', ''),
|
||||
duration_ms=item.get('duration_ms', 0),
|
||||
popularity=item.get('popularity', 0),
|
||||
preview_url=item.get('preview_url'),
|
||||
external_urls=item.get('external_urls'),
|
||||
image_url=item.get('image_url'),
|
||||
release_date=self._normalize_release_date(item.get('release_date', ''))
|
||||
))
|
||||
except Exception as e:
|
||||
logger.debug(f"Skipping malformed Hydrabase album track: {e}")
|
||||
return tracks
|
||||
|
||||
# ==================== Raw access (for comparison) ====================
|
||||
|
||||
def search_raw(self, query: str, search_type: str) -> Optional[list]:
|
||||
|
|
|
|||
200
web_server.py
200
web_server.py
|
|
@ -2579,23 +2579,32 @@ def _is_hydrabase_active():
|
|||
except NameError:
|
||||
return False
|
||||
|
||||
def _run_background_comparison(query):
|
||||
"""Run Spotify + iTunes searches in background and store for comparison."""
|
||||
def _run_background_comparison(query, hydrabase_counts=None):
|
||||
"""Run Spotify + iTunes searches in background and store for comparison.
|
||||
|
||||
Args:
|
||||
query: Search query string.
|
||||
hydrabase_counts: Optional pre-computed dict {'tracks': N, 'artists': N, 'albums': N}
|
||||
from the primary search to avoid redundant Hydrabase round-trips.
|
||||
"""
|
||||
def _worker():
|
||||
try:
|
||||
result = {'timestamp': time.time(), 'query': query}
|
||||
|
||||
# Hydrabase raw results (already fetched for the primary search)
|
||||
hydra_data = {'tracks': 0, 'artists': 0, 'albums': 0}
|
||||
if hydrabase_client and hydrabase_client.is_connected():
|
||||
raw_t = hydrabase_client.search_raw(query, 'track')
|
||||
raw_ar = hydrabase_client.search_raw(query, 'artist')
|
||||
raw_al = hydrabase_client.search_raw(query, 'album')
|
||||
hydra_data = {
|
||||
'tracks': len(raw_t) if raw_t else 0,
|
||||
'artists': len(raw_ar) if raw_ar else 0,
|
||||
'albums': len(raw_al) if raw_al else 0
|
||||
}
|
||||
# Use pre-computed counts if available, otherwise fetch from Hydrabase
|
||||
if hydrabase_counts is not None:
|
||||
hydra_data = hydrabase_counts
|
||||
else:
|
||||
hydra_data = {'tracks': 0, 'artists': 0, 'albums': 0}
|
||||
if _is_hydrabase_active():
|
||||
raw_t = hydrabase_client.search_raw(query, 'track')
|
||||
raw_ar = hydrabase_client.search_raw(query, 'artist')
|
||||
raw_al = hydrabase_client.search_raw(query, 'album')
|
||||
hydra_data = {
|
||||
'tracks': len(raw_t) if raw_t else 0,
|
||||
'artists': len(raw_ar) if raw_ar else 0,
|
||||
'albums': len(raw_al) if raw_al else 0
|
||||
}
|
||||
result['hydrabase'] = hydra_data
|
||||
|
||||
# Spotify results
|
||||
|
|
@ -4178,8 +4187,12 @@ def enhanced_search():
|
|||
|
||||
logger.info(f"Hydrabase search results: {len(spotify_artists)} artists, {len(spotify_albums)} albums, {len(spotify_tracks)} tracks")
|
||||
|
||||
# Fire off background comparison
|
||||
_run_background_comparison(query)
|
||||
# Fire off background comparison with pre-computed Hydrabase counts
|
||||
_run_background_comparison(query, hydrabase_counts={
|
||||
'tracks': len(track_objs),
|
||||
'artists': len(artist_objs),
|
||||
'albums': len(album_objs)
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Hydrabase search failed, falling back to Spotify/iTunes: {e}")
|
||||
|
|
@ -5843,11 +5856,21 @@ def get_artist_discography(artist_id):
|
|||
albums = []
|
||||
active_source = None
|
||||
|
||||
# Try Hydrabase first when active
|
||||
if _is_hydrabase_active() and artist_name:
|
||||
try:
|
||||
albums = hydrabase_client.search_discography(artist_name, limit=50)
|
||||
if albums:
|
||||
active_source = 'hydrabase'
|
||||
print(f"📊 Got {len(albums)} albums from Hydrabase")
|
||||
except Exception as e:
|
||||
print(f"Hydrabase discography failed: {e}")
|
||||
|
||||
# Try to get albums from the appropriate source
|
||||
# Check if the ID looks like Spotify (alphanumeric) or iTunes (numeric only)
|
||||
is_numeric_id = artist_id.isdigit()
|
||||
|
||||
if spotify_available and not is_numeric_id:
|
||||
if not albums and spotify_available and not is_numeric_id:
|
||||
# Try Spotify first for alphanumeric IDs
|
||||
try:
|
||||
albums = spotify_client.get_artist_albums(artist_id, album_type='album,single')
|
||||
|
|
@ -7082,20 +7105,24 @@ def search_match():
|
|||
elif context == 'album':
|
||||
# Search for albums by specific artist
|
||||
artist_id = data.get('artist_id')
|
||||
if not artist_id:
|
||||
return jsonify({"error": "Artist ID required for album search"}), 400
|
||||
|
||||
# Get artist's albums and filter by query
|
||||
artist_albums = spotify_client.get_artist_albums(artist_id)
|
||||
|
||||
if use_hydrabase:
|
||||
# Hydrabase: search albums by query directly
|
||||
album_matches = hydrabase_client.search_albums(query, limit=20)
|
||||
else:
|
||||
if not artist_id:
|
||||
return jsonify({"error": "Artist ID required for album search"}), 400
|
||||
# Get artist's albums and filter by query
|
||||
album_matches = spotify_client.get_artist_albums(artist_id)
|
||||
|
||||
results = []
|
||||
|
||||
for album in artist_albums:
|
||||
for album in album_matches:
|
||||
# Calculate confidence based on query similarity
|
||||
confidence = matching_engine.similarity_score(
|
||||
matching_engine.normalize_string(query),
|
||||
matching_engine.normalize_string(album.name)
|
||||
)
|
||||
|
||||
|
||||
# Only include results with reasonable similarity
|
||||
if confidence > 0.3:
|
||||
results.append({
|
||||
|
|
@ -7109,7 +7136,7 @@ def search_match():
|
|||
},
|
||||
"confidence": confidence
|
||||
})
|
||||
|
||||
|
||||
# Sort by confidence
|
||||
results.sort(key=lambda x: x['confidence'], reverse=True)
|
||||
return jsonify({"results": results[:8]})
|
||||
|
|
@ -17284,6 +17311,42 @@ def get_playlist_tracks(playlist_id):
|
|||
@app.route('/api/spotify/album/<album_id>', methods=['GET'])
|
||||
def get_album_tracks(album_id):
|
||||
"""Fetches full track details for a specific album."""
|
||||
use_hydrabase = _is_hydrabase_active()
|
||||
|
||||
# Try Hydrabase first when active and album name provided
|
||||
if use_hydrabase:
|
||||
album_name = request.args.get('name', '')
|
||||
album_artist = request.args.get('artist', '')
|
||||
if album_name:
|
||||
try:
|
||||
query = f"{album_artist} {album_name}".strip() if album_artist else album_name
|
||||
hydra_tracks = hydrabase_client.get_album_tracks(query, limit=50)
|
||||
if hydra_tracks:
|
||||
track_items = []
|
||||
for t in hydra_tracks:
|
||||
artist_list = t.artists if isinstance(t.artists, list) else [t.artists] if t.artists else []
|
||||
track_items.append({
|
||||
'name': t.name,
|
||||
'track_number': getattr(t, 'track_number', 0) or 0,
|
||||
'disc_number': getattr(t, 'disc_number', 1) or 1,
|
||||
'duration_ms': t.duration_ms,
|
||||
'id': t.id,
|
||||
'artists': [{'name': a} if isinstance(a, str) else a for a in artist_list],
|
||||
'uri': ''
|
||||
})
|
||||
return jsonify({
|
||||
'id': album_id,
|
||||
'name': album_name,
|
||||
'artists': [{'name': album_artist}] if album_artist else [],
|
||||
'release_date': '',
|
||||
'total_tracks': len(track_items),
|
||||
'album_type': 'album',
|
||||
'images': [],
|
||||
'tracks': track_items
|
||||
})
|
||||
except Exception as e:
|
||||
logger.warning(f"Hydrabase album_tracks failed for '{album_name}', falling back to Spotify: {e}")
|
||||
|
||||
if not spotify_client or not spotify_client.is_authenticated():
|
||||
return jsonify({"error": "Spotify not authenticated."}), 401
|
||||
try:
|
||||
|
|
@ -27756,37 +27819,76 @@ def import_album_match():
|
|||
try:
|
||||
data = request.get_json()
|
||||
album_id = data.get('album_id')
|
||||
album_name = data.get('album_name', '')
|
||||
album_artist = data.get('album_artist', '')
|
||||
if not album_id:
|
||||
return jsonify({'success': False, 'error': 'Missing album_id'}), 400
|
||||
|
||||
# Get album info and tracklist from Spotify
|
||||
album_data = spotify_client.get_album(album_id)
|
||||
if not album_data:
|
||||
return jsonify({'success': False, 'error': 'Album not found'}), 404
|
||||
spotify_tracks = None
|
||||
album_info = None
|
||||
|
||||
tracks_data = spotify_client.get_album_tracks(album_id)
|
||||
if not tracks_data or 'items' not in tracks_data:
|
||||
return jsonify({'success': False, 'error': 'Could not get album tracks'}), 500
|
||||
# Try Hydrabase first when active and album_name available
|
||||
if _is_hydrabase_active() and album_name:
|
||||
try:
|
||||
query = f"{album_artist} {album_name}".strip() if album_artist else album_name
|
||||
hydra_tracks = hydrabase_client.get_album_tracks(query, limit=50)
|
||||
if hydra_tracks:
|
||||
spotify_tracks = []
|
||||
for t in hydra_tracks:
|
||||
artist_list = t.artists if isinstance(t.artists, list) else [t.artists] if t.artists else []
|
||||
spotify_tracks.append({
|
||||
'name': t.name,
|
||||
'track_number': getattr(t, 'track_number', 0) or 0,
|
||||
'disc_number': getattr(t, 'disc_number', 1) or 1,
|
||||
'duration_ms': t.duration_ms,
|
||||
'id': t.id,
|
||||
'artists': [{'name': a} if isinstance(a, str) else a for a in artist_list],
|
||||
'uri': ''
|
||||
})
|
||||
album_info = {
|
||||
'id': album_id,
|
||||
'name': album_name,
|
||||
'artist': album_artist,
|
||||
'artists': [album_artist] if album_artist else [],
|
||||
'release_date': '',
|
||||
'total_tracks': len(spotify_tracks),
|
||||
'image_url': None,
|
||||
'genres': []
|
||||
}
|
||||
logger.info(f"Hydrabase album_tracks returned {len(spotify_tracks)} tracks for '{query}'")
|
||||
except Exception as e:
|
||||
logger.warning(f"Hydrabase album_tracks failed, falling back to Spotify: {e}")
|
||||
spotify_tracks = None
|
||||
|
||||
spotify_tracks = tracks_data['items']
|
||||
# Fall back to Spotify
|
||||
if spotify_tracks is None:
|
||||
album_data = spotify_client.get_album(album_id)
|
||||
if not album_data:
|
||||
return jsonify({'success': False, 'error': 'Album not found'}), 404
|
||||
|
||||
# Build album summary
|
||||
album_artists = [a['name'] for a in album_data.get('artists', [])]
|
||||
album_info = {
|
||||
'id': album_id,
|
||||
'name': album_data.get('name', 'Unknown Album'),
|
||||
'artist': ', '.join(album_artists),
|
||||
'artists': album_artists,
|
||||
'release_date': album_data.get('release_date', ''),
|
||||
'total_tracks': album_data.get('total_tracks', len(spotify_tracks)),
|
||||
'image_url': (album_data.get('images', [{}])[0].get('url') if album_data.get('images') else None),
|
||||
'genres': album_data.get('genres', [])
|
||||
}
|
||||
tracks_data = spotify_client.get_album_tracks(album_id)
|
||||
if not tracks_data or 'items' not in tracks_data:
|
||||
return jsonify({'success': False, 'error': 'Could not get album tracks'}), 500
|
||||
|
||||
# Get artist info for context building later
|
||||
if album_data.get('artists'):
|
||||
primary_artist = album_data['artists'][0]
|
||||
album_info['artist_id'] = primary_artist.get('id', '')
|
||||
spotify_tracks = tracks_data['items']
|
||||
|
||||
# Build album summary
|
||||
album_artists = [a['name'] for a in album_data.get('artists', [])]
|
||||
album_info = {
|
||||
'id': album_id,
|
||||
'name': album_data.get('name', 'Unknown Album'),
|
||||
'artist': ', '.join(album_artists),
|
||||
'artists': album_artists,
|
||||
'release_date': album_data.get('release_date', ''),
|
||||
'total_tracks': album_data.get('total_tracks', len(spotify_tracks)),
|
||||
'image_url': (album_data.get('images', [{}])[0].get('url') if album_data.get('images') else None),
|
||||
'genres': album_data.get('genres', [])
|
||||
}
|
||||
|
||||
# Get artist info for context building later
|
||||
if album_data.get('artists'):
|
||||
primary_artist = album_data['artists'][0]
|
||||
album_info['artist_id'] = primary_artist.get('id', '')
|
||||
|
||||
# Scan staging files
|
||||
staging_path = _get_staging_path()
|
||||
|
|
|
|||
|
|
@ -3364,8 +3364,9 @@ function initializeSearchModeToggle() {
|
|||
showLoadingOverlay('Loading album...');
|
||||
|
||||
try {
|
||||
// Fetch full album data with tracks from Spotify
|
||||
const response = await fetch(`/api/spotify/album/${album.id}`);
|
||||
// Fetch full album data with tracks (Hydrabase or Spotify)
|
||||
const albumParams = new URLSearchParams({ name: album.name || '', artist: album.artist || '' });
|
||||
const response = await fetch(`/api/spotify/album/${album.id}?${albumParams}`);
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue