- backend/explore/coverart.go - backend/explore/explore.go - backend/explore/coverart_test.go - frontend/wailsjs/go/explore/Service.js - frontend/wailsjs/go/explore/Service.d.ts
228 lines
6.3 KiB
Go
228 lines
6.3 KiB
Go
package explore
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"sync"
|
|
|
|
"yellowjacket/backend/database"
|
|
)
|
|
|
|
// Service is the Wails-bound service for the explore feature.
|
|
// It owns the lifecycle of all explore-related components: the
|
|
// MusicBrainz client, ListenBrainz client, rate limiter, and
|
|
// response cache. Its exported methods form the binding surface
|
|
// that the frontend calls via generated TypeScript stubs.
|
|
type Service struct {
|
|
mb *MusicBrainzClient
|
|
lb *ListenBrainzClient
|
|
cache *Cache
|
|
logger *slog.Logger
|
|
ctx context.Context
|
|
}
|
|
|
|
// NewExploreService creates a Service backed by the given
|
|
// database. It instantiates the rate limiter, cache, MusicBrainz
|
|
// client, and ListenBrainz client internally.
|
|
func NewExploreService(logger *slog.Logger, db *database.DB) *Service {
|
|
cache := NewCache(db, logger.WithGroup("cache"))
|
|
limiter := NewRateLimiter()
|
|
mb := NewMusicBrainzClient(cache, logger.WithGroup("musicbrainz"))
|
|
lb := NewListenBrainzClient(limiter, cache, logger.WithGroup("listenbrainz"))
|
|
|
|
logger.Info("explore service created")
|
|
|
|
return &Service{
|
|
mb: mb,
|
|
lb: lb,
|
|
cache: cache,
|
|
logger: logger,
|
|
ctx: context.Background(),
|
|
}
|
|
}
|
|
|
|
// SetContext injects the Wails runtime context. Called from
|
|
// OnStartup after the Wails runtime is initialised.
|
|
func (e *Service) SetContext(ctx context.Context) {
|
|
e.ctx = ctx
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// MusicBrainz search
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// SearchArtists queries MusicBrainz for artists matching the query.
|
|
func (e *Service) SearchArtists(query string) ([]MBArtist, error) {
|
|
return e.mb.SearchArtists(e.ctx, query, 0)
|
|
}
|
|
|
|
// SearchReleaseGroups queries MusicBrainz for release groups matching the query.
|
|
func (e *Service) SearchReleaseGroups(query string) ([]MBReleaseGroup, error) {
|
|
return e.mb.SearchReleaseGroups(e.ctx, query, 0)
|
|
}
|
|
|
|
// SearchRecordings queries MusicBrainz for recordings matching the query.
|
|
func (e *Service) SearchRecordings(query string) ([]MBRecording, error) {
|
|
return e.mb.SearchRecordings(e.ctx, query, 0)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// MusicBrainz lookup
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// LookupArtist fetches a single MusicBrainz artist by MBID.
|
|
func (e *Service) LookupArtist(mbid string) (*MBArtist, error) {
|
|
return e.mb.LookupArtist(e.ctx, mbid)
|
|
}
|
|
|
|
// LookupReleaseGroup fetches a single MusicBrainz release group by MBID.
|
|
func (e *Service) LookupReleaseGroup(mbid string) (*MBReleaseGroup, error) {
|
|
return e.mb.LookupReleaseGroup(e.ctx, mbid)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// MusicBrainz browse
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// BrowseReleaseGroups fetches release groups for a given artist MBID.
|
|
func (e *Service) BrowseReleaseGroups(artistMBID string) ([]MBReleaseGroup, error) {
|
|
return e.mb.BrowseReleaseGroups(e.ctx, artistMBID)
|
|
}
|
|
|
|
// BrowseReleases fetches releases for a given release group MBID.
|
|
func (e *Service) BrowseReleases(releaseGroupMBID string) ([]MBRelease, error) {
|
|
return e.mb.BrowseReleases(e.ctx, releaseGroupMBID)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ListenBrainz
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// TopRecordingsForArtist returns the most-listened recordings for an artist.
|
|
func (e *Service) TopRecordingsForArtist(artistMBID string) ([]LBTopRecording, error) {
|
|
return e.lb.TopRecordingsForArtist(e.ctx, artistMBID)
|
|
}
|
|
|
|
// SimilarArtists returns artists similar to the given artist MBID.
|
|
func (e *Service) SimilarArtists(artistMBID string) ([]LBSimilarArtist, error) {
|
|
return e.lb.SimilarArtists(e.ctx, artistMBID)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Cover Art Archive
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// CoverArtURL returns the Cover Art Archive URL for a release's
|
|
// front cover at the default 250px size.
|
|
func (e *Service) CoverArtURL(releaseMBID string) string {
|
|
return CoverArtURL(releaseMBID)
|
|
}
|
|
|
|
// CoverArtGroupURL returns the Cover Art Archive URL for a release
|
|
// group's front cover at the default 250px size. This is the
|
|
// correct endpoint for search results, which return release group
|
|
// MBIDs rather than individual release MBIDs.
|
|
func (e *Service) CoverArtGroupURL(releaseGroupMBID string) string {
|
|
return CoverArtGroupURL(releaseGroupMBID)
|
|
}
|
|
|
|
// Search concurrently queries MusicBrainz for artists, release
|
|
// groups, and recordings matching the query, returning aggregated
|
|
// results in a single round-trip. If any sub-search fails the
|
|
// error is logged and the remaining results are still returned.
|
|
func (e *Service) Search(query string) (*MBSearchResult, error) {
|
|
e.logger.Info("search started", "query", query)
|
|
|
|
var (
|
|
result MBSearchResult
|
|
mu sync.Mutex
|
|
wg sync.WaitGroup
|
|
)
|
|
|
|
type searchFunc struct {
|
|
name string
|
|
fn func()
|
|
}
|
|
|
|
searches := []searchFunc{
|
|
{
|
|
name: "artists",
|
|
fn: func() {
|
|
artists, err := e.mb.SearchArtists(e.ctx, query, 0)
|
|
if err != nil {
|
|
e.logger.Warn("search sub-call failed",
|
|
"entity", "artists",
|
|
"query", query,
|
|
"error", err,
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
mu.Lock()
|
|
result.Artists = artists
|
|
mu.Unlock()
|
|
},
|
|
},
|
|
{
|
|
name: "releaseGroups",
|
|
fn: func() {
|
|
rgs, err := e.mb.SearchReleaseGroups(e.ctx, query, 0)
|
|
if err != nil {
|
|
e.logger.Warn("search sub-call failed",
|
|
"entity", "releaseGroups",
|
|
"query", query,
|
|
"error", err,
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
mu.Lock()
|
|
result.ReleaseGroups = rgs
|
|
mu.Unlock()
|
|
},
|
|
},
|
|
{
|
|
name: "recordings",
|
|
fn: func() {
|
|
recs, err := e.mb.SearchRecordings(e.ctx, query, 0)
|
|
if err != nil {
|
|
e.logger.Warn("search sub-call failed",
|
|
"entity", "recordings",
|
|
"query", query,
|
|
"error", err,
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
mu.Lock()
|
|
result.Recordings = recs
|
|
mu.Unlock()
|
|
},
|
|
},
|
|
}
|
|
|
|
wg.Add(len(searches))
|
|
|
|
for _, s := range searches {
|
|
go func() {
|
|
defer wg.Done()
|
|
|
|
s.fn()
|
|
}()
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
e.logger.Info("search completed",
|
|
"query", query,
|
|
"artists", len(result.Artists),
|
|
"releaseGroups", len(result.ReleaseGroups),
|
|
"recordings", len(result.Recordings),
|
|
)
|
|
|
|
return &result, nil
|
|
}
|