diff --git a/core/deezer_client.py b/core/deezer_client.py index 22c1a746..e564663c 100644 --- a/core/deezer_client.py +++ b/core/deezer_client.py @@ -212,6 +212,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': @@ -243,7 +244,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/hydrabase_client.py b/core/hydrabase_client.py index df204d38..7c73dd73 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 95d64669..6f08dba3 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 35f6df77..6ee88fa2 100644 --- a/core/metadata/discography.py +++ b/core/metadata/discography.py @@ -150,6 +150,7 @@ def _build_discography_release_dict(release: Any, artist_id: str, 'image_url': typed_album.image_url, 'total_tracks': typed_album.total_tracks or 0, 'external_urls': typed_album.external_urls or {}, + 'explicit': typed_album.explicit, } release_id = _extract_lookup_value(release, 'id', 'album_id', 'release_id') @@ -168,6 +169,7 @@ def _build_discography_release_dict(release: Any, artist_id: str, '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': _extract_lookup_value(release, 'explicit'), } @@ -436,6 +438,7 @@ def _build_artist_detail_release_card(release: Dict[str, Any], 'track_count': typed_album.total_tracks or 0, 'owned': None, 'track_completion': 'checking', + 'explicit': typed_album.explicit, } if typed_album.release_date: card['release_date'] = typed_album.release_date @@ -470,6 +473,7 @@ def _build_artist_detail_release_card(release: Dict[str, Any], 'track_count': _extract_lookup_value(release, 'track_count', 'total_tracks', default=0) or 0, 'owned': None, 'track_completion': 'checking', + 'explicit': _extract_lookup_value(release, 'explicit'), } if release_date: diff --git a/core/metadata/types.py b/core/metadata/types.py index 4ed01ff4..00c9749b 100644 --- a/core/metadata/types.py +++ b/core/metadata/types.py @@ -100,6 +100,8 @@ class Album: label: Optional[str] = None # Record label / publisher barcode: Optional[str] = None # UPC/EAN — Discogs/MusicBrainz only + explicit: Optional[bool] = None # True=explicit, False=clean, None=unknown + # Source provenance source: str = '' # 'spotify' / 'itunes' / etc — set by converter external_ids: Dict[str, str] = field(default_factory=dict) @@ -193,6 +195,8 @@ class Album: release_date = release_date.split('T', 1)[0] primary_genre = _str(raw.get('primaryGenreName')) + ce = _str(raw.get('collectionExplicitness')) + explicit = True if ce == 'explicit' else (False if ce in ('notExplicit', 'cleaned') else None) return cls( id=_str(raw.get('collectionId')), name=name, @@ -203,6 +207,7 @@ class Album: image_url=_itunes_artwork(raw.get('artworkUrl100')), artist_id=artist_id, genres=[primary_genre] if primary_genre else [], + explicit=explicit, source='itunes', external_ids=external_ids, external_urls=external_urls, @@ -238,6 +243,8 @@ class Album: if raw.get('link'): external_urls['deezer'] = _str(raw['link']) + _el = raw.get('explicit_lyrics') + explicit = bool(_el) if _el is not None else None return cls( id=_str(raw.get('id')), name=_str(raw.get('title')), @@ -251,6 +258,7 @@ class Album: if isinstance(g, dict) and g.get('name')], label=_str(raw.get('label')) or None, barcode=external_ids.get('upc'), + explicit=explicit, source='deezer', external_ids=external_ids, external_urls=external_urls, @@ -513,6 +521,8 @@ class Album: if raw.get('soul_id'): external_ids['soul'] = _str(raw['soul_id']) + _he = raw.get('explicit') + explicit = bool(_he) if _he is not None else None return cls( id=_str(raw.get('id')), name=_str(raw.get('name') or raw.get('title')), @@ -522,6 +532,7 @@ class Album: album_type=_str(raw.get('album_type'), default='album'), image_url=_str(raw.get('image_url') or raw.get('thumb_url')) or None, artist_id=_str(raw.get('artist_id')) or None, + explicit=explicit, source='hydrabase', external_ids=external_ids, ) diff --git a/webui/static/library.js b/webui/static/library.js index 434b9319..cacc2ba6 100644 --- a/webui/static/library.js +++ b/webui/static/library.js @@ -1859,7 +1859,7 @@ function createReleaseCard(release) { content.className = "album-card-content"; const _esc = (s) => String(s || '').replace(/&/g, '&').replace(/"/g, '"').replace(//g, '>'); content.innerHTML = ` -