Fix Tidal V2 search endpoint, duration parsing, and library badge display

pull/253/head
Broque Thomas 2 months ago
parent cc35864e7d
commit ecfa30c918

@ -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…
Cancel
Save