Use Cover Art Archive for albums with MusicBrainz IDs #12
@@ -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
|
||||
|
||||
+2
-2
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
+68
-8
@@ -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))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user