From 677ed01d8916c28999b3c33ef5df8033a551fab9 Mon Sep 17 00:00:00 2001 From: Caleb Allen Date: Mon, 23 Mar 2026 17:31:01 -0400 Subject: [PATCH] =?UTF-8?q?feat(S02/T01):=20Added=20CoverArtGroupURL=20for?= =?UTF-8?q?=20release-group=20cover=20art,=20conc=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- backend/explore/coverart.go | 20 ++++- backend/explore/coverart_test.go | 45 ++++++++++ backend/explore/explore.go | 109 +++++++++++++++++++++++ frontend/wailsjs/go/explore/Service.d.ts | 92 +++++++++++++++++++ frontend/wailsjs/go/explore/Service.js | 51 +++++++++++ 5 files changed, 316 insertions(+), 1 deletion(-) create mode 100644 frontend/wailsjs/go/explore/Service.d.ts create mode 100644 frontend/wailsjs/go/explore/Service.js diff --git a/backend/explore/coverart.go b/backend/explore/coverart.go index 120c553..cf7f252 100644 --- a/backend/explore/coverart.go +++ b/backend/explore/coverart.go @@ -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) +} diff --git a/backend/explore/coverart_test.go b/backend/explore/coverart_test.go index 41095bb..616f393 100644 --- a/backend/explore/coverart_test.go +++ b/backend/explore/coverart_test.go @@ -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) + } + } +} diff --git a/backend/explore/explore.go b/backend/explore/explore.go index 7f402f9..b3fcca1 100644 --- a/backend/explore/explore.go +++ b/backend/explore/explore.go @@ -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 +} diff --git a/frontend/wailsjs/go/explore/Service.d.ts b/frontend/wailsjs/go/explore/Service.d.ts new file mode 100644 index 0000000..7af98c8 --- /dev/null +++ b/frontend/wailsjs/go/explore/Service.d.ts @@ -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; + +export function SearchArtists(arg1:string):Promise>; + +export function SearchReleaseGroups(arg1:string):Promise>; + +export function SearchRecordings(arg1:string):Promise>; + +export function LookupArtist(arg1:string):Promise; + +export function LookupReleaseGroup(arg1:string):Promise; + +export function BrowseReleaseGroups(arg1:string):Promise>; + +export function BrowseReleases(arg1:string):Promise>; + +export function TopRecordingsForArtist(arg1:string):Promise>; + +export function SimilarArtists(arg1:string):Promise>; + +export function CoverArtURL(arg1:string):Promise; + +export function CoverArtGroupURL(arg1:string):Promise; diff --git a/frontend/wailsjs/go/explore/Service.js b/frontend/wailsjs/go/explore/Service.js new file mode 100644 index 0000000..7cb06de --- /dev/null +++ b/frontend/wailsjs/go/explore/Service.js @@ -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); +}