feat(S02/T01): Added CoverArtGroupURL for release-group cover art, conc…

- 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
This commit is contained in:
2026-03-23 17:31:01 -04:00
parent 1ff53d8084
commit 677ed01d89
5 changed files with 316 additions and 1 deletions
+19 -1
View File
@@ -2,7 +2,10 @@ package explore
import "fmt"
const coverArtBaseURL = "https://coverartarchive.org/release"
const (
coverArtBaseURL = "https://coverartarchive.org/release"
coverArtGroupBaseURL = "https://coverartarchive.org/release-group"
)
// CoverArtURL returns the Cover Art Archive URL for the 250px
// front cover of the given release MBID.
@@ -16,3 +19,18 @@ func CoverArtURL(releaseMBID string) string {
func CoverArtURLSize(releaseMBID string, size int) string {
return fmt.Sprintf("%s/%s/front-%d", coverArtBaseURL, releaseMBID, size)
}
// CoverArtGroupURL returns the Cover Art Archive URL for the 250px
// front cover of the given release group MBID. Search results
// return release group MBIDs (not release MBIDs), so this is the
// correct endpoint for displaying cover art in search results.
func CoverArtGroupURL(releaseGroupMBID string) string {
return fmt.Sprintf("%s/%s/front-250", coverArtGroupBaseURL, releaseGroupMBID)
}
// CoverArtGroupURLSize returns the Cover Art Archive URL for the
// front cover of the given release group MBID at the specified
// pixel size. Common sizes are 250, 500, and 1200.
func CoverArtGroupURLSize(releaseGroupMBID string, size int) string {
return fmt.Sprintf("%s/%s/front-%d", coverArtGroupBaseURL, releaseGroupMBID, size)
}
+45
View File
@@ -50,3 +50,48 @@ func TestCoverArtURLSize(t *testing.T) {
}
}
}
func TestCoverArtGroupURL(t *testing.T) {
t.Parallel()
mbid := "abc-123"
got := explore.CoverArtGroupURL(mbid)
want := "https://coverartarchive.org/release-group/abc-123/front-250"
if got != want {
t.Errorf("CoverArtGroupURL(%q) = %q, want %q", mbid, got, want)
}
}
func TestCoverArtGroupURLSize(t *testing.T) {
t.Parallel()
mbid := "abc-123"
tests := []struct {
size int
want string
}{
{
250,
"https://coverartarchive.org/release-group/abc-123/front-250",
},
{
500,
"https://coverartarchive.org/release-group/abc-123/front-500",
},
{
1200,
"https://coverartarchive.org/release-group/abc-123/front-1200",
},
}
for _, tt := range tests {
got := explore.CoverArtGroupURLSize(mbid, tt.size)
if got != tt.want {
t.Errorf("CoverArtGroupURLSize(%q, %d) = %q, want %q",
mbid, tt.size, got, tt.want)
}
}
}
+109
View File
@@ -3,6 +3,7 @@ package explore
import (
"context"
"log/slog"
"sync"
"yellowjacket/backend/database"
)
@@ -116,3 +117,111 @@ func (e *Service) SimilarArtists(artistMBID string) ([]LBSimilarArtist, error) {
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
}
+92
View File
@@ -0,0 +1,92 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
// -- Explore types (inline, matching Go JSON tags) ------------------
export interface MBArtist {
mbid: string;
name: string;
sortName: string;
type: string;
country: string;
disambiguation: string;
score: number;
}
export interface MBReleaseGroup {
mbid: string;
title: string;
primaryType: string;
secondaryTypes?: string[];
firstReleaseDate: string;
artistCredit: string;
}
export interface MBRecording {
mbid: string;
title: string;
length: number;
artistCredit: string;
score: number;
}
export interface MBRelease {
mbid: string;
title: string;
date: string;
country: string;
status: string;
tracks?: MBTrack[];
}
export interface MBTrack {
position: number;
title: string;
length: number;
mbid: string;
}
export interface MBSearchResult {
artists?: MBArtist[];
releaseGroups?: MBReleaseGroup[];
recordings?: MBRecording[];
}
export interface LBTopRecording {
recordingMbid: string;
artistName: string;
trackName: string;
totalListenCount: number;
}
export interface LBSimilarArtist {
artistMbid: string;
name: string;
score: number;
}
// -- Service methods ------------------------------------------------
export function Search(arg1:string):Promise<MBSearchResult>;
export function SearchArtists(arg1:string):Promise<Array<MBArtist>>;
export function SearchReleaseGroups(arg1:string):Promise<Array<MBReleaseGroup>>;
export function SearchRecordings(arg1:string):Promise<Array<MBRecording>>;
export function LookupArtist(arg1:string):Promise<MBArtist>;
export function LookupReleaseGroup(arg1:string):Promise<MBReleaseGroup>;
export function BrowseReleaseGroups(arg1:string):Promise<Array<MBReleaseGroup>>;
export function BrowseReleases(arg1:string):Promise<Array<MBRelease>>;
export function TopRecordingsForArtist(arg1:string):Promise<Array<LBTopRecording>>;
export function SimilarArtists(arg1:string):Promise<Array<LBSimilarArtist>>;
export function CoverArtURL(arg1:string):Promise<string>;
export function CoverArtGroupURL(arg1:string):Promise<string>;
+51
View File
@@ -0,0 +1,51 @@
// @ts-check
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
export function Search(arg1) {
return window['go']['explore']['Service']['Search'](arg1);
}
export function SearchArtists(arg1) {
return window['go']['explore']['Service']['SearchArtists'](arg1);
}
export function SearchReleaseGroups(arg1) {
return window['go']['explore']['Service']['SearchReleaseGroups'](arg1);
}
export function SearchRecordings(arg1) {
return window['go']['explore']['Service']['SearchRecordings'](arg1);
}
export function LookupArtist(arg1) {
return window['go']['explore']['Service']['LookupArtist'](arg1);
}
export function LookupReleaseGroup(arg1) {
return window['go']['explore']['Service']['LookupReleaseGroup'](arg1);
}
export function BrowseReleaseGroups(arg1) {
return window['go']['explore']['Service']['BrowseReleaseGroups'](arg1);
}
export function BrowseReleases(arg1) {
return window['go']['explore']['Service']['BrowseReleases'](arg1);
}
export function TopRecordingsForArtist(arg1) {
return window['go']['explore']['Service']['TopRecordingsForArtist'](arg1);
}
export function SimilarArtists(arg1) {
return window['go']['explore']['Service']['SimilarArtists'](arg1);
}
export function CoverArtURL(arg1) {
return window['go']['explore']['Service']['CoverArtURL'](arg1);
}
export function CoverArtGroupURL(arg1) {
return window['go']['explore']['Service']['CoverArtGroupURL'](arg1);
}