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:
@@ -1,7 +1,7 @@
|
||||
import { LitElement, html, css, nothing } from 'lit';
|
||||
import { customElement, state, query as litQuery } from 'lit/decorators.js';
|
||||
import { designTokens } from '../../styles/tokens.css';
|
||||
import { Search, GetThumbnails } from '@go/explore/Service';
|
||||
import { Search, GetThumbnails, GetArtistImageURL } from '@go/explore/Service';
|
||||
import type { ThumbnailRequest } from '@go/explore/Service';
|
||||
import type {
|
||||
MBSearchResult,
|
||||
@@ -76,6 +76,7 @@ export class ExploreView extends LitElement {
|
||||
private searchVersion = 0;
|
||||
private debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private thumbnailCache = new Map<string, string>();
|
||||
private artistImageCache = new Map<string, string>();
|
||||
|
||||
@litQuery('input') private inputEl!: HTMLInputElement;
|
||||
|
||||
@@ -293,6 +294,13 @@ export class ExploreView extends LitElement {
|
||||
text-transform: uppercase;
|
||||
user-select: none;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.artist-avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.artist-name {
|
||||
@@ -568,6 +576,7 @@ export class ExploreView extends LitElement {
|
||||
|
||||
this.results = result;
|
||||
this.loadThumbnails();
|
||||
this.loadArtistImages();
|
||||
|
||||
const elapsed = (performance.now() - startTime).toFixed(0);
|
||||
console.log(
|
||||
@@ -653,6 +662,32 @@ export class ExploreView extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load artist images for all visible artist cards. Each call
|
||||
* is async and updates the cache + re-renders on success.
|
||||
*/
|
||||
private loadArtistImages() {
|
||||
if (!this.results?.artists?.length) return;
|
||||
|
||||
for (const a of this.results.artists) {
|
||||
if (this.artistImageCache.has(a.mbid)) continue;
|
||||
|
||||
// Mark as loading.
|
||||
this.artistImageCache.set(a.mbid, '');
|
||||
|
||||
GetArtistImageURL(a.mbid)
|
||||
.then((url) => {
|
||||
if (url) {
|
||||
this.artistImageCache.set(a.mbid, url);
|
||||
this.requestUpdate();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// No image — leave empty string.
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Top Results ── */
|
||||
|
||||
private getTopResults(): ScoredItem[] {
|
||||
@@ -905,7 +940,12 @@ export class ExploreView extends LitElement {
|
||||
class="artist-avatar"
|
||||
style="background: hsl(${hue}, 45%, 35%)"
|
||||
>
|
||||
${(a.englishName || a.name).charAt(0).toUpperCase()}
|
||||
${this.artistImageCache.get(a.mbid)
|
||||
? html`<img
|
||||
src="${this.artistImageCache.get(a.mbid)}"
|
||||
alt="${a.englishName || a.name}"
|
||||
/>`
|
||||
: (a.englishName || a.name).charAt(0).toUpperCase()}
|
||||
</div>
|
||||
<div class="artist-name" title="${a.englishName || a.name}">
|
||||
${a.englishName || a.name}
|
||||
|
||||
Reference in New Issue
Block a user