diff --git a/core/artist_source_detail.py b/core/artist_source_detail.py index 8cf11fbf..248f9ddb 100644 --- a/core/artist_source_detail.py +++ b/core/artist_source_detail.py @@ -126,7 +126,7 @@ def build_source_only_artist_detail( allow_fallback=True, skip_cache=False, max_pages=0, - limit=50, + limit=500, artist_source_ids={source: artist_id}, dedup_variants=False, ), diff --git a/core/deezer_client.py b/core/deezer_client.py index 6b2a26c1..fbe42a4b 100644 --- a/core/deezer_client.py +++ b/core/deezer_client.py @@ -168,6 +168,7 @@ class Album: album_type: str image_url: Optional[str] = None external_urls: Optional[Dict[str, str]] = None + explicit: Optional[bool] = None @classmethod def from_deezer_album(cls, album_data: Dict[str, Any]) -> 'Album': @@ -199,7 +200,8 @@ class Album: total_tracks=album_data.get('nb_tracks', 0), album_type=album_type, image_url=image_url, - external_urls=external_urls if external_urls else None + external_urls=external_urls if external_urls else None, + explicit=bool(album_data.get('explicit_lyrics', False)), ) diff --git a/core/discogs_client.py b/core/discogs_client.py index 351fe7bc..8de1de38 100644 --- a/core/discogs_client.py +++ b/core/discogs_client.py @@ -209,6 +209,7 @@ class Album: album_type: str image_url: Optional[str] = None external_urls: Optional[Dict[str, str]] = None + explicit: Optional[bool] = None @classmethod def from_discogs_release(cls, release_data: Dict[str, Any]) -> 'Album': @@ -543,10 +544,26 @@ class DiscogsClient: artist_data = self._api_get(f'/artists/{artist_id}') artist_name = artist_data.get('name', '').lower() if artist_data else '' - data = self._api_get(f'/artists/{artist_id}/releases', { - 'sort': 'year', 'sort_order': 'desc', 'per_page': min(limit * 3, 200), - }) - if not data or not data.get('releases'): + # Paginate through all releases (Discogs max per_page=100). + # We collect raw items across pages before filtering so the limit cap + # applies to qualified albums, not raw API rows. + PAGE_SIZE = 100 + all_items = [] + page = 1 + while True: + data = self._api_get(f'/artists/{artist_id}/releases', { + 'sort': 'year', 'sort_order': 'desc', + 'per_page': PAGE_SIZE, 'page': page, + }) + if not data or not data.get('releases'): + break + all_items.extend(data['releases']) + pagination = data.get('pagination', {}) + if page >= pagination.get('pages', 1): + break + page += 1 + + if not all_items: return [] # Separate masters from individual releases — prefer masters (canonical versions) @@ -554,7 +571,7 @@ class DiscogsClient: releases_no_master = [] master_titles = set() - for item in data['releases']: + for item in all_items: # Skip non-main roles role = item.get('role', 'Main').lower() if role not in ('main', ''): @@ -595,7 +612,8 @@ class DiscogsClient: album = Album(id=album.id, name=album.name, artists=album.artists, release_date=album.release_date, total_tracks=album.total_tracks, album_type=album.album_type, image_url=thumb, - external_urls=album.external_urls) + external_urls=album.external_urls, + explicit=album.explicit) # Deduplicate by normalized title (but keep deluxe/special editions as separate) dedup_key = album.name.lower().strip() @@ -607,7 +625,7 @@ class DiscogsClient: if album.album_type in allowed_types: albums.append(album) - if len(albums) >= limit: + if limit and len(albums) >= limit: break except Exception as e: logger.debug(f"Error parsing Discogs artist release: {e}") diff --git a/core/hydrabase_client.py b/core/hydrabase_client.py index f30324b2..acfc368f 100644 --- a/core/hydrabase_client.py +++ b/core/hydrabase_client.py @@ -565,6 +565,7 @@ class HydrabaseClient: album_type=item_type, image_url=item.get('image_url'), external_urls=ext_urls, + explicit=item.get('explicit'), )) except Exception as e: logger.debug(f"Skipping malformed Hydrabase artist album: {e}") diff --git a/core/itunes_client.py b/core/itunes_client.py index a62fb035..1a2d269d 100644 --- a/core/itunes_client.py +++ b/core/itunes_client.py @@ -167,7 +167,8 @@ class Album: album_type: str image_url: Optional[str] = None external_urls: Optional[Dict[str, str]] = None - + explicit: Optional[bool] = None + @classmethod def from_itunes_album(cls, album_data: Dict[str, Any]) -> 'Album': # Get highest quality artwork @@ -209,7 +210,8 @@ class Album: total_tracks=track_count, album_type=album_type, image_url=image_url, - external_urls=external_urls if external_urls else None + external_urls=external_urls if external_urls else None, + explicit=album_data.get('collectionExplicitness') == 'explicit', ) @dataclass diff --git a/core/metadata/discography.py b/core/metadata/discography.py index ac51e141..04ad7642 100644 --- a/core/metadata/discography.py +++ b/core/metadata/discography.py @@ -97,6 +97,9 @@ def _build_discography_release_dict(release: Any, artist_id: str) -> Optional[Di album_type = _extract_lookup_value(release, 'album_type', default='album') or 'album' release_date = _extract_lookup_value(release, 'release_date') + raw_explicit = _extract_lookup_value(release, 'explicit') + explicit: Optional[bool] = bool(raw_explicit) if raw_explicit is not None else None + return { 'id': release_id, 'name': _extract_lookup_value(release, 'name', 'title', default=release_id), @@ -106,6 +109,7 @@ def _build_discography_release_dict(release: Any, artist_id: str) -> Optional[Di 'image_url': _extract_lookup_value(release, 'image_url', 'thumb_url', 'cover_image'), 'total_tracks': _extract_lookup_value(release, 'total_tracks', default=0) or 0, 'external_urls': _extract_lookup_value(release, 'external_urls', default={}) or {}, + 'explicit': explicit, } @@ -359,6 +363,9 @@ def _build_artist_detail_release_card(release: Dict[str, Any]) -> Optional[Dict[ if release_year is not None: release_year = str(release_year) + raw_explicit = _extract_lookup_value(release, 'explicit') + card_explicit: Optional[bool] = bool(raw_explicit) if raw_explicit is not None else None + card = { 'id': release_id, 'name': _extract_lookup_value(release, 'name', 'title', default=release_id), @@ -369,6 +376,7 @@ def _build_artist_detail_release_card(release: Dict[str, Any]) -> Optional[Dict[ 'track_count': _extract_lookup_value(release, 'track_count', 'total_tracks', default=0) or 0, 'owned': None, 'track_completion': 'checking', + 'explicit': card_explicit, } if release_date: diff --git a/core/musicbrainz_search.py b/core/musicbrainz_search.py index 965317be..89b54595 100644 --- a/core/musicbrainz_search.py +++ b/core/musicbrainz_search.py @@ -57,6 +57,7 @@ class Album: album_type: str image_url: Optional[str] = None external_urls: Optional[Dict[str, str]] = None + explicit: Optional[bool] = None def _cover_art_url(mbid: str, scope: str = 'release') -> Optional[str]: diff --git a/core/spotify_client.py b/core/spotify_client.py index 50979bd5..d8a182c7 100644 --- a/core/spotify_client.py +++ b/core/spotify_client.py @@ -429,6 +429,7 @@ class Album: image_url: Optional[str] = None external_urls: Optional[Dict[str, str]] = None artist_ids: Optional[List[str]] = None + explicit: Optional[bool] = None @classmethod def from_spotify_album(cls, album_data: Dict[str, Any]) -> 'Album': diff --git a/web_server.py b/web_server.py index 831976c8..ce97cbd7 100644 --- a/web_server.py +++ b/web_server.py @@ -8553,7 +8553,7 @@ def get_artist_detail(artist_id): allow_fallback=True, skip_cache=False, max_pages=0, - limit=50, + limit=500, artist_source_ids=artist_source_ids, ), ) @@ -8783,7 +8783,7 @@ def get_artist_discography(artist_id): allow_fallback=True, skip_cache=False, max_pages=0, - limit=50, + limit=500, ), ) diff --git a/webui/static/library.js b/webui/static/library.js index 6c4e7a4c..7f211f86 100644 --- a/webui/static/library.js +++ b/webui/static/library.js @@ -1611,8 +1611,9 @@ function createReleaseCard(release) { const content = document.createElement("div"); content.className = "album-card-content"; const _esc = (s) => String(s || '').replace(/&/g, '&').replace(/"/g, '"').replace(//g, '>'); + const explicitTag = release.explicit === true ? 'E' : ''; content.innerHTML = ` -