feat: artist images from MusicBrainz/Wikidata/Wikimedia Commons
Add ArtistImageProvider that resolves artist MBIDs to photo URLs: 1. MB url-rels 'image' type → extract Commons filename → thumb URL 2. MB url-rels 'wikidata' type → Wikidata P18 property → thumb URL 3. No image → falls back to initial-letter avatar Wikimedia Commons thumb URLs constructed via MD5 hash bucketing (standard Commons URL scheme). Results cached in explore_cache with 30-day TTL — subsequent lookups are instant. Frontend: search results and artist detail page show artist photos in the circular avatar when available. Images load async and replace the initial-letter fallback on arrival. Artist detail page fires the image fetch alongside the other 4 parallel data loads. Architecture supports adding more sources (fanart.tv, etc.) by extending the resolve() method's source chain.
This commit is contained in:
@@ -6,6 +6,7 @@ import {
|
||||
BrowseReleaseGroups,
|
||||
TopRecordingsForArtist,
|
||||
SimilarArtists,
|
||||
GetArtistImageURL,
|
||||
} from '@go/explore/Service';
|
||||
import type {
|
||||
MBArtist,
|
||||
@@ -88,6 +89,7 @@ export class ExploreArtistDetails extends LitElement {
|
||||
@state() private errorReleases = '';
|
||||
@state() private similarArtists: LBSimilarArtist[] = [];
|
||||
@state() private loadingSimilar = true;
|
||||
@state() private artistImageURL = '';
|
||||
|
||||
/* ── Styles ── */
|
||||
|
||||
@@ -149,6 +151,13 @@ export class ExploreArtistDetails extends LitElement {
|
||||
user-select: none;
|
||||
flex-shrink: 0;
|
||||
line-height: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.artist-avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.artist-info {
|
||||
@@ -484,7 +493,7 @@ export class ExploreArtistDetails extends LitElement {
|
||||
`[explore-artist] loading: "${this.artistName}" (${mbid})`,
|
||||
);
|
||||
|
||||
// Fire all four requests in parallel — each section is independent.
|
||||
// Fire all five requests in parallel — each section is independent.
|
||||
const [artistResult, tracksResult, releasesResult, similarResult] =
|
||||
await Promise.allSettled([
|
||||
this.fetchArtist(mbid),
|
||||
@@ -493,6 +502,9 @@ export class ExploreArtistDetails extends LitElement {
|
||||
this.fetchSimilarArtists(mbid),
|
||||
]);
|
||||
|
||||
// Artist image is fire-and-forget — doesn't block the page.
|
||||
this.fetchArtistImage(mbid);
|
||||
|
||||
const summary = [
|
||||
`artist=${artistResult.status}`,
|
||||
`tracks=${tracksResult.status}`,
|
||||
@@ -562,6 +574,17 @@ export class ExploreArtistDetails extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private async fetchArtistImage(mbid: string) {
|
||||
try {
|
||||
const url = await GetArtistImageURL(mbid);
|
||||
if (url) {
|
||||
this.artistImageURL = url;
|
||||
}
|
||||
} catch {
|
||||
// No image available — avatar stays as initial letter.
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Navigation ── */
|
||||
|
||||
private navigateBack() {
|
||||
@@ -709,7 +732,12 @@ export class ExploreArtistDetails extends LitElement {
|
||||
class="artist-avatar"
|
||||
style="background: hsl(${hue}, 45%, 35%)"
|
||||
>
|
||||
${this.getInitial(this.displayName)}
|
||||
${this.artistImageURL
|
||||
? html`<img
|
||||
src="${this.artistImageURL}"
|
||||
alt="${this.displayName}"
|
||||
/>`
|
||||
: this.getInitial(this.displayName)}
|
||||
</div>
|
||||
<div class="artist-info">
|
||||
<h1 class="artist-title" title="${this.displayName}">
|
||||
|
||||
Reference in New Issue
Block a user