Fix Tidal V2 search endpoint, duration parsing, and library badge display
This commit is contained in:
parent
cc35864e7d
commit
ecfa30c918
5 changed files with 93 additions and 19 deletions
|
|
@ -340,6 +340,11 @@ class QobuzWorker:
|
|||
self._process_track(item_id, item_name, item.get('artist', ''), item)
|
||||
|
||||
except Exception as e:
|
||||
error_str = str(e).lower()
|
||||
if '429' in error_str or 'rate limit' in error_str:
|
||||
logger.warning(f"Rate limited while processing {item['type']} #{item['id']}, backing off 30s")
|
||||
time.sleep(30)
|
||||
return
|
||||
logger.error(f"Error processing {item['type']} #{item['id']}: {e}")
|
||||
self.stats['errors'] += 1
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -696,23 +696,40 @@ class TidalClient:
|
|||
timeout=10
|
||||
)
|
||||
|
||||
if response.status_code == 429:
|
||||
raise Exception("Rate limited (429) on search_tracks")
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
tracks = []
|
||||
|
||||
if 'tracks' in data and 'items' in data['tracks']:
|
||||
for item in data['tracks']['items']:
|
||||
track = self._parse_track_data(item)
|
||||
if track:
|
||||
tracks.append(track)
|
||||
|
||||
|
||||
# Handle V2 JSON:API response formats
|
||||
items = []
|
||||
if 'tracks' in data and isinstance(data['tracks'], list):
|
||||
items = data['tracks']
|
||||
elif 'tracks' in data and 'items' in data['tracks']:
|
||||
items = data['tracks']['items']
|
||||
elif 'included' in data:
|
||||
items = [r for r in data['included'] if r.get('type') == 'tracks']
|
||||
|
||||
for item in items:
|
||||
# Flatten JSON:API resource if needed
|
||||
if 'attributes' in item and 'id' in item:
|
||||
flat = dict(item['attributes'])
|
||||
flat['id'] = item['id']
|
||||
item = flat
|
||||
track = self._parse_track_data(item)
|
||||
if track:
|
||||
tracks.append(track)
|
||||
|
||||
logger.info(f"Found {len(tracks)} Tidal tracks for query: '{query}'")
|
||||
return tracks
|
||||
else:
|
||||
logger.error(f"Tidal search failed: {response.status_code} - {response.text}")
|
||||
return []
|
||||
|
||||
|
||||
except Exception as e:
|
||||
if "429" in str(e):
|
||||
raise # Let rate_limited decorator handle retry
|
||||
logger.error(f"Error searching Tidal tracks: {e}")
|
||||
return []
|
||||
|
||||
|
|
@ -807,6 +824,13 @@ class TidalClient:
|
|||
if 'attributes' in item and 'id' in item:
|
||||
flat = dict(item['attributes'])
|
||||
flat['id'] = item['id']
|
||||
# Preserve artist relationship for cross-verification
|
||||
try:
|
||||
rel_artists = item.get('relationships', {}).get('artists', {}).get('data', [])
|
||||
if rel_artists:
|
||||
flat['artist'] = {'id': rel_artists[0].get('id')}
|
||||
except (AttributeError, IndexError, TypeError):
|
||||
pass
|
||||
return flat
|
||||
return item
|
||||
else:
|
||||
|
|
@ -857,6 +881,13 @@ class TidalClient:
|
|||
if 'attributes' in item and 'id' in item:
|
||||
flat = dict(item['attributes'])
|
||||
flat['id'] = item['id']
|
||||
# Preserve artist relationship for cross-verification
|
||||
try:
|
||||
rel_artists = item.get('relationships', {}).get('artists', {}).get('data', [])
|
||||
if rel_artists:
|
||||
flat['artist'] = {'id': rel_artists[0].get('id')}
|
||||
except (AttributeError, IndexError, TypeError):
|
||||
pass
|
||||
return flat
|
||||
return item
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -11,6 +11,28 @@ from core.tidal_client import TidalClient
|
|||
logger = get_logger("tidal_worker")
|
||||
|
||||
|
||||
def _parse_duration_to_ms(duration) -> Optional[int]:
|
||||
"""Convert duration to milliseconds. Handles integer seconds and ISO-8601 strings (PT3M36S)."""
|
||||
if not duration:
|
||||
return None
|
||||
if isinstance(duration, (int, float)) and duration > 0:
|
||||
return int(duration * 1000)
|
||||
if isinstance(duration, str) and duration.startswith('PT'):
|
||||
total_seconds = 0
|
||||
hours_match = re.search(r'(\d+)H', duration)
|
||||
minutes_match = re.search(r'(\d+)M', duration)
|
||||
seconds_match = re.search(r'(\d+)S', duration)
|
||||
if hours_match:
|
||||
total_seconds += int(hours_match.group(1)) * 3600
|
||||
if minutes_match:
|
||||
total_seconds += int(minutes_match.group(1)) * 60
|
||||
if seconds_match:
|
||||
total_seconds += int(seconds_match.group(1))
|
||||
if total_seconds > 0:
|
||||
return total_seconds * 1000
|
||||
return None
|
||||
|
||||
|
||||
class TidalWorker:
|
||||
"""Background worker for enriching library artists, albums, and tracks with Tidal metadata"""
|
||||
|
||||
|
|
@ -332,8 +354,9 @@ class TidalWorker:
|
|||
except Exception as e:
|
||||
error_str = str(e).lower()
|
||||
if '429' in error_str or 'rate limit' in error_str:
|
||||
# Rate limit — don't mark as error, leave for retry on next loop
|
||||
logger.warning(f"Rate limited while processing {item['type']} #{item['id']}, will retry")
|
||||
# Rate limit — don't mark as error, back off then retry
|
||||
logger.warning(f"Rate limited while processing {item['type']} #{item['id']}, backing off 30s")
|
||||
time.sleep(30)
|
||||
return
|
||||
logger.error(f"Error processing {item['type']} #{item['id']}: {e}")
|
||||
self.stats['errors'] += 1
|
||||
|
|
@ -590,10 +613,9 @@ class TidalWorker:
|
|||
WHERE id = ? AND track_count IS NULL
|
||||
""", (num_tracks, album_id))
|
||||
|
||||
# Backfill duration (Tidal returns seconds, DB stores milliseconds)
|
||||
duration = data.get('duration')
|
||||
if duration and isinstance(duration, (int, float)) and duration > 0:
|
||||
duration_ms = int(duration * 1000)
|
||||
# Backfill duration (Tidal returns seconds or ISO-8601, DB stores milliseconds)
|
||||
duration_ms = _parse_duration_to_ms(data.get('duration'))
|
||||
if duration_ms:
|
||||
cursor.execute("""
|
||||
UPDATE albums SET duration = ?
|
||||
WHERE id = ? AND duration IS NULL
|
||||
|
|
@ -677,9 +699,8 @@ class TidalWorker:
|
|||
WHERE id = ? AND (isrc IS NULL OR isrc = '')
|
||||
""", (isrc, track_id))
|
||||
|
||||
duration = data.get('duration')
|
||||
if duration and isinstance(duration, (int, float)) and duration > 0:
|
||||
duration_ms = int(duration * 1000)
|
||||
duration_ms = _parse_duration_to_ms(data.get('duration'))
|
||||
if duration_ms:
|
||||
cursor.execute("""
|
||||
UPDATE tracks SET duration = ?
|
||||
WHERE id = ? AND duration IS NULL
|
||||
|
|
|
|||
|
|
@ -6054,6 +6054,8 @@ class MusicDatabase:
|
|||
a.audiodb_id,
|
||||
a.lastfm_url,
|
||||
a.genius_url,
|
||||
a.tidal_id,
|
||||
a.qobuz_id,
|
||||
COUNT(DISTINCT al.id) as album_count,
|
||||
COUNT(DISTINCT t.id) as track_count
|
||||
FROM artists a
|
||||
|
|
@ -6064,7 +6066,7 @@ class MusicDatabase:
|
|||
WHERE {where_clause}
|
||||
AND a.id = (SELECT MIN(a2.id) FROM artists a2
|
||||
WHERE a2.name = a.name AND a2.server_source = a.server_source)
|
||||
GROUP BY a.id, a.name, a.thumb_url, a.genres, a.musicbrainz_id, a.spotify_artist_id, a.itunes_artist_id, a.deezer_id, a.audiodb_id, a.lastfm_url, a.genius_url
|
||||
GROUP BY a.id, a.name, a.thumb_url, a.genres, a.musicbrainz_id, a.spotify_artist_id, a.itunes_artist_id, a.deezer_id, a.audiodb_id, a.lastfm_url, a.genius_url, a.tidal_id, a.qobuz_id
|
||||
ORDER BY a.name COLLATE NOCASE
|
||||
LIMIT ? OFFSET ?
|
||||
"""
|
||||
|
|
@ -6114,6 +6116,8 @@ class MusicDatabase:
|
|||
'audiodb_id': row['audiodb_id'],
|
||||
'lastfm_url': row['lastfm_url'],
|
||||
'genius_url': row['genius_url'],
|
||||
'tidal_id': row['tidal_id'],
|
||||
'qobuz_id': row['qobuz_id'],
|
||||
'album_count': row['album_count'] or 0,
|
||||
'track_count': row['track_count'] or 0,
|
||||
'is_watched': bool(is_watched)
|
||||
|
|
@ -6171,7 +6175,8 @@ class MusicDatabase:
|
|||
SELECT
|
||||
id, name, thumb_url, genres, server_source,
|
||||
musicbrainz_id, deezer_id, audiodb_id,
|
||||
spotify_artist_id, itunes_artist_id, lastfm_url, genius_url
|
||||
spotify_artist_id, itunes_artist_id, lastfm_url, genius_url,
|
||||
tidal_id, qobuz_id
|
||||
FROM artists
|
||||
WHERE id = ?
|
||||
""", (artist_id,))
|
||||
|
|
@ -6324,6 +6329,8 @@ class MusicDatabase:
|
|||
'itunes_artist_id': artist_row['itunes_artist_id'],
|
||||
'lastfm_url': artist_row['lastfm_url'],
|
||||
'genius_url': artist_row['genius_url'],
|
||||
'tidal_id': artist_row['tidal_id'],
|
||||
'qobuz_id': artist_row['qobuz_id'],
|
||||
'album_count': album_count,
|
||||
'track_count': track_count
|
||||
},
|
||||
|
|
|
|||
|
|
@ -35209,6 +35209,16 @@ function getServiceUrl(service, entityType, id) {
|
|||
artist: id, // genius_url is already a full URL
|
||||
track: id, // genius_url on tracks is already a full URL
|
||||
},
|
||||
tidal: {
|
||||
artist: `https://tidal.com/browse/artist/${id}`,
|
||||
album: `https://tidal.com/browse/album/${id}`,
|
||||
track: `https://tidal.com/browse/track/${id}`,
|
||||
},
|
||||
qobuz: {
|
||||
artist: `https://www.qobuz.com/artist/${id}`,
|
||||
album: `https://www.qobuz.com/album/${id}`,
|
||||
track: `https://www.qobuz.com/track/${id}`,
|
||||
},
|
||||
};
|
||||
return urls[service] && urls[service][entityType] || null;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue