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] 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