diff --git a/backend/database/sql/sqlcgen/models.go b/backend/database/sql/sqlcgen/models.go index e860e49..9df7e55 100644 --- a/backend/database/sql/sqlcgen/models.go +++ b/backend/database/sql/sqlcgen/models.go @@ -49,6 +49,15 @@ type CoverArt struct { MimeType string } +type ExploreCache struct { + UrlKey string + Response string + Mbid sql.NullString + EntityType sql.NullString + ExpiresAt time.Time + CreatedAt time.Time +} + type FileType struct { ID int64 Extension string diff --git a/backend/explore/explore.go b/backend/explore/explore.go index 42785d3..b98b5f8 100644 --- a/backend/explore/explore.go +++ b/backend/explore/explore.go @@ -24,6 +24,7 @@ type Service struct { index *SearchIndex artProxy *CoverArtProxy artistImg *ArtistImageProvider + libMBID *LibraryMBIDIndex logger *slog.Logger ctx context.Context } @@ -41,6 +42,7 @@ func NewExploreService(logger *slog.Logger, db *database.DB) *Service { db, cache, NewRateLimiter(), logger.WithGroup("artist-image"), ) index := NewSearchIndex(db, lb, artistImg, logger.WithGroup("search-index")) + libMBID := NewLibraryMBIDIndex(db) logger.Info("explore service created") @@ -51,6 +53,7 @@ func NewExploreService(logger *slog.Logger, db *database.DB) *Service { index: index, artProxy: artProxy, artistImg: artistImg, + libMBID: libMBID, logger: logger, ctx: context.Background(), } @@ -198,6 +201,19 @@ func (e *Service) GetArtistImageURL(artistMBID string) string { return e.artistImg.GetArtistImage(artistMBID) } +// CheckLibraryMBIDs returns which of the given MBIDs exist in the +// local music library. Returns a map of MBID → entity type +// ("artist", "release_group", "recording"). +func (e *Service) CheckLibraryMBIDs(mbids []string) map[string]string { + return e.libMBID.CheckMBIDs(mbids) +} + +// GetArtistMBID returns the MusicBrainz ID for a local library +// artist by name, or "" if not found or no MBID tagged. +func (e *Service) GetArtistMBID(artistName string) string { + return e.libMBID.GetArtistMBID(artistName) +} + // Search concurrently queries MusicBrainz for artists, release // groups, and recordings matching the query, then boosts results // using ListenBrainz popularity data. The final score blends diff --git a/backend/explore/searchindex.go b/backend/explore/searchindex.go index be20e01..ededa88 100644 --- a/backend/explore/searchindex.go +++ b/backend/explore/searchindex.go @@ -683,8 +683,11 @@ func (si *SearchIndex) buildTier3Library( } } - // Read local library artist names. - libRows, err := si.db.QueryContext("SELECT DISTINCT name FROM artists") + // Read local library artists — prefer direct MBIDs from tags, + // fall back to name matching against the sitewide/index map. + libRows, err := si.db.QueryContext( + "SELECT DISTINCT name, mbid FROM artists", + ) if err != nil { si.logger.Warn("search index: library artists query failed", "error", err) @@ -693,18 +696,35 @@ func (si *SearchIndex) buildTier3Library( defer func() { _ = libRows.Close() }() - // Collect MBIDs for matched library artists. var matched []lbSitewideArtist var resolvedMBIDs []string for libRows.Next() { var name string - if err := libRows.Scan(&name); err != nil { + + var mbidPtr *string + + if err := libRows.Scan(&name, &mbidPtr); err != nil { continue } - // Normalize: strip "feat." suffixes. + // Direct MBID from tags — most reliable. + if mbidPtr != nil && *mbidPtr != "" { + mbid := *mbidPtr + resolvedMBIDs = append(resolvedMBIDs, mbid) + + if !indexed[mbid] { + matched = append(matched, lbSitewideArtist{ + ArtistMBID: mbid, + ArtistName: name, + }) + } + + continue + } + + // Fall back to name matching. normalized := strings.ToLower(name) if idx := strings.Index(normalized, " feat."); idx >= 0 { normalized = normalized[:idx] @@ -719,7 +739,6 @@ func (si *SearchIndex) buildTier3Library( if a, ok := nameMap[normalized]; ok { resolvedMBIDs = append(resolvedMBIDs, a.ArtistMBID) - // Only index if not already in the index from Tier 2. if !indexed[a.ArtistMBID] { matched = append(matched, a) } diff --git a/frontend/src/components/artist-details/artist-details.ts b/frontend/src/components/artist-details/artist-details.ts index 4aca04e..5180d26 100644 --- a/frontend/src/components/artist-details/artist-details.ts +++ b/frontend/src/components/artist-details/artist-details.ts @@ -6,6 +6,7 @@ import { } from 'lit/decorators.js'; import { library } from '@go/models'; import { LibraryController } from '@store/controllers/library-controller'; +import { GetArtistImageURL, GetArtistMBID } from '@go/explore/Service'; import '@awesome.me/webawesome/dist/components/icon/icon.js'; import '@components/cover-grid/cover-grid.js'; import { designTokens } from '../../styles/tokens.css'; @@ -18,12 +19,18 @@ export class ArtistDetails extends LitElement { @property({ type: String, attribute: 'artist-name' }) artistName = ''; + @property({ type: String, attribute: 'artist-mbid' }) + artistMBID = ''; + @state() private albums: library.Album[] = []; @state() private loading = true; + @state() + private artistImageURL = ''; + private libraryCtrl = new LibraryController(this); /** Tracks the store's cached array reference to detect refreshes. */ @@ -99,6 +106,12 @@ export class ArtistDetails extends LitElement { flex-shrink: 0; } + .artist-avatar img { + width: 100%; + height: 100%; + object-fit: cover; + } + .artist-avatar .initial { color: var( --yj-text-secondary, @@ -156,6 +169,7 @@ export class ArtistDetails extends LitElement { override connectedCallback() { super.connectedCallback(); this.loadAlbums(); + this.loadArtistImage(); } override updated() { @@ -174,6 +188,31 @@ export class ArtistDetails extends LitElement { * Data loading * ================================================================ */ + private async loadArtistImage() { + // Resolve MBID from tags if not provided via attribute. + let mbid = this.artistMBID; + + if (!mbid && this.artistName) { + try { + mbid = await GetArtistMBID(this.artistName); + } catch { + return; + } + } + + if (!mbid) return; + + try { + const url = await GetArtistImageURL(mbid); + + if (url) { + this.artistImageURL = url; + } + } catch { + // No image — avatar stays as initial letter. + } + } + private async loadAlbums() { if (!this.artistId) return; @@ -293,11 +332,14 @@ export class ArtistDetails extends LitElement { >