From b9214fcc82ab9f50daa66df000b600f673c9d715 Mon Sep 17 00:00:00 2001 From: sproutsberry <238929279+sproutsberry@users.noreply.github.com> Date: Sun, 8 Feb 2026 18:28:54 -0500 Subject: [PATCH 01/10] Initial prototype --- coverart.go | 176 ++++++++++++++++++++++++++++++++++++++++++++------ manifest.json | 12 +++- 2 files changed, 168 insertions(+), 20 deletions(-) diff --git a/coverart.go b/coverart.go index 46592a0..a356301 100644 --- a/coverart.go +++ b/coverart.go @@ -9,8 +9,11 @@ import ( "github.com/navidrome/navidrome/plugins/pdk/go/pdk" ) -// Configuration key for uguu.se image hosting -const uguuEnabledKey = "uguuenabled" +const cacheKeyFormat = "artwork.%s" + +// ============================================================================ +// uguu.se +// ============================================================================ // uguu.se API response type uguuResponse struct { @@ -20,15 +23,6 @@ type uguuResponse struct { } `json:"files"` } -// getImageURL retrieves the track artwork URL, optionally uploading to uguu.se. -func getImageURL(username, trackID string) string { - uguuEnabled, _ := pdk.GetConfig(uguuEnabledKey) - if uguuEnabled == "true" { - return getImageViaUguu(username, trackID) - } - return getImageDirect(trackID) -} - // getImageDirect returns the artwork URL directly from Navidrome (current behavior). func getImageDirect(trackID string) string { artworkURL, err := host.ArtworkGetTrackUrl(trackID, 300) @@ -46,14 +40,6 @@ func getImageDirect(trackID string) string { // getImageViaUguu fetches artwork and uploads it to uguu.se. func getImageViaUguu(username, trackID string) string { - // Check cache first - cacheKey := fmt.Sprintf("uguu.artwork.%s", trackID) - cachedURL, exists, err := host.CacheGetString(cacheKey) - if err == nil && exists { - pdk.Log(pdk.LogDebug, fmt.Sprintf("Cache hit for uguu artwork: %s", trackID)) - return cachedURL - } - // Fetch artwork data from Navidrome contentType, data, err := host.SubsonicAPICallRaw(fmt.Sprintf("/getCoverArt?u=%s&id=%s&size=300", username, trackID)) if err != nil { @@ -68,7 +54,9 @@ func getImageViaUguu(username, trackID string) string { return "" } + cacheKey := fmt.Sprintf(cacheKeyFormat, trackID) _ = host.CacheSetString(cacheKey, url, 9000) + return url } @@ -108,3 +96,153 @@ func uploadToUguu(imageData []byte, contentType string) (string, error) { return result.Files[0].URL, nil } + +// ============================================================================ +// Cover Art Archive +// ============================================================================ + +type subsonicGetSongResponse struct { + Data struct { + Song struct { + AlbumID string `json:"albumId"` + } `json:"song"` + } `json:"subsonic-response"` +} + +type subsonicGetAlbumResponse struct { + Data struct { + Song struct { + MusicBrainzId string `json:"musicBrainzId"` + } `json:"album"` + } `json:"subsonic-response"` +} + +// https://musicbrainz.org/doc/Cover_Art_Archive/API +type caaResponse struct { + Images []struct { + Front bool `json:"front"` + Back bool `json:"back"` + ImageURL string `json:"image"` + ThumbnailImageURLs struct { + Size250 string `json:"250"` + Size500 string `json:"500"` + Size1200 string `json:"1200"` + Small string `json:"small"` // deprecated; use 250 + Large string `json:"large"` // deprecated; use 500 + } `json:"thumbnails"` + } `json:"images"` + ReleaseURL string `json:"release"` +} + +func getAlbumIDFromTrackID(username, trackID string) (string, error) { + data, err := host.SubsonicAPICall(fmt.Sprintf("getSong?u=%s&id=%s", username, trackID)) + if err != nil { + return "", fmt.Errorf("getSong failed: %w", err) + } + + var response subsonicGetSongResponse + if err := json.Unmarshal([]byte(data), &response); err != nil { + return "", fmt.Errorf("failed to parse getSong response: %w", err) + } + + return response.Data.Song.AlbumID, nil +} + +func getMusicBrainzIDFromAlbumID(username, albumID string) (string, error) { + data, err := host.SubsonicAPICall(fmt.Sprintf("getAlbum?u=%s&id=%s", username, albumID)) + if err != nil { + return "", fmt.Errorf("getAlbum failed: %w", err) + } + + var response subsonicGetAlbumResponse + if err := json.Unmarshal([]byte(data), &response); err != nil { + return "", fmt.Errorf("failed to parse getAlbum response: %w", err) + } + + return response.Data.Song.MusicBrainzId, nil +} + +func fetchImageFromCAA(username, trackID string) (string, error) { + albumID, err := getAlbumIDFromTrackID(username, trackID) + if err != nil { + return "", fmt.Errorf("failed to get album ID from track ID %s: %w", trackID, err) + } + + musicBrainzID, err := getMusicBrainzIDFromAlbumID(username, albumID) + if err != nil { + return "", fmt.Errorf("failed to get MusicBrainz ID from album ID %s: %w", trackID, err) + } + if musicBrainzID == "" { // TODO: Check for nil as well + pdk.Log(pdk.LogDebug, fmt.Sprintf("No MusicBrainz ID for album %s", albumID)) + return "", nil + } + + req := pdk.NewHTTPRequest(pdk.MethodGet, fmt.Sprintf("https://coverartarchive.org/release/%s", musicBrainzID)) + resp := req.Send() + + status := resp.Status() + if status == 404 { + pdk.Log(pdk.LogDebug, fmt.Sprintf("No cover art for MusicBrainz ID %s", musicBrainzID)) + return "", nil + } + if status >= 400 { + return "", fmt.Errorf("Cover Art Archive request failed: HTTP %d", resp.Status()) + } + + var result caaResponse + if err := json.Unmarshal(resp.Body(), &result); err != nil { + return "", fmt.Errorf("failed to parse Cover Art Archive response: %w", err) + } + + for _, image := range result.Images { + if image.Front { + return image.ThumbnailImageURLs.Size250, nil + } + } + + pdk.Log(pdk.LogDebug, fmt.Sprintf("No viable cover art for MusicBrainz ID %s (%d images)", musicBrainzID, len(result.Images))) + return "", nil +} + +func getImageViaCAA(username, trackID string) string { + url, err := fetchImageFromCAA(username, trackID) + if err != nil { + pdk.Log(pdk.LogWarn, fmt.Sprintf("Failed to get image from Cover Art archive: %v", err)) + return "" + } + + cacheKey := fmt.Sprintf(cacheKeyFormat, trackID) + _ = host.CacheSetString(cacheKey, url, 86400) + + return url +} + +// ============================================================================ +// TODO: name this section +// ============================================================================ + +const uguuEnabledKey = "uguuenabled" +const caaEnabledKey = "caaenabled" + +func getImageURL(username, trackID string) string { + cacheKey := fmt.Sprintf(cacheKeyFormat, trackID) + cachedURL, exists, err := host.CacheGetString(cacheKey) + if err == nil && exists { + pdk.Log(pdk.LogDebug, fmt.Sprintf("Cache hit for artwork: %s", trackID)) + return cachedURL + } + + caaEnabled, _ := pdk.GetConfig(caaEnabledKey) + if caaEnabled == "true" { + if url := getImageViaCAA(username, trackID); url != "" { + return url + } + } + + uguuEnabled, _ := pdk.GetConfig(uguuEnabledKey) + if uguuEnabled == "true" { + return getImageViaUguu(username, trackID) + } + + return getImageDirect(trackID) +} diff --git a/manifest.json b/manifest.json index 42cfdd4..0e8accb 100644 --- a/manifest.json +++ b/manifest.json @@ -13,7 +13,8 @@ "reason": "To communicate with Discord API for gateway discovery and image uploads", "requiredHosts": [ "discord.com", - "uguu.se" + "uguu.se", + "coverartarchive.org" ] }, "websocket": { @@ -64,6 +65,11 @@ "title": "Upload artwork to uguu.se (enable if Navidrome is not publicly accessible)", "default": false }, + "caaenabled": { + "type": "boolean", + "title": "Use artwork from Cover Art Archive when available", + "default": true + }, "users": { "type": "array", "title": "User Tokens", @@ -111,6 +117,10 @@ "format": "radio" } }, + { + "type": "Control", + "scope": "#/properties/caaenabled" + }, { "type": "Control", "scope": "#/properties/uguuenabled" -- 2.52.0 From fba33efe8d295b294ade54cb96c2a5d29294ce84 Mon Sep 17 00:00:00 2001 From: sproutsberry <238929279+sproutsberry@users.noreply.github.com> Date: Sun, 8 Feb 2026 18:49:46 -0500 Subject: [PATCH 02/10] Resolve Gemini suggestions --- coverart.go | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/coverart.go b/coverart.go index a356301..d55a84e 100644 --- a/coverart.go +++ b/coverart.go @@ -111,8 +111,8 @@ type subsonicGetSongResponse struct { type subsonicGetAlbumResponse struct { Data struct { - Song struct { - MusicBrainzId string `json:"musicBrainzId"` + Album struct { + MusicBrainzId string `json:"musicBrainzId,omitempty"` } `json:"album"` } `json:"subsonic-response"` } @@ -159,20 +159,24 @@ func getMusicBrainzIDFromAlbumID(username, albumID string) (string, error) { return "", fmt.Errorf("failed to parse getAlbum response: %w", err) } - return response.Data.Song.MusicBrainzId, nil + return response.Data.Album.MusicBrainzId, nil } func fetchImageFromCAA(username, trackID string) (string, error) { albumID, err := getAlbumIDFromTrackID(username, trackID) if err != nil { - return "", fmt.Errorf("failed to get album ID from track ID %s: %w", trackID, err) + return "", fmt.Errorf("failed to get album ID from track %s: %w", trackID, err) + } + if albumID == "" { + pdk.Log(pdk.LogDebug, fmt.Sprintf("No Album ID for track %s", trackID)) + return "", nil } musicBrainzID, err := getMusicBrainzIDFromAlbumID(username, albumID) if err != nil { - return "", fmt.Errorf("failed to get MusicBrainz ID from album ID %s: %w", trackID, err) + return "", fmt.Errorf("failed to get MusicBrainz ID from album %s: %w", trackID, err) } - if musicBrainzID == "" { // TODO: Check for nil as well + if musicBrainzID == "" { pdk.Log(pdk.LogDebug, fmt.Sprintf("No MusicBrainz ID for album %s", albumID)) return "", nil } @@ -200,7 +204,7 @@ func fetchImageFromCAA(username, trackID string) (string, error) { } } - pdk.Log(pdk.LogDebug, fmt.Sprintf("No viable cover art for MusicBrainz ID %s (%d images)", musicBrainzID, len(result.Images))) + pdk.Log(pdk.LogDebug, fmt.Sprintf("No front cover art for MusicBrainz ID %s (%d images)", musicBrainzID, len(result.Images))) return "", nil } @@ -218,7 +222,7 @@ func getImageViaCAA(username, trackID string) string { } // ============================================================================ -// TODO: name this section +// Image URL Resolution // ============================================================================ const uguuEnabledKey = "uguuenabled" -- 2.52.0 From 9cdaf35a71d3e120587d4b698d3a9fb3c5ad9d0f Mon Sep 17 00:00:00 2001 From: sproutsberry <238929279+sproutsberry@users.noreply.github.com> Date: Sun, 8 Feb 2026 19:05:12 -0500 Subject: [PATCH 03/10] Cleanup pass --- coverart.go | 70 +++++++++++++++++++++++++---------------------------- 1 file changed, 33 insertions(+), 37 deletions(-) diff --git a/coverart.go b/coverart.go index d55a84e..0db0046 100644 --- a/coverart.go +++ b/coverart.go @@ -109,6 +109,20 @@ type subsonicGetSongResponse struct { } `json:"subsonic-response"` } +func getAlbumIDFromTrackID(username, trackID string) (string, error) { + data, err := host.SubsonicAPICall(fmt.Sprintf("getSong?u=%s&id=%s", username, trackID)) + if err != nil { + return "", fmt.Errorf("getSong failed: %w", err) + } + + var response subsonicGetSongResponse + if err := json.Unmarshal([]byte(data), &response); err != nil { + return "", fmt.Errorf("failed to parse getSong response: %w", err) + } + + return response.Data.Song.AlbumID, nil +} + type subsonicGetAlbumResponse struct { Data struct { Album struct { @@ -117,6 +131,20 @@ type subsonicGetAlbumResponse struct { } `json:"subsonic-response"` } +func getMusicBrainzIDFromAlbumID(username, albumID string) (string, error) { + data, err := host.SubsonicAPICall(fmt.Sprintf("getAlbum?u=%s&id=%s", username, albumID)) + if err != nil { + return "", fmt.Errorf("getAlbum failed: %w", err) + } + + var response subsonicGetAlbumResponse + if err := json.Unmarshal([]byte(data), &response); err != nil { + return "", fmt.Errorf("failed to parse getAlbum response: %w", err) + } + + return response.Data.Album.MusicBrainzId, nil +} + // https://musicbrainz.org/doc/Cover_Art_Archive/API type caaResponse struct { Images []struct { @@ -134,49 +162,19 @@ type caaResponse struct { ReleaseURL string `json:"release"` } -func getAlbumIDFromTrackID(username, trackID string) (string, error) { - data, err := host.SubsonicAPICall(fmt.Sprintf("getSong?u=%s&id=%s", username, trackID)) - if err != nil { - return "", fmt.Errorf("getSong failed: %w", err) - } - - var response subsonicGetSongResponse - if err := json.Unmarshal([]byte(data), &response); err != nil { - return "", fmt.Errorf("failed to parse getSong response: %w", err) - } - - return response.Data.Song.AlbumID, nil -} - -func getMusicBrainzIDFromAlbumID(username, albumID string) (string, error) { - data, err := host.SubsonicAPICall(fmt.Sprintf("getAlbum?u=%s&id=%s", username, albumID)) - if err != nil { - return "", fmt.Errorf("getAlbum failed: %w", err) - } - - var response subsonicGetAlbumResponse - if err := json.Unmarshal([]byte(data), &response); err != nil { - return "", fmt.Errorf("failed to parse getAlbum response: %w", err) - } - - return response.Data.Album.MusicBrainzId, nil -} - func fetchImageFromCAA(username, trackID string) (string, error) { albumID, err := getAlbumIDFromTrackID(username, trackID) if err != nil { return "", fmt.Errorf("failed to get album ID from track %s: %w", trackID, err) - } - if albumID == "" { - pdk.Log(pdk.LogDebug, fmt.Sprintf("No Album ID for track %s", trackID)) + } else if albumID == "" { + pdk.Log(pdk.LogDebug, fmt.Sprintf("No album for track %s", trackID)) return "", nil } musicBrainzID, err := getMusicBrainzIDFromAlbumID(username, albumID) if err != nil { return "", fmt.Errorf("failed to get MusicBrainz ID from album %s: %w", trackID, err) - } - if musicBrainzID == "" { + } else if musicBrainzID == "" { pdk.Log(pdk.LogDebug, fmt.Sprintf("No MusicBrainz ID for album %s", albumID)) return "", nil } @@ -184,12 +182,10 @@ func fetchImageFromCAA(username, trackID string) (string, error) { req := pdk.NewHTTPRequest(pdk.MethodGet, fmt.Sprintf("https://coverartarchive.org/release/%s", musicBrainzID)) resp := req.Send() - status := resp.Status() - if status == 404 { + if status := resp.Status(); status == 404 { pdk.Log(pdk.LogDebug, fmt.Sprintf("No cover art for MusicBrainz ID %s", musicBrainzID)) return "", nil - } - if status >= 400 { + } else if status >= 400 { return "", fmt.Errorf("Cover Art Archive request failed: HTTP %d", resp.Status()) } -- 2.52.0 From 6824cdf693aaf2b1d51928872221e6e16fe6f1ab Mon Sep 17 00:00:00 2001 From: sproutsberry <238929279+sproutsberry@users.noreply.github.com> Date: Sun, 8 Feb 2026 19:23:46 -0500 Subject: [PATCH 04/10] Cache cover art methods individually again --- coverart.go | 80 ++++++++++++++++++++++++++++------------------------- 1 file changed, 42 insertions(+), 38 deletions(-) diff --git a/coverart.go b/coverart.go index 0db0046..3454bca 100644 --- a/coverart.go +++ b/coverart.go @@ -9,8 +9,6 @@ import ( "github.com/navidrome/navidrome/plugins/pdk/go/pdk" ) -const cacheKeyFormat = "artwork.%s" - // ============================================================================ // uguu.se // ============================================================================ @@ -40,6 +38,14 @@ func getImageDirect(trackID string) string { // getImageViaUguu fetches artwork and uploads it to uguu.se. func getImageViaUguu(username, trackID string) string { + // Check cache first + cacheKey := fmt.Sprintf("uguu.artwork.%s", trackID) + cachedURL, exists, err := host.CacheGetString(cacheKey) + if err == nil && exists { + pdk.Log(pdk.LogDebug, fmt.Sprintf("Cache hit for uguu.se artwork: %s", trackID)) + return cachedURL + } + // Fetch artwork data from Navidrome contentType, data, err := host.SubsonicAPICallRaw(fmt.Sprintf("/getCoverArt?u=%s&id=%s&size=300", username, trackID)) if err != nil { @@ -54,9 +60,7 @@ func getImageViaUguu(username, trackID string) string { return "" } - cacheKey := fmt.Sprintf(cacheKeyFormat, trackID) _ = host.CacheSetString(cacheKey, url, 9000) - return url } @@ -112,12 +116,12 @@ type subsonicGetSongResponse struct { func getAlbumIDFromTrackID(username, trackID string) (string, error) { data, err := host.SubsonicAPICall(fmt.Sprintf("getSong?u=%s&id=%s", username, trackID)) if err != nil { - return "", fmt.Errorf("getSong failed: %w", err) + return "", err } var response subsonicGetSongResponse if err := json.Unmarshal([]byte(data), &response); err != nil { - return "", fmt.Errorf("failed to parse getSong response: %w", err) + return "", fmt.Errorf("failed to parse response: %w", err) } return response.Data.Song.AlbumID, nil @@ -134,12 +138,12 @@ type subsonicGetAlbumResponse struct { func getMusicBrainzIDFromAlbumID(username, albumID string) (string, error) { data, err := host.SubsonicAPICall(fmt.Sprintf("getAlbum?u=%s&id=%s", username, albumID)) if err != nil { - return "", fmt.Errorf("getAlbum failed: %w", err) + return "", err } var response subsonicGetAlbumResponse if err := json.Unmarshal([]byte(data), &response); err != nil { - return "", fmt.Errorf("failed to parse getAlbum response: %w", err) + return "", fmt.Errorf("failed to parse response: %w", err) } return response.Data.Album.MusicBrainzId, nil @@ -162,23 +166,7 @@ type caaResponse struct { ReleaseURL string `json:"release"` } -func fetchImageFromCAA(username, trackID string) (string, error) { - albumID, err := getAlbumIDFromTrackID(username, trackID) - if err != nil { - return "", fmt.Errorf("failed to get album ID from track %s: %w", trackID, err) - } else if albumID == "" { - pdk.Log(pdk.LogDebug, fmt.Sprintf("No album for track %s", trackID)) - return "", nil - } - - musicBrainzID, err := getMusicBrainzIDFromAlbumID(username, albumID) - if err != nil { - return "", fmt.Errorf("failed to get MusicBrainz ID from album %s: %w", trackID, err) - } else if musicBrainzID == "" { - pdk.Log(pdk.LogDebug, fmt.Sprintf("No MusicBrainz ID for album %s", albumID)) - return "", nil - } - +func getImageURLFromMusicBrainzID(musicBrainzID string) (string, error) { req := pdk.NewHTTPRequest(pdk.MethodGet, fmt.Sprintf("https://coverartarchive.org/release/%s", musicBrainzID)) resp := req.Send() @@ -186,12 +174,12 @@ func fetchImageFromCAA(username, trackID string) (string, error) { pdk.Log(pdk.LogDebug, fmt.Sprintf("No cover art for MusicBrainz ID %s", musicBrainzID)) return "", nil } else if status >= 400 { - return "", fmt.Errorf("Cover Art Archive request failed: HTTP %d", resp.Status()) + return "", fmt.Errorf("HTTP %d", resp.Status()) } var result caaResponse if err := json.Unmarshal(resp.Body(), &result); err != nil { - return "", fmt.Errorf("failed to parse Cover Art Archive response: %w", err) + return "", fmt.Errorf("failed to parse: %w", err) } for _, image := range result.Images { @@ -205,14 +193,37 @@ func fetchImageFromCAA(username, trackID string) (string, error) { } func getImageViaCAA(username, trackID string) string { - url, err := fetchImageFromCAA(username, trackID) + albumID, err := getAlbumIDFromTrackID(username, trackID) if err != nil { - pdk.Log(pdk.LogWarn, fmt.Sprintf("Failed to get image from Cover Art archive: %v", err)) + pdk.Log(pdk.LogWarn, fmt.Sprintf("Failed to get album ID from track %s: %s", trackID, err)) + return "" + } else if albumID == "" { + pdk.Log(pdk.LogDebug, fmt.Sprintf("No album for track %s", trackID)) return "" } - cacheKey := fmt.Sprintf(cacheKeyFormat, trackID) - _ = host.CacheSetString(cacheKey, url, 86400) + musicBrainzID, err := getMusicBrainzIDFromAlbumID(username, albumID) + if err != nil { + pdk.Log(pdk.LogWarn, fmt.Sprintf("Failed to get MusicBrainz ID from album %s: %s", trackID, err)) + return "" + } else if musicBrainzID == "" { + pdk.Log(pdk.LogDebug, fmt.Sprintf("No MusicBrainz ID for album %s", albumID)) + return "" + } + + // Check cache first + cacheKey := fmt.Sprintf("caa.artwork.%s", musicBrainzID) + cachedURL, exists, err := host.CacheGetString(cacheKey) + if err == nil && exists { + pdk.Log(pdk.LogDebug, fmt.Sprintf("Cache hit for Cover Art Archive artwork: %s", musicBrainzID)) + return cachedURL + } + + url, err := getImageURLFromMusicBrainzID(musicBrainzID) + if err != nil { + pdk.Log(pdk.LogWarn, fmt.Sprintf("Cover Art Archive request failed for %s: %s", musicBrainzID, err)) + return "" + } return url } @@ -225,13 +236,6 @@ const uguuEnabledKey = "uguuenabled" const caaEnabledKey = "caaenabled" func getImageURL(username, trackID string) string { - cacheKey := fmt.Sprintf(cacheKeyFormat, trackID) - cachedURL, exists, err := host.CacheGetString(cacheKey) - if err == nil && exists { - pdk.Log(pdk.LogDebug, fmt.Sprintf("Cache hit for artwork: %s", trackID)) - return cachedURL - } - caaEnabled, _ := pdk.GetConfig(caaEnabledKey) if caaEnabled == "true" { if url := getImageViaCAA(username, trackID); url != "" { -- 2.52.0 From 9c5127c471219cd68fd453529685749eb3c506f5 Mon Sep 17 00:00:00 2001 From: sproutsberry <238929279+sproutsberry@users.noreply.github.com> Date: Sun, 8 Feb 2026 19:30:14 -0500 Subject: [PATCH 05/10] Forward scrobbler.TrackInfo into getImageURL instead of fetching manually (whoops...) --- coverart.go | 75 +++++------------------------------------------------ 1 file changed, 7 insertions(+), 68 deletions(-) diff --git a/coverart.go b/coverart.go index 3454bca..8f9ffa8 100644 --- a/coverart.go +++ b/coverart.go @@ -7,6 +7,7 @@ import ( "github.com/navidrome/navidrome/plugins/pdk/go/host" "github.com/navidrome/navidrome/plugins/pdk/go/pdk" + "github.com/navidrome/navidrome/plugins/pdk/go/scrobbler" ) // ============================================================================ @@ -105,50 +106,6 @@ func uploadToUguu(imageData []byte, contentType string) (string, error) { // Cover Art Archive // ============================================================================ -type subsonicGetSongResponse struct { - Data struct { - Song struct { - AlbumID string `json:"albumId"` - } `json:"song"` - } `json:"subsonic-response"` -} - -func getAlbumIDFromTrackID(username, trackID string) (string, error) { - data, err := host.SubsonicAPICall(fmt.Sprintf("getSong?u=%s&id=%s", username, trackID)) - if err != nil { - return "", err - } - - var response subsonicGetSongResponse - if err := json.Unmarshal([]byte(data), &response); err != nil { - return "", fmt.Errorf("failed to parse response: %w", err) - } - - return response.Data.Song.AlbumID, nil -} - -type subsonicGetAlbumResponse struct { - Data struct { - Album struct { - MusicBrainzId string `json:"musicBrainzId,omitempty"` - } `json:"album"` - } `json:"subsonic-response"` -} - -func getMusicBrainzIDFromAlbumID(username, albumID string) (string, error) { - data, err := host.SubsonicAPICall(fmt.Sprintf("getAlbum?u=%s&id=%s", username, albumID)) - if err != nil { - return "", err - } - - var response subsonicGetAlbumResponse - if err := json.Unmarshal([]byte(data), &response); err != nil { - return "", fmt.Errorf("failed to parse response: %w", err) - } - - return response.Data.Album.MusicBrainzId, nil -} - // https://musicbrainz.org/doc/Cover_Art_Archive/API type caaResponse struct { Images []struct { @@ -192,25 +149,7 @@ func getImageURLFromMusicBrainzID(musicBrainzID string) (string, error) { return "", nil } -func getImageViaCAA(username, trackID string) string { - albumID, err := getAlbumIDFromTrackID(username, trackID) - if err != nil { - pdk.Log(pdk.LogWarn, fmt.Sprintf("Failed to get album ID from track %s: %s", trackID, err)) - return "" - } else if albumID == "" { - pdk.Log(pdk.LogDebug, fmt.Sprintf("No album for track %s", trackID)) - return "" - } - - musicBrainzID, err := getMusicBrainzIDFromAlbumID(username, albumID) - if err != nil { - pdk.Log(pdk.LogWarn, fmt.Sprintf("Failed to get MusicBrainz ID from album %s: %s", trackID, err)) - return "" - } else if musicBrainzID == "" { - pdk.Log(pdk.LogDebug, fmt.Sprintf("No MusicBrainz ID for album %s", albumID)) - return "" - } - +func getImageViaCAA(username, musicBrainzID string) string { // Check cache first cacheKey := fmt.Sprintf("caa.artwork.%s", musicBrainzID) cachedURL, exists, err := host.CacheGetString(cacheKey) @@ -235,18 +174,18 @@ func getImageViaCAA(username, trackID string) string { const uguuEnabledKey = "uguuenabled" const caaEnabledKey = "caaenabled" -func getImageURL(username, trackID string) string { +func getImageURL(username string, track scrobbler.TrackInfo) string { caaEnabled, _ := pdk.GetConfig(caaEnabledKey) - if caaEnabled == "true" { - if url := getImageViaCAA(username, trackID); url != "" { + if caaEnabled == "true" && track.MBZAlbumID != "" { + if url := getImageViaCAA(username, track.MBZAlbumID); url != "" { return url } } uguuEnabled, _ := pdk.GetConfig(uguuEnabledKey) if uguuEnabled == "true" { - return getImageViaUguu(username, trackID) + return getImageViaUguu(username, track.ID) } - return getImageDirect(trackID) + return getImageDirect(track.ID) } -- 2.52.0 From fe11087f7d11b03980cd68e98dca6f67957929f4 Mon Sep 17 00:00:00 2001 From: sproutsberry <238929279+sproutsberry@users.noreply.github.com> Date: Sun, 8 Feb 2026 19:42:33 -0500 Subject: [PATCH 06/10] More cleanup --- coverart.go | 45 +++++++++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/coverart.go b/coverart.go index 8f9ffa8..c6b62b3 100644 --- a/coverart.go +++ b/coverart.go @@ -11,17 +11,9 @@ import ( ) // ============================================================================ -// uguu.se +// Direct // ============================================================================ -// uguu.se API response -type uguuResponse struct { - Success bool `json:"success"` - Files []struct { - URL string `json:"url"` - } `json:"files"` -} - // getImageDirect returns the artwork URL directly from Navidrome (current behavior). func getImageDirect(trackID string) string { artworkURL, err := host.ArtworkGetTrackUrl(trackID, 300) @@ -37,6 +29,18 @@ func getImageDirect(trackID string) string { return artworkURL } +// ============================================================================ +// uguu.se +// ============================================================================ + +// uguu.se API response +type uguuResponse struct { + Success bool `json:"success"` + Files []struct { + URL string `json:"url"` + } `json:"files"` +} + // getImageViaUguu fetches artwork and uploads it to uguu.se. func getImageViaUguu(username, trackID string) string { // Check cache first @@ -106,6 +110,7 @@ func uploadToUguu(imageData []byte, contentType string) (string, error) { // Cover Art Archive // ============================================================================ +// caaResponse only includes relevant parameters; see API for full response // https://musicbrainz.org/doc/Cover_Art_Archive/API type caaResponse struct { Images []struct { @@ -116,19 +121,16 @@ type caaResponse struct { Size250 string `json:"250"` Size500 string `json:"500"` Size1200 string `json:"1200"` - Small string `json:"small"` // deprecated; use 250 - Large string `json:"large"` // deprecated; use 500 } `json:"thumbnails"` } `json:"images"` - ReleaseURL string `json:"release"` } -func getImageURLFromMusicBrainzID(musicBrainzID string) (string, error) { - req := pdk.NewHTTPRequest(pdk.MethodGet, fmt.Sprintf("https://coverartarchive.org/release/%s", musicBrainzID)) +func getThumbnailForMBZAlbumID(mbzAlbumID string) (string, error) { + req := pdk.NewHTTPRequest(pdk.MethodGet, fmt.Sprintf("https://coverartarchive.org/release/%s", mbzAlbumID)) resp := req.Send() if status := resp.Status(); status == 404 { - pdk.Log(pdk.LogDebug, fmt.Sprintf("No cover art for MusicBrainz ID %s", musicBrainzID)) + pdk.Log(pdk.LogDebug, fmt.Sprintf("No cover art for MusicBrainz Album ID: %s", mbzAlbumID)) return "", nil } else if status >= 400 { return "", fmt.Errorf("HTTP %d", resp.Status()) @@ -145,22 +147,21 @@ func getImageURLFromMusicBrainzID(musicBrainzID string) (string, error) { } } - pdk.Log(pdk.LogDebug, fmt.Sprintf("No front cover art for MusicBrainz ID %s (%d images)", musicBrainzID, len(result.Images))) + pdk.Log(pdk.LogDebug, fmt.Sprintf("No front cover art for MusicBrainz Album ID: %s (%d images)", mbzAlbumID, len(result.Images))) return "", nil } -func getImageViaCAA(username, musicBrainzID string) string { - // Check cache first - cacheKey := fmt.Sprintf("caa.artwork.%s", musicBrainzID) +func getImageViaCAA(username, mbzAlbumID string) string { + cacheKey := fmt.Sprintf("caa.artwork.%s", mbzAlbumID) cachedURL, exists, err := host.CacheGetString(cacheKey) if err == nil && exists { - pdk.Log(pdk.LogDebug, fmt.Sprintf("Cache hit for Cover Art Archive artwork: %s", musicBrainzID)) + pdk.Log(pdk.LogDebug, fmt.Sprintf("Cache hit for Cover Art Archive artwork: %s", mbzAlbumID)) return cachedURL } - url, err := getImageURLFromMusicBrainzID(musicBrainzID) + url, err := getThumbnailForMBZAlbumID(mbzAlbumID) if err != nil { - pdk.Log(pdk.LogWarn, fmt.Sprintf("Cover Art Archive request failed for %s: %s", musicBrainzID, err)) + pdk.Log(pdk.LogWarn, fmt.Sprintf("Cover Art Archive request failed for %s: %v", mbzAlbumID, err)) return "" } -- 2.52.0 From 69e5dd63486bde35dbdae9fbea36d6139eb804f2 Mon Sep 17 00:00:00 2001 From: sproutsberry <238929279+sproutsberry@users.noreply.github.com> Date: Sun, 8 Feb 2026 23:02:32 -0500 Subject: [PATCH 07/10] Re-add forgotten cache for getImageViaCAA --- coverart.go | 1 + 1 file changed, 1 insertion(+) diff --git a/coverart.go b/coverart.go index c6b62b3..2993167 100644 --- a/coverart.go +++ b/coverart.go @@ -165,6 +165,7 @@ func getImageViaCAA(username, mbzAlbumID string) string { return "" } + _ = host.CacheSetString(cacheKey, url, 86400) return url } -- 2.52.0 From 10e42be952ca4291d1c9a4a2ca62a0624ea651e1 Mon Sep 17 00:00:00 2001 From: sproutsberry <238929279+sproutsberry@users.noreply.github.com> Date: Mon, 9 Feb 2026 13:22:44 -0500 Subject: [PATCH 08/10] Update tests & write README --- README.md | 13 +++++++++ coverart.go | 4 +-- coverart_test.go | 76 +++++++++++++++++++++++++++++++++++++++++++----- main.go | 2 +- main_test.go | 2 ++ 5 files changed, 86 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index b190fe5..6a8e129 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,15 @@ For album artwork to display in Discord, Discord needs to be able to access the **How it works**: Album art is automatically uploaded to uguu.se (temporary, anonymous hosting service) so Discord can access it. Files are deleted after 3 hours. +### Backup: Cover Art Archive +**Use this if**: You have your music tagged with MusicBrainz + +**Setup**: +1. In plugin settings: **Enable** "Use artwork from Cover Art Archive" +2. No other configuration needed + +**How it works**: Cover art is linked directly from MusicBrainz via Release ID using the [Cover Art Archive API](https://musicbrainz.org/doc/Cover_Art_Archive/API). Will fall back to other methods if no artwork is found. + ### Troubleshooting Album Art - **No album art showing**: Check Navidrome logs for errors - **Using public instance**: Verify ND_BASEURL is correct and Navidrome was restarted @@ -115,6 +124,10 @@ Access the plugin configuration in Navidrome: **Settings > Plugins > Discord Ric - **Album**: Shows the currently playing track's album name - **Artist**: Shows the currently playing track's artist name +#### Use artwork from Cover Art Archive +- **When to enable**: Your Navidrome instance is NOT publicly accessible from the internet, or you don't feel comfortable with directly linking +- **What it does**: Attempts to find and link album artwork with MusicBrainz before using other methods + #### Upload to uguu.se - **When to enable**: Your Navidrome instance is NOT publicly accessible from the internet - **What it does**: Automatically uploads album artwork to uguu.se (temporary hosting) so Discord can display it diff --git a/coverart.go b/coverart.go index 2993167..af62f62 100644 --- a/coverart.go +++ b/coverart.go @@ -151,7 +151,7 @@ func getThumbnailForMBZAlbumID(mbzAlbumID string) (string, error) { return "", nil } -func getImageViaCAA(username, mbzAlbumID string) string { +func getImageViaCAA(mbzAlbumID string) string { cacheKey := fmt.Sprintf("caa.artwork.%s", mbzAlbumID) cachedURL, exists, err := host.CacheGetString(cacheKey) if err == nil && exists { @@ -179,7 +179,7 @@ const caaEnabledKey = "caaenabled" func getImageURL(username string, track scrobbler.TrackInfo) string { caaEnabled, _ := pdk.GetConfig(caaEnabledKey) if caaEnabled == "true" && track.MBZAlbumID != "" { - if url := getImageViaCAA(username, track.MBZAlbumID); url != "" { + if url := getImageViaCAA(track.MBZAlbumID); url != "" { return url } } diff --git a/coverart_test.go b/coverart_test.go index e3131e8..d962556 100644 --- a/coverart_test.go +++ b/coverart_test.go @@ -5,6 +5,7 @@ import ( "github.com/navidrome/navidrome/plugins/pdk/go/host" "github.com/navidrome/navidrome/plugins/pdk/go/pdk" + "github.com/navidrome/navidrome/plugins/pdk/go/scrobbler" "github.com/stretchr/testify/mock" . "github.com/onsi/ginkgo/v2" @@ -23,29 +24,30 @@ var _ = Describe("getImageURL", func() { pdk.PDKMock.On("Log", mock.Anything, mock.Anything).Maybe() }) - Describe("uguu disabled (default)", func() { + Describe("direct", func() { BeforeEach(func() { pdk.PDKMock.On("GetConfig", uguuEnabledKey).Return("", false) + pdk.PDKMock.On("GetConfig", caaEnabledKey).Return("", false) }) It("returns artwork URL directly", func() { host.ArtworkMock.On("GetTrackUrl", "track1", int32(300)).Return("https://example.com/art.jpg", nil) - url := getImageURL("testuser", "track1") + url := getImageURL("testuser", scrobbler.TrackInfo{ID: "track1"}) Expect(url).To(Equal("https://example.com/art.jpg")) }) It("returns empty for localhost URL", func() { host.ArtworkMock.On("GetTrackUrl", "track1", int32(300)).Return("http://localhost:4533/art.jpg", nil) - url := getImageURL("testuser", "track1") + url := getImageURL("testuser", scrobbler.TrackInfo{ID: "track1"}) Expect(url).To(BeEmpty()) }) It("returns empty when artwork fetch fails", func() { host.ArtworkMock.On("GetTrackUrl", "track1", int32(300)).Return("", errors.New("not found")) - url := getImageURL("testuser", "track1") + url := getImageURL("testuser", scrobbler.TrackInfo{ID: "track1"}) Expect(url).To(BeEmpty()) }) }) @@ -53,12 +55,13 @@ var _ = Describe("getImageURL", func() { Describe("uguu enabled", func() { BeforeEach(func() { pdk.PDKMock.On("GetConfig", uguuEnabledKey).Return("true", true) + pdk.PDKMock.On("GetConfig", caaEnabledKey).Return("", false) }) It("returns cached URL when available", func() { host.CacheMock.On("GetString", "uguu.artwork.track1").Return("https://a.uguu.se/cached.jpg", true, nil) - url := getImageURL("testuser", "track1") + url := getImageURL("testuser", scrobbler.TrackInfo{ID: "track1"}) Expect(url).To(Equal("https://a.uguu.se/cached.jpg")) }) @@ -79,7 +82,7 @@ var _ = Describe("getImageURL", func() { // Mock cache set host.CacheMock.On("SetString", "uguu.artwork.track1", "https://a.uguu.se/uploaded.jpg", int64(9000)).Return(nil) - url := getImageURL("testuser", "track1") + url := getImageURL("testuser", scrobbler.TrackInfo{ID: "track1"}) Expect(url).To(Equal("https://a.uguu.se/uploaded.jpg")) host.CacheMock.AssertCalled(GinkgoT(), "SetString", "uguu.artwork.track1", "https://a.uguu.se/uploaded.jpg", int64(9000)) }) @@ -89,7 +92,7 @@ var _ = Describe("getImageURL", func() { host.SubsonicAPIMock.On("CallRaw", "/getCoverArt?u=testuser&id=track1&size=300"). Return("", []byte(nil), errors.New("fetch failed")) - url := getImageURL("testuser", "track1") + url := getImageURL("testuser", scrobbler.TrackInfo{ID: "track1"}) Expect(url).To(BeEmpty()) }) @@ -102,8 +105,65 @@ var _ = Describe("getImageURL", func() { pdk.PDKMock.On("NewHTTPRequest", pdk.MethodPost, "https://uguu.se/upload").Return(uguuReq) pdk.PDKMock.On("Send", uguuReq).Return(pdk.NewStubHTTPResponse(500, nil, []byte(`{"success":false}`))) - url := getImageURL("testuser", "track1") + url := getImageURL("testuser", scrobbler.TrackInfo{ID: "track1"}) Expect(url).To(BeEmpty()) }) }) + + Describe("caa enabled", func() { + BeforeEach(func() { + pdk.PDKMock.On("GetConfig", uguuEnabledKey).Return("", false) + pdk.PDKMock.On("GetConfig", caaEnabledKey).Return("true", true) + }) + + It("returns cached URL when available", func() { + host.CacheMock.On("GetString", "caa.artwork.test").Return("https://coverartarchive.org/release/test/0-250.jpg", true, nil) + + url := getImageURL("testuser", scrobbler.TrackInfo{MBZAlbumID: "test"}) + Expect(url).To(Equal("https://coverartarchive.org/release/test/0-250.jpg")) + }) + + It("fetches 250px thumbnail and caches the result", func() { + host.CacheMock.On("GetString", "caa.artwork.test").Return("", false, nil) + + // Mock coverartarchive.org HTTP get + caaReq := &pdk.HTTPRequest{} + pdk.PDKMock.On("NewHTTPRequest", pdk.MethodGet, "https://coverartarchive.org/release/test").Return(caaReq) + pdk.PDKMock.On("Send", caaReq).Return(pdk.NewStubHTTPResponse(200, nil, + []byte(`{"images":[{"front":true,"thumbnails":{"250":"https://coverartarchive.org/release/test/0-250.jpg"}}]}`))) + + // Mock cache set + host.CacheMock.On("SetString", "caa.artwork.test", "https://coverartarchive.org/release/test/0-250.jpg", int64(86400)).Return(nil) + + url := getImageURL("testuser", scrobbler.TrackInfo{MBZAlbumID: "test"}) + Expect(url).To(Equal("https://coverartarchive.org/release/test/0-250.jpg")) + host.CacheMock.AssertCalled(GinkgoT(), "SetString", "caa.artwork.test", "https://coverartarchive.org/release/test/0-250.jpg", int64(86400)) + }) + + It("returns artwork directly when no MBZAlbumID is provided", func() { + host.CacheMock.On("GetString", "caa.artwork.test").Return("", false, nil) + host.ArtworkMock.On("GetTrackUrl", "track1", int32(300)).Return("https://example.com/art.jpg", nil) + + url := getImageURL("testuser", scrobbler.TrackInfo{ID: "track1"}) + Expect(url).To(Equal("https://example.com/art.jpg")) + }) + + It("returns artwork directly when no suitable artwork is found", func() { + host.CacheMock.On("GetString", "caa.artwork.test").Return("", false, nil) + host.ArtworkMock.On("GetTrackUrl", "track1", int32(300)).Return("https://example.com/art.jpg", nil) + + // Mock coverartarchive.org HTTP get + caaReq := &pdk.HTTPRequest{} + pdk.PDKMock.On("NewHTTPRequest", pdk.MethodGet, "https://coverartarchive.org/release/test").Return(caaReq) + pdk.PDKMock.On("Send", caaReq).Return(pdk.NewStubHTTPResponse(200, nil, + []byte(`{"images":[{"front":false,"thumbnails":{"250":"https://coverartarchive.org/release/test/0-250.jpg"}}]}`))) + + // Mock cache set + host.CacheMock.On("SetString", "caa.artwork.test", "", int64(86400)).Return(nil) + + url := getImageURL("testuser", scrobbler.TrackInfo{ID: "track1", MBZAlbumID: "test"}) + Expect(url).To(Equal("https://example.com/art.jpg")) + host.CacheMock.AssertCalled(GinkgoT(), "SetString", "caa.artwork.test", "", int64(86400)) + }) + }) }) diff --git a/main.go b/main.go index be5a81e..006225c 100644 --- a/main.go +++ b/main.go @@ -169,7 +169,7 @@ func (p *discordPlugin) NowPlaying(input scrobbler.NowPlayingRequest) error { End: endTime, }, Assets: activityAssets{ - LargeImage: getImageURL(input.Username, input.Track.ID), + LargeImage: getImageURL(input.Username, input.Track), LargeText: input.Track.Album, }, }); err != nil { diff --git a/main_test.go b/main_test.go index cd9aa90..52b0b45 100644 --- a/main_test.go +++ b/main_test.go @@ -120,6 +120,7 @@ var _ = Describe("discordPlugin", func() { pdk.PDKMock.On("GetConfig", clientIDKey).Return("test-client-id", true) pdk.PDKMock.On("GetConfig", usersKey).Return(`[{"username":"testuser","token":"test-token"}]`, true) pdk.PDKMock.On("GetConfig", uguuEnabledKey).Return("", false) + pdk.PDKMock.On("GetConfig", caaEnabledKey).Return("", false) pdk.PDKMock.On("GetConfig", activityNameKey).Return("", false) // Connect mocks (isConnected check via heartbeat) @@ -177,6 +178,7 @@ var _ = Describe("discordPlugin", func() { pdk.PDKMock.On("GetConfig", clientIDKey).Return("test-client-id", true) pdk.PDKMock.On("GetConfig", usersKey).Return(`[{"username":"testuser","token":"test-token"}]`, true) pdk.PDKMock.On("GetConfig", uguuEnabledKey).Return("", false) + pdk.PDKMock.On("GetConfig", caaEnabledKey).Return("", false) pdk.PDKMock.On("GetConfig", activityNameKey).Return(configValue, configExists) // Connect mocks -- 2.52.0 From 371dff635237b4b5bf6ebd69ccacf203f6853614 Mon Sep 17 00:00:00 2001 From: sproutsberry <238929279+sproutsberry@users.noreply.github.com> Date: Mon, 9 Feb 2026 14:55:34 -0500 Subject: [PATCH 09/10] Default caaenabled to false --- manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manifest.json b/manifest.json index 0e8accb..045f7f2 100644 --- a/manifest.json +++ b/manifest.json @@ -68,7 +68,7 @@ "caaenabled": { "type": "boolean", "title": "Use artwork from Cover Art Archive when available", - "default": true + "default": false }, "users": { "type": "array", -- 2.52.0 From 4a6db5a12383afbb0664bf1af254a80c44bacaf1 Mon Sep 17 00:00:00 2001 From: sproutsberry <238929279+sproutsberry@users.noreply.github.com> Date: Thu, 12 Feb 2026 20:03:02 -0500 Subject: [PATCH 10/10] Implement 5 second timeout for Cover Art Archive --- coverart.go | 29 ++++++++++++++++++++--------- coverart_test.go | 15 +++++++++++++++ 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/coverart.go b/coverart.go index af62f62..dbc3f65 100644 --- a/coverart.go +++ b/coverart.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "strings" + "time" "github.com/navidrome/navidrome/plugins/pdk/go/host" "github.com/navidrome/navidrome/plugins/pdk/go/pdk" @@ -110,6 +111,8 @@ func uploadToUguu(imageData []byte, contentType string) (string, error) { // Cover Art Archive // ============================================================================ +const CAA_TIMEOUT = 5 * time.Second + // caaResponse only includes relevant parameters; see API for full response // https://musicbrainz.org/doc/Cover_Art_Archive/API type caaResponse struct { @@ -127,18 +130,26 @@ type caaResponse struct { func getThumbnailForMBZAlbumID(mbzAlbumID string) (string, error) { req := pdk.NewHTTPRequest(pdk.MethodGet, fmt.Sprintf("https://coverartarchive.org/release/%s", mbzAlbumID)) - resp := req.Send() - if status := resp.Status(); status == 404 { - pdk.Log(pdk.LogDebug, fmt.Sprintf("No cover art for MusicBrainz Album ID: %s", mbzAlbumID)) - return "", nil - } else if status >= 400 { - return "", fmt.Errorf("HTTP %d", resp.Status()) - } + respChan := make(chan pdk.HTTPResponse, 1) + go func() { respChan <- req.Send() }() var result caaResponse - if err := json.Unmarshal(resp.Body(), &result); err != nil { - return "", fmt.Errorf("failed to parse: %w", err) + + select { + case resp := <-respChan: + if status := resp.Status(); status == 404 { + pdk.Log(pdk.LogDebug, fmt.Sprintf("No cover art for MusicBrainz Album ID: %s", mbzAlbumID)) + return "", nil + } else if status >= 400 { + return "", fmt.Errorf("HTTP %d", resp.Status()) + } + + if err := json.Unmarshal(resp.Body(), &result); err != nil { + return "", fmt.Errorf("failed to parse: %w", err) + } + case <-time.After(CAA_TIMEOUT): + return "", fmt.Errorf("Timed out") } for _, image := range result.Images { diff --git a/coverart_test.go b/coverart_test.go index d962556..7ffd242 100644 --- a/coverart_test.go +++ b/coverart_test.go @@ -2,6 +2,7 @@ package main import ( "errors" + "time" "github.com/navidrome/navidrome/plugins/pdk/go/host" "github.com/navidrome/navidrome/plugins/pdk/go/pdk" @@ -165,5 +166,19 @@ var _ = Describe("getImageURL", func() { Expect(url).To(Equal("https://example.com/art.jpg")) host.CacheMock.AssertCalled(GinkgoT(), "SetString", "caa.artwork.test", "", int64(86400)) }) + + It("returns artwork directly after 5 second timeout", func() { + host.CacheMock.On("GetString", "caa.artwork.test").Return("", false, nil) + host.ArtworkMock.On("GetTrackUrl", "track1", int32(300)).Return("https://example.com/art.jpg", nil) + + // Mock coverartarchive.org HTTP get + caaReq := &pdk.HTTPRequest{} + pdk.PDKMock.On("NewHTTPRequest", pdk.MethodGet, "https://coverartarchive.org/release/test").Return(caaReq) + pdk.PDKMock.On("Send", caaReq).WaitUntil(time.After(7 * time.Second)).Return(pdk.NewStubHTTPResponse(200, nil, + []byte(`{"images":[{"front":false,"thumbnails":{"250":"https://coverartarchive.org/release/test/0-250.jpg"}}]}`))) + + url := getImageURL("testuser", scrobbler.TrackInfo{ID: "track1", MBZAlbumID: "test"}) + Expect(url).To(Equal("https://example.com/art.jpg")) + }) }) }) -- 2.52.0