feat: multi-source artist images with thumbnails + grid integration
Complete rewrite of the artist image pipeline:
STORAGE:
- Migration 16: artist_images table tracking source, URL, path,
primary flag, dimensions per image (up to 10 per artist)
- Directory structure: artist-images/{mbid[:2]}/{mbid}/ with
primary.jpg + primary_sm.jpg/_md.jpg/_lg.jpg thumbnails
- Miss marker (.miss file) prevents re-fetching artists with no image
SOURCES (priority order):
1. MusicBrainz direct image relations (Wikimedia Commons)
2. Wikidata P18 property (Wikimedia Commons)
3. Wikipedia lead image (NEW — via Wikidata sitelinks → Wikipedia API)
Each source is checked, deduplicated, and the first available
image becomes the primary with sm/md/lg thumbnail generation
(100px/200px/400px, matching cover art tier sizes).
ASSET SERVING:
- /artist-images/ path registered with Wails asset handler
- Serves files via http.FileServer from the artist-images directory
- Same pattern as /covers/ for cover art
ARTIST MODEL:
- Artist struct gains ImageSmall/ImageMedium/ImageLarge fields
- resolveArtistImages does bulk MBID lookup → disk stat for each
- Populated in GetAllArtists and GetAllArtistsByLibrary
GRID VIEW:
- artists-view uses model URLs directly (no more base64 data URLs)
- Size selection based on imageSize * devicePixelRatio (like cover-grid)
- Removed batch GetArtistImages call and in-memory cache — no longer needed
This commit is contained in:
@@ -16,7 +16,6 @@ import {
|
||||
GetAlbumsByArtistByLibrary,
|
||||
GetAlbumTracksByLibrary,
|
||||
} from '@go/library/Library';
|
||||
import { GetArtistImageURL, GetArtistMBID, GetArtistImages } from '@go/explore/Service';
|
||||
import { library } from '@go/models';
|
||||
import { LibraryController } from '@store/controllers/library-controller';
|
||||
import { SearchController } from '@store/controllers/search-controller';
|
||||
@@ -177,8 +176,6 @@ export class ArtistsView
|
||||
private cachedGridEntries: ArtistEntry[] = [];
|
||||
private prevFilterArtists: library.Artist[] = [];
|
||||
private prevFilterTerm = '';
|
||||
private artistImageCache = new Map<string, string>();
|
||||
private artistImageLoading = new Set<string>();
|
||||
|
||||
/**
|
||||
* Recompute the filtered-artists and grid-entries
|
||||
@@ -439,8 +436,6 @@ export class ArtistsView
|
||||
) {
|
||||
this.lastArtistsRef = cached;
|
||||
this.loadArtists();
|
||||
this.imagesBatchLoaded = false;
|
||||
void this.loadArtistImagesBatch();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -456,9 +451,6 @@ export class ArtistsView
|
||||
await this.libraryCtrl.getArtists();
|
||||
|
||||
this.artists = artists ?? [];
|
||||
|
||||
// Batch load artist images after artists are loaded.
|
||||
void this.loadArtistImagesBatch();
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Error loading artists:',
|
||||
@@ -990,91 +982,32 @@ export class ArtistsView
|
||||
* Helpers
|
||||
* ================================================================ */
|
||||
|
||||
private renderArtistAvatar(name: string) {
|
||||
const imageURL = this.artistImageCache.get(name);
|
||||
private renderArtistAvatar(artist: library.Artist) {
|
||||
const needed = (this.imageSize ?? 176) * window.devicePixelRatio;
|
||||
let imageURL = '';
|
||||
|
||||
if (needed <= 100) {
|
||||
imageURL = artist.ImageSmall || artist.ImageMedium || artist.ImageLarge || '';
|
||||
} else if (needed <= 200) {
|
||||
imageURL = artist.ImageMedium || artist.ImageLarge || '';
|
||||
} else {
|
||||
imageURL = artist.ImageLarge || '';
|
||||
}
|
||||
|
||||
if (imageURL) {
|
||||
return html`<img
|
||||
class="avatar-image"
|
||||
src="${imageURL}"
|
||||
alt="${name}"
|
||||
alt="${artist.Name}"
|
||||
loading="lazy"
|
||||
/>`;
|
||||
}
|
||||
|
||||
return html`<span class="avatar-placeholder">
|
||||
${this.getArtistInitial(name)}
|
||||
${this.getArtistInitial(artist.Name)}
|
||||
</span>`;
|
||||
}
|
||||
|
||||
private imagesBatchLoaded = false;
|
||||
|
||||
/**
|
||||
* Batch load all artist images in one Wails call.
|
||||
* Only returns already-cached images (from the disk cache
|
||||
* populated by the index build). Uncached artists fall back
|
||||
* to the initial letter.
|
||||
*/
|
||||
private async loadArtistImagesBatch() {
|
||||
if (this.imagesBatchLoaded) return;
|
||||
|
||||
this.imagesBatchLoaded = true;
|
||||
|
||||
const artists = this.libraryCtrl.cachedArtists;
|
||||
|
||||
if (!artists || artists.length === 0) return;
|
||||
|
||||
const names = artists.map((a) => a.Name);
|
||||
|
||||
try {
|
||||
const images = await GetArtistImages(names);
|
||||
|
||||
if (images && Object.keys(images).length > 0) {
|
||||
for (const [name, url] of Object.entries(images)) {
|
||||
if (url) {
|
||||
this.artistImageCache.set(name, url);
|
||||
}
|
||||
}
|
||||
|
||||
this.requestUpdate();
|
||||
}
|
||||
} catch {
|
||||
// Non-critical.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load artist image for a single artist on-demand (fallback
|
||||
* for artists not resolved by the batch call).
|
||||
*/
|
||||
private loadArtistImage(name: string) {
|
||||
if (this.artistImageCache.has(name) || this.artistImageLoading.has(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.artistImageLoading.add(name);
|
||||
|
||||
GetArtistMBID(name)
|
||||
.then((mbid) => {
|
||||
if (!mbid) return Promise.resolve('');
|
||||
|
||||
return GetArtistImageURL(mbid);
|
||||
})
|
||||
.then((url) => {
|
||||
if (url) {
|
||||
this.artistImageCache.set(name, url);
|
||||
this.requestUpdate();
|
||||
} else {
|
||||
this.artistImageCache.set(name, '');
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
this.artistImageCache.set(name, '');
|
||||
})
|
||||
.finally(() => {
|
||||
this.artistImageLoading.delete(name);
|
||||
});
|
||||
}
|
||||
|
||||
private getArtistInitial(
|
||||
name: string,
|
||||
): string {
|
||||
@@ -1147,7 +1080,7 @@ export class ArtistsView
|
||||
}}
|
||||
>
|
||||
<div class="avatar-container">
|
||||
${this.renderArtistAvatar(artist.Name)}
|
||||
${this.renderArtistAvatar(artist)}
|
||||
</div>
|
||||
<div
|
||||
class="artist-name"
|
||||
|
||||
Reference in New Issue
Block a user