Raise artist discography limit from 50 to 200 with Deezer pagination

Deezer and iTunes defaulted to 50 albums max, silently truncating large
discographies. Deezer now paginates (100 per page) up to 200. iTunes
raised to 200 (single call). All callers in web_server.py updated to
use the new defaults instead of hardcoding limit=50.

Also adds diagnostic logging for allow_duplicates album comparison
to help debug inconsistent singles behavior.
This commit is contained in:
Broque Thomas 2026-04-12 21:46:04 -07:00
parent b4cf8b4cc1
commit 0edd8f5c81
4 changed files with 39 additions and 25 deletions

View file

@ -635,26 +635,37 @@ class DeezerClient:
'_raw_data': artist_data '_raw_data': artist_data
} }
def get_artist_albums_list(self, artist_id: str, album_type: str = 'album,single', limit: int = 50) -> List[Album]: def get_artist_albums_list(self, artist_id: str, album_type: str = 'album,single', limit: int = 200) -> List[Album]:
"""Get albums by artist ID — returns Album dataclass list (metadata source interface). """Get albums by artist ID — returns Album dataclass list (metadata source interface).
Matches iTunesClient.get_artist_albums() interface.""" Matches iTunesClient.get_artist_albums() interface.
data = self._api_get(f'artist/{artist_id}/albums', {'limit': min(limit, 100)}) Paginates through all results up to the requested limit."""
if not data or 'data' not in data:
return []
albums = [] albums = []
all_raw = []
requested_types = [t.strip() for t in album_type.split(',')] requested_types = [t.strip() for t in album_type.split(',')]
offset = 0
page_size = 100 # Deezer API max per request
for album_data in data['data']: while offset < limit:
album = Album.from_deezer_album(album_data) fetch_limit = min(page_size, limit - offset)
data = self._api_get(f'artist/{artist_id}/albums', {'limit': fetch_limit, 'index': offset})
if not data or 'data' not in data or len(data['data']) == 0:
break
if album_type != 'album,single': for album_data in data['data']:
if album.album_type not in requested_types: all_raw.append(album_data)
if not (album.album_type == 'ep' and 'single' in requested_types): album = Album.from_deezer_album(album_data)
continue
albums.append(album) if album_type != 'album,single':
if album.album_type not in requested_types:
if not (album.album_type == 'ep' and 'single' in requested_types):
continue
albums.append(album)
if len(data['data']) < fetch_limit:
break # Last page
offset += len(data['data'])
cache = get_metadata_cache() cache = get_metadata_cache()
# Deezer's /artist/{id}/albums endpoint doesn't include artist info on each album. # Deezer's /artist/{id}/albums endpoint doesn't include artist info on each album.
@ -663,7 +674,7 @@ class DeezerClient:
if albums and albums[0].artists: if albums and albums[0].artists:
artist_stub = {'id': int(artist_id) if artist_id.isdigit() else 0, 'name': albums[0].artists[0]} artist_stub = {'id': int(artist_id) if artist_id.isdigit() else 0, 'name': albums[0].artists[0]}
entries = [] entries = []
for ad in data['data']: for ad in all_raw:
if ad.get('id'): if ad.get('id'):
if artist_stub and not ad.get('artist'): if artist_stub and not ad.get('artist'):
ad['artist'] = artist_stub ad['artist'] = artist_stub

View file

@ -991,7 +991,7 @@ class iTunesClient:
return None return None
def get_artist_albums(self, artist_id: str, album_type: str = 'album,single', limit: int = 50) -> List[Album]: def get_artist_albums(self, artist_id: str, album_type: str = 'album,single', limit: int = 200) -> List[Album]:
""" """
Get albums by artist ID Get albums by artist ID

View file

@ -1542,10 +1542,13 @@ class WatchlistScanner:
from difflib import SequenceMatcher from difflib import SequenceMatcher
album_sim = SequenceMatcher(None, album_name.lower(), lib_album.lower()).ratio() album_sim = SequenceMatcher(None, album_name.lower(), lib_album.lower()).ratio()
if album_sim < 0.85: if album_sim < 0.85:
logger.debug(f"Track found but different album (allow_duplicates=True): '{original_title}' — library: '{lib_album}', wanted: '{album_name}'") logger.info(f"[AllowDup] Different album — allowing: '{original_title}' (wanted: '{album_name}', library: '{lib_album}', sim: {album_sim:.2f})")
continue # Different album — allow it continue # Different album — allow it
else:
logger.info(f"[AllowDup] Same album — skipping: '{original_title}' (wanted: '{album_name}', library: '{lib_album}', sim: {album_sim:.2f})")
else: else:
# No album info in library — can't compare, allow it # No album info in library — can't compare, allow it
logger.info(f"[AllowDup] No album info in library — allowing: '{original_title}'")
continue continue
logger.debug(f"Track found in library: '{original_title}' by '{artist_name}' (confidence: {confidence:.2f})") logger.debug(f"Track found in library: '{original_title}' by '{artist_name}' (confidence: {confidence:.2f})")
return False # Track exists in library return False # Track exists in library

View file

@ -10458,17 +10458,17 @@ def get_artist_discography(artist_id):
active_source = 'spotify' active_source = 'spotify'
elif source_override == 'itunes': elif source_override == 'itunes':
itunes_cl = _get_itunes_client() itunes_cl = _get_itunes_client()
albums = itunes_cl.get_artist_albums(artist_id, album_type='album,single', limit=50) albums = itunes_cl.get_artist_albums(artist_id, album_type='album,single')
if albums: if albums:
active_source = 'itunes' active_source = 'itunes'
elif source_override == 'deezer': elif source_override == 'deezer':
deezer_cl = _get_deezer_client() deezer_cl = _get_deezer_client()
albums = deezer_cl.get_artist_albums(artist_id, album_type='album,single', limit=50) albums = deezer_cl.get_artist_albums(artist_id, album_type='album,single')
if albums: if albums:
active_source = 'deezer' active_source = 'deezer'
elif source_override == 'discogs': elif source_override == 'discogs':
discogs_cl = _get_discogs_client() discogs_cl = _get_discogs_client()
albums = discogs_cl.get_artist_albums(artist_id, album_type='album,single', limit=50) albums = discogs_cl.get_artist_albums(artist_id, album_type='album,single')
if albums: if albums:
active_source = 'discogs' active_source = 'discogs'
elif source_override == 'hydrabase': elif source_override == 'hydrabase':
@ -10479,7 +10479,7 @@ def get_artist_discography(artist_id):
hb_cl = _get_itunes_client() hb_cl = _get_itunes_client()
else: else:
hb_cl = spotify_client hb_cl = spotify_client
albums = hb_cl.get_artist_albums(artist_id, album_type='album,single', limit=50) albums = hb_cl.get_artist_albums(artist_id, album_type='album,single')
if albums: if albums:
active_source = plugin or 'hydrabase' active_source = plugin or 'hydrabase'
@ -10506,7 +10506,7 @@ def get_artist_discography(artist_id):
search_artists = cl.search_artists(artist_name, limit=5) search_artists = cl.search_artists(artist_name, limit=5)
if search_artists: if search_artists:
best = next((a for a in search_artists if a.name.lower() == artist_name.lower()), search_artists[0]) best = next((a for a in search_artists if a.name.lower() == artist_name.lower()), search_artists[0])
albums = cl.get_artist_albums(best.id, album_type='album,single', limit=50) albums = cl.get_artist_albums(best.id, album_type='album,single')
if albums: if albums:
active_source = source_override active_source = source_override
@ -10544,7 +10544,7 @@ def get_artist_discography(artist_id):
try: try:
if is_numeric_id: if is_numeric_id:
# It's a numeric ID (iTunes/Deezer), use directly # It's a numeric ID (iTunes/Deezer), use directly
albums = fallback_client.get_artist_albums(artist_id, album_type='album,single', limit=50) albums = fallback_client.get_artist_albums(artist_id, album_type='album,single')
if albums: if albums:
active_source = fallback_source active_source = fallback_source
print(f"Got {len(albums)} albums from {fallback_source} (direct ID)") print(f"Got {len(albums)} albums from {fallback_source} (direct ID)")
@ -10563,7 +10563,7 @@ def get_artist_discography(artist_id):
best_match = fallback_artists[0] best_match = fallback_artists[0]
print(f"Found {fallback_source} artist: {best_match.name} (ID: {best_match.id})") print(f"Found {fallback_source} artist: {best_match.name} (ID: {best_match.id})")
albums = fallback_client.get_artist_albums(best_match.id, album_type='album,single', limit=50) albums = fallback_client.get_artist_albums(best_match.id, album_type='album,single')
if albums: if albums:
active_source = fallback_source active_source = fallback_source
print(f"Got {len(albums)} albums from {fallback_source} (name search)") print(f"Got {len(albums)} albums from {fallback_source} (name search)")
@ -48064,7 +48064,7 @@ def playlist_explorer_build_tree():
try: try:
# skip_cache only supported by spotify_client — other clients don't cache this call # skip_cache only supported by spotify_client — other clients don't cache this call
_skip = {'skip_cache': True} if hasattr(active_client, 'sp') else {} _skip = {'skip_cache': True} if hasattr(active_client, 'sp') else {}
all_albums = active_client.get_artist_albums(artist_id, album_type='album,single', limit=50, **_skip) all_albums = active_client.get_artist_albums(artist_id, album_type='album,single', **_skip)
except Exception as e: except Exception as e:
return {'success': False, 'error': f'Album fetch failed: {e}'} return {'success': False, 'error': f'Album fetch failed: {e}'}
@ -49269,7 +49269,7 @@ def get_spotify_artist_discography(artist_name):
print(f"Found Spotify artist: {artist.name} (ID: {spotify_artist_id}, confidence: {highest_score:.3f})") print(f"Found Spotify artist: {artist.name} (ID: {spotify_artist_id}, confidence: {highest_score:.3f})")
# Get all albums (albums, singles, and compilations) # Get all albums (albums, singles, and compilations)
all_albums = spotify_client.get_artist_albums(spotify_artist_id, album_type='album,single,compilation', limit=50) all_albums = spotify_client.get_artist_albums(spotify_artist_id, album_type='album,single,compilation')
if not all_albums: if not all_albums:
return { return {