feat: add explicit badges to discography modal and artist-detail cards
Adds an explicit field to the Album dataclass in core/metadata/types.py
and the client-level Album dataclasses in deezer_client.py,
itunes_client.py, and hydrabase_client.py (the legacy discography path
reads from client objects, not typed dicts).
Deezer extracts explicit_lyrics (int→bool), iTunes extracts
collectionExplicitness ('explicit' string), Hydrabase forwards the
explicit field from the server response. Spotify, Discogs, MusicBrainz,
Qobuz, and Tidal have no explicit signal and stay None.
The flag threads through both builder functions in discography.py and
renders as a small "E" badge next to explicit titles in the discography
download modal and artist-detail page cards.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
09597eb6db
commit
42bee21c9f
7 changed files with 40 additions and 5 deletions
|
|
@ -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)),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1838,7 +1838,7 @@ function createReleaseCard(release) {
|
|||
content.className = "album-card-content";
|
||||
const _esc = (s) => String(s || '').replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
||||
content.innerHTML = `
|
||||
<div class="album-card-name" title="${_esc(release.title)}">${_esc(release.title)}</div>
|
||||
<div class="album-card-name" title="${_esc(release.title)}">${_esc(release.title)}${release.explicit === true ? ' <span class="explicit-badge">E</span>' : ''}</div>
|
||||
${yearText ? `<div class="album-card-year">${_esc(yearText)}</div>` : ''}
|
||||
`;
|
||||
card.appendChild(content);
|
||||
|
|
@ -2486,7 +2486,7 @@ function _renderDiscogCard(release, index, completionData) {
|
|||
${statusIcon ? `<span class="discog-card-status">${statusIcon}</span>` : ''}
|
||||
</div>
|
||||
<div class="discog-card-info">
|
||||
<div class="discog-card-title">${_esc(albumName)}</div>
|
||||
<div class="discog-card-title">${_esc(albumName)}${release.explicit === true ? ' <span class="explicit-badge">E</span>' : ''}</div>
|
||||
<div class="discog-card-meta">${year}${year && tracks ? ' · ' : ''}${tracks ? tracks + ' tracks' : ''}</div>
|
||||
</div>
|
||||
<div class="discog-card-check"></div>
|
||||
|
|
|
|||
|
|
@ -45469,6 +45469,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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue