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 = ` -
${_esc(release.title)}
+
${_esc(release.title)}${release.explicit === true ? ' E' : ''}
${yearText ? `
${_esc(yearText)}
` : ''} `; card.appendChild(content); @@ -2507,7 +2507,7 @@ function _renderDiscogCard(release, index, completionData) { ${statusIcon ? `${statusIcon}` : ''}
-
${_esc(albumName)}
+
${_esc(albumName)}${release.explicit === true ? ' E' : ''}
${year}${year && tracks ? ' · ' : ''}${tracks ? tracks + ' tracks' : ''}
diff --git a/webui/static/style.css b/webui/static/style.css index 15b132ac..2ca10132 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -45985,6 +45985,21 @@ textarea.enhanced-meta-field-input { .discog-card-meta { font-size: 11px; color: rgba(255,255,255,0.35); margin-top: 2px; } +.explicit-badge { + display: inline-block; font-size: 9px; font-weight: 800; letter-spacing: 0.04em; + padding: 1px 4px; border-radius: 3px; vertical-align: middle; margin-left: 5px; + background: rgba(255,255,255,0.12); color: rgba(255,255,255,0.55); + border: 1px solid rgba(255,255,255,0.15); line-height: 1.4; flex-shrink: 0; + font-family: inherit; +} +.album-card-explicit { + display: inline-block; font-size: 9px; font-weight: 800; letter-spacing: 0.04em; + padding: 1px 4px; border-radius: 3px; vertical-align: middle; margin-left: 4px; + background: rgba(255,255,255,0.12); color: rgba(255,255,255,0.55); + border: 1px solid rgba(255,255,255,0.2); line-height: 1.4; + font-family: inherit; text-shadow: none; +} + .discog-card-check { width: 20px; height: 20px; border-radius: 6px; flex-shrink: 0; border: 2px solid rgba(255,255,255,0.15); transition: all 0.15s ease;