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:
2026-03-29 08:33:32 -04:00
parent 7ec9785463
commit 19a803d1fc
10 changed files with 668 additions and 317 deletions
+70 -2
View File
@@ -4,12 +4,15 @@ import (
"database/sql"
"errors"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"yellowjacket/backend/coverart"
"yellowjacket/backend/database/sql/sqlcgen"
"yellowjacket/backend/system"
)
// Sentinel errors for library queries.
@@ -142,8 +145,11 @@ func (l *Library) GetTrackMBIDs(filePath string) TrackMBIDs {
// Artist represents an artist in the library.
type Artist struct {
ID int64
Name string
ID int64
Name string
ImageSmall string
ImageMedium string
ImageLarge string
}
// Album represents an album for the cover grid display.
@@ -366,9 +372,69 @@ func (l *Library) GetAllArtists() ([]Artist, error) {
})
}
// Resolve artist image URLs from the disk cache.
l.resolveArtistImages(artists)
return artists, nil
}
// resolveArtistImages populates ImageSmall/Medium/Large for artists
// that have cached images on disk. Does a bulk MBID lookup from the
// artists table, then checks the artist-images directory for each.
func (l *Library) resolveArtistImages(artists []Artist) {
if len(artists) == 0 {
return
}
dataDir, err := system.GetUserDataDirPath()
if err != nil {
return
}
baseDir := filepath.Join(dataDir, "artist-images")
// Bulk load name→mbid from the artists table.
rows, err := l.db.QueryContext(
"SELECT name, mbid FROM artists WHERE mbid IS NOT NULL AND mbid != ''",
)
if err != nil {
return
}
defer func() { _ = rows.Close() }()
mbidMap := make(map[string]string)
for rows.Next() {
var name, mbid string
if err := rows.Scan(&name, &mbid); err == nil {
mbidMap[name] = mbid
}
}
for i := range artists {
mbid, ok := mbidMap[artists[i].Name]
if !ok || len(mbid) < 2 {
continue
}
dir := filepath.Join(baseDir, mbid[:2], mbid)
prefix := "/artist-images/" + mbid[:2] + "/" + mbid + "/"
if _, err := os.Stat(filepath.Join(dir, "primary_sm.jpg")); err == nil {
artists[i].ImageSmall = prefix + "primary_sm.jpg"
}
if _, err := os.Stat(filepath.Join(dir, "primary_md.jpg")); err == nil {
artists[i].ImageMedium = prefix + "primary_md.jpg"
}
if _, err := os.Stat(filepath.Join(dir, "primary_lg.jpg")); err == nil {
artists[i].ImageLarge = prefix + "primary_lg.jpg"
}
}
}
// GetAlbumsByArtist returns all albums where the given artist is the album artist.
func (l *Library) GetAlbumsByArtist(
artistID int64,
@@ -646,6 +712,8 @@ func (l *Library) GetAllArtistsByLibrary(
})
}
l.resolveArtistImages(artists)
return artists, nil
}