Merge pull request #530 from dlynas/feat/explicit-badges

feat: add explicit badges to discography modal and artist-detail cards
This commit is contained in:
BoulderBadgeDad 2026-05-13 15:04:14 -07:00 committed by GitHub
commit c77aa61fdf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 40 additions and 5 deletions

View file

@ -212,6 +212,7 @@ class Album:
album_type: str album_type: str
image_url: Optional[str] = None image_url: Optional[str] = None
external_urls: Optional[Dict[str, str]] = None external_urls: Optional[Dict[str, str]] = None
explicit: Optional[bool] = None
@classmethod @classmethod
def from_deezer_album(cls, album_data: Dict[str, Any]) -> 'Album': 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), total_tracks=album_data.get('nb_tracks', 0),
album_type=album_type, album_type=album_type,
image_url=image_url, 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)),
) )

View file

@ -565,6 +565,7 @@ class HydrabaseClient:
album_type=item_type, album_type=item_type,
image_url=item.get('image_url'), image_url=item.get('image_url'),
external_urls=ext_urls, external_urls=ext_urls,
explicit=item.get('explicit'),
)) ))
except Exception as e: except Exception as e:
logger.debug(f"Skipping malformed Hydrabase artist album: {e}") logger.debug(f"Skipping malformed Hydrabase artist album: {e}")

View file

@ -167,7 +167,8 @@ class Album:
album_type: str album_type: str
image_url: Optional[str] = None image_url: Optional[str] = None
external_urls: Optional[Dict[str, str]] = None external_urls: Optional[Dict[str, str]] = None
explicit: Optional[bool] = None
@classmethod @classmethod
def from_itunes_album(cls, album_data: Dict[str, Any]) -> 'Album': def from_itunes_album(cls, album_data: Dict[str, Any]) -> 'Album':
# Get highest quality artwork # Get highest quality artwork
@ -209,7 +210,8 @@ class Album:
total_tracks=track_count, total_tracks=track_count,
album_type=album_type, album_type=album_type,
image_url=image_url, 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 @dataclass

View file

@ -150,6 +150,7 @@ def _build_discography_release_dict(release: Any, artist_id: str,
'image_url': typed_album.image_url, 'image_url': typed_album.image_url,
'total_tracks': typed_album.total_tracks or 0, 'total_tracks': typed_album.total_tracks or 0,
'external_urls': typed_album.external_urls or {}, 'external_urls': typed_album.external_urls or {},
'explicit': typed_album.explicit,
} }
release_id = _extract_lookup_value(release, 'id', 'album_id', 'release_id') 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'), 'image_url': _extract_lookup_value(release, 'image_url', 'thumb_url', 'cover_image'),
'total_tracks': _extract_lookup_value(release, 'total_tracks', default=0) or 0, 'total_tracks': _extract_lookup_value(release, 'total_tracks', default=0) or 0,
'external_urls': _extract_lookup_value(release, 'external_urls', default={}) or {}, '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, 'track_count': typed_album.total_tracks or 0,
'owned': None, 'owned': None,
'track_completion': 'checking', 'track_completion': 'checking',
'explicit': typed_album.explicit,
} }
if typed_album.release_date: if typed_album.release_date:
card['release_date'] = 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, 'track_count': _extract_lookup_value(release, 'track_count', 'total_tracks', default=0) or 0,
'owned': None, 'owned': None,
'track_completion': 'checking', 'track_completion': 'checking',
'explicit': _extract_lookup_value(release, 'explicit'),
} }
if release_date: if release_date:

View file

@ -100,6 +100,8 @@ class Album:
label: Optional[str] = None # Record label / publisher label: Optional[str] = None # Record label / publisher
barcode: Optional[str] = None # UPC/EAN — Discogs/MusicBrainz only barcode: Optional[str] = None # UPC/EAN — Discogs/MusicBrainz only
explicit: Optional[bool] = None # True=explicit, False=clean, None=unknown
# Source provenance # Source provenance
source: str = '' # 'spotify' / 'itunes' / etc — set by converter source: str = '' # 'spotify' / 'itunes' / etc — set by converter
external_ids: Dict[str, str] = field(default_factory=dict) external_ids: Dict[str, str] = field(default_factory=dict)
@ -193,6 +195,8 @@ class Album:
release_date = release_date.split('T', 1)[0] release_date = release_date.split('T', 1)[0]
primary_genre = _str(raw.get('primaryGenreName')) 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( return cls(
id=_str(raw.get('collectionId')), id=_str(raw.get('collectionId')),
name=name, name=name,
@ -203,6 +207,7 @@ class Album:
image_url=_itunes_artwork(raw.get('artworkUrl100')), image_url=_itunes_artwork(raw.get('artworkUrl100')),
artist_id=artist_id, artist_id=artist_id,
genres=[primary_genre] if primary_genre else [], genres=[primary_genre] if primary_genre else [],
explicit=explicit,
source='itunes', source='itunes',
external_ids=external_ids, external_ids=external_ids,
external_urls=external_urls, external_urls=external_urls,
@ -238,6 +243,8 @@ class Album:
if raw.get('link'): if raw.get('link'):
external_urls['deezer'] = _str(raw['link']) external_urls['deezer'] = _str(raw['link'])
_el = raw.get('explicit_lyrics')
explicit = bool(_el) if _el is not None else None
return cls( return cls(
id=_str(raw.get('id')), id=_str(raw.get('id')),
name=_str(raw.get('title')), name=_str(raw.get('title')),
@ -251,6 +258,7 @@ class Album:
if isinstance(g, dict) and g.get('name')], if isinstance(g, dict) and g.get('name')],
label=_str(raw.get('label')) or None, label=_str(raw.get('label')) or None,
barcode=external_ids.get('upc'), barcode=external_ids.get('upc'),
explicit=explicit,
source='deezer', source='deezer',
external_ids=external_ids, external_ids=external_ids,
external_urls=external_urls, external_urls=external_urls,
@ -513,6 +521,8 @@ class Album:
if raw.get('soul_id'): if raw.get('soul_id'):
external_ids['soul'] = _str(raw['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( return cls(
id=_str(raw.get('id')), id=_str(raw.get('id')),
name=_str(raw.get('name') or raw.get('title')), name=_str(raw.get('name') or raw.get('title')),
@ -522,6 +532,7 @@ class Album:
album_type=_str(raw.get('album_type'), default='album'), album_type=_str(raw.get('album_type'), default='album'),
image_url=_str(raw.get('image_url') or raw.get('thumb_url')) or None, image_url=_str(raw.get('image_url') or raw.get('thumb_url')) or None,
artist_id=_str(raw.get('artist_id')) or None, artist_id=_str(raw.get('artist_id')) or None,
explicit=explicit,
source='hydrabase', source='hydrabase',
external_ids=external_ids, external_ids=external_ids,
) )

View file

@ -1859,7 +1859,7 @@ function createReleaseCard(release) {
content.className = "album-card-content"; content.className = "album-card-content";
const _esc = (s) => String(s || '').replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); const _esc = (s) => String(s || '').replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
content.innerHTML = ` 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>` : ''} ${yearText ? `<div class="album-card-year">${_esc(yearText)}</div>` : ''}
`; `;
card.appendChild(content); card.appendChild(content);
@ -2507,7 +2507,7 @@ function _renderDiscogCard(release, index, completionData) {
${statusIcon ? `<span class="discog-card-status">${statusIcon}</span>` : ''} ${statusIcon ? `<span class="discog-card-status">${statusIcon}</span>` : ''}
</div> </div>
<div class="discog-card-info"> <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 class="discog-card-meta">${year}${year && tracks ? ' · ' : ''}${tracks ? tracks + ' tracks' : ''}</div>
</div> </div>
<div class="discog-card-check"></div> <div class="discog-card-check"></div>

View file

@ -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; } .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 { .discog-card-check {
width: 20px; height: 20px; border-radius: 6px; flex-shrink: 0; width: 20px; height: 20px; border-radius: 6px; flex-shrink: 0;
border: 2px solid rgba(255,255,255,0.15); transition: all 0.15s ease; border: 2px solid rgba(255,255,255,0.15); transition: all 0.15s ease;