From 606a7f238920642efbf8b5296036f25c8b99e55d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deluan=20Quint=C3=A3o?= Date: Fri, 20 Mar 2026 20:34:11 -0400 Subject: [PATCH 1/7] feat: use Cover Art Archive for albums with MusicBrainz IDs (#27) --- coverart.go | 107 ++++++++++++++++++-- coverart_test.go | 255 +++++++++++++++++++++++++++++++++++++++++++++-- go.mod | 2 +- go.sum | 4 +- main.go | 4 +- main_test.go | 2 + manifest.json | 13 ++- 7 files changed, 366 insertions(+), 21 deletions(-) diff --git a/coverart.go b/coverart.go index 81b8ceb..2f1bfe2 100644 --- a/coverart.go +++ b/coverart.go @@ -7,10 +7,94 @@ 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" ) -// Configuration key for uguu.se image hosting -const uguuEnabledKey = "uguuenabled" +// Cache TTLs for cover art lookups +const ( + caaCacheTTLHit int64 = 24 * 60 * 60 // 24 hours for resolved CAA artwork + caaCacheTTLMiss int64 = 4 * 60 * 60 // 4 hours for CAA misses + uguuCacheTTL int64 = 150 * 60 // 2.5 hours for uguu.se uploads + + caaTimeOut = 4000 // 4 seconds timeout for CAA HEAD requests to avoid blocking NowPlaying +) + +// headCoverArt sends a HEAD request to the given CAA URL without following redirects. +// Returns (location, true) on 307 with a Location header (image exists), +// ("", true) on 404 (definitive miss — safe to cache), +// ("", false) on network errors or unexpected responses (transient — do not cache). +func headCoverArt(url string) (string, bool) { + resp, err := host.HTTPSend(host.HTTPRequest{ + Method: "HEAD", + URL: url, + NoFollowRedirects: true, + TimeoutMs: caaTimeOut, + }) + if err != nil { + pdk.Log(pdk.LogDebug, fmt.Sprintf("CAA HEAD request failed for %s: %v", url, err)) + return "", false + } + if resp.StatusCode == 404 { + return "", true + } + if resp.StatusCode != 307 { + pdk.Log(pdk.LogDebug, fmt.Sprintf("CAA HEAD unexpected status %d for %s", resp.StatusCode, url)) + return "", false + } + location := resp.Headers["Location"] + if location == "" { + pdk.Log(pdk.LogWarn, fmt.Sprintf("CAA returned 307 but no Location header for %s", url)) + } + return location, true +} + +// getImageViaCoverArt checks the Cover Art Archive for album artwork. +// Tries the release first, then falls back to the release group. +// Returns the archive.org image URL on success, "" on failure. +func getImageViaCoverArt(mbzAlbumID, mbzReleaseGroupID string) string { + if mbzAlbumID == "" && mbzReleaseGroupID == "" { + return "" + } + + // Determine cache key: use album ID when available, otherwise release group ID + cacheKey := "caa.artwork." + mbzAlbumID + if mbzAlbumID == "" { + cacheKey = "caa.artwork.rg." + mbzReleaseGroupID + } + + // Check cache + cachedURL, exists, err := host.CacheGetString(cacheKey) + if err == nil && exists { + pdk.Log(pdk.LogDebug, fmt.Sprintf("CAA cache hit for %s", cacheKey)) + return cachedURL + } + + // Try release first + var imageURL string + definitive := false + if mbzAlbumID != "" { + imageURL, definitive = headCoverArt(fmt.Sprintf("https://coverartarchive.org/release/%s/front-500", mbzAlbumID)) + } + + // Fall back to release group + if imageURL == "" && mbzReleaseGroupID != "" { + imageURL, definitive = headCoverArt(fmt.Sprintf("https://coverartarchive.org/release-group/%s/front-500", mbzReleaseGroupID)) + } + + // Cache hits always; only cache misses if the response was definitive (404), + // not transient failures (network errors, 5xx) which should be retried sooner. + if imageURL != "" { + _ = host.CacheSetString(cacheKey, imageURL, caaCacheTTLHit) + } else if definitive { + _ = host.CacheSetString(cacheKey, "", caaCacheTTLMiss) + } + + if imageURL != "" { + pdk.Log(pdk.LogDebug, fmt.Sprintf("CAA resolved artwork for %s: %s", cacheKey, imageURL)) + } + + return imageURL +} // uguu.se API response type uguuResponse struct { @@ -20,13 +104,22 @@ type uguuResponse struct { } `json:"files"` } -// getImageURL retrieves the track artwork URL, optionally uploading to uguu.se. -func getImageURL(username, trackID string) string { +// getImageURL retrieves the track artwork URL, checking CAA first if enabled, +// then uguu.se, then direct Navidrome URL. +func getImageURL(username string, track scrobbler.TrackInfo) string { + caaEnabled, _ := pdk.GetConfig(caaEnabledKey) + if caaEnabled == "true" { + if url := getImageViaCoverArt(track.MBZAlbumID, track.MBZReleaseGroupID); 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) } // getImageDirect returns the artwork URL directly from Navidrome (current behavior). @@ -68,7 +161,7 @@ func getImageViaUguu(username, trackID string) string { return "" } - _ = host.CacheSetString(cacheKey, url, 9000) + _ = host.CacheSetString(cacheKey, url, uguuCacheTTL) return url } diff --git a/coverart_test.go b/coverart_test.go index 8d9eeeb..c6be6bf 100644 --- a/coverart_test.go +++ b/coverart_test.go @@ -5,12 +5,70 @@ 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" . "github.com/onsi/gomega" ) +var _ = Describe("headCoverArt", func() { + BeforeEach(func() { + pdk.ResetMock() + host.HTTPMock.ExpectedCalls = nil + host.HTTPMock.Calls = nil + pdk.PDKMock.On("Log", mock.Anything, mock.Anything).Maybe() + }) + + It("returns Location header and definitive=true on 307 response", func() { + host.HTTPMock.On("Send", mock.MatchedBy(func(req host.HTTPRequest) bool { + return req.Method == "HEAD" && + req.URL == "https://coverartarchive.org/release/test-mbid/front-500" && + req.NoFollowRedirects == true + })).Return(&host.HTTPResponse{ + StatusCode: 307, + Headers: map[string]string{"Location": "https://archive.org/download/mbid-test/thumb500.jpg"}, + }, nil) + + result, definitive := headCoverArt("https://coverartarchive.org/release/test-mbid/front-500") + Expect(result).To(Equal("https://archive.org/download/mbid-test/thumb500.jpg")) + Expect(definitive).To(BeTrue()) + }) + + It("returns empty and definitive=true on 404 response", func() { + host.HTTPMock.On("Send", mock.MatchedBy(func(req host.HTTPRequest) bool { + return req.Method == "HEAD" && req.NoFollowRedirects == true + })).Return(&host.HTTPResponse{StatusCode: 404}, nil) + + result, definitive := headCoverArt("https://coverartarchive.org/release/no-art/front-500") + Expect(result).To(BeEmpty()) + Expect(definitive).To(BeTrue()) + }) + + It("returns empty and definitive=false on HTTP error", func() { + host.HTTPMock.On("Send", mock.MatchedBy(func(req host.HTTPRequest) bool { + return req.Method == "HEAD" + })).Return((*host.HTTPResponse)(nil), errors.New("connection refused")) + + result, definitive := headCoverArt("https://coverartarchive.org/release/err/front-500") + Expect(result).To(BeEmpty()) + Expect(definitive).To(BeFalse()) + }) + + It("returns empty and definitive=true when Location header is missing on 307", func() { + host.HTTPMock.On("Send", mock.MatchedBy(func(req host.HTTPRequest) bool { + return req.Method == "HEAD" + })).Return(&host.HTTPResponse{ + StatusCode: 307, + Headers: map[string]string{}, + }, nil) + + result, definitive := headCoverArt("https://coverartarchive.org/release/no-location/front-500") + Expect(result).To(BeEmpty()) + Expect(definitive).To(BeTrue()) + }) +}) + var _ = Describe("getImageURL", func() { BeforeEach(func() { pdk.ResetMock() @@ -27,40 +85,42 @@ var _ = Describe("getImageURL", func() { Describe("uguu disabled (default)", func() { BeforeEach(func() { + pdk.PDKMock.On("GetConfig", caaEnabledKey).Return("", false) pdk.PDKMock.On("GetConfig", uguuEnabledKey).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()) }) }) Describe("uguu enabled", func() { BeforeEach(func() { + pdk.PDKMock.On("GetConfig", caaEnabledKey).Return("", false) pdk.PDKMock.On("GetConfig", uguuEnabledKey).Return("true", true) }) 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")) }) @@ -78,11 +138,11 @@ var _ = Describe("getImageURL", func() { })).Return(&host.HTTPResponse{StatusCode: 200, Body: []byte(`{"success":true,"files":[{"url":"https://a.uguu.se/uploaded.jpg"}]}`)}, nil) // Mock cache set - host.CacheMock.On("SetString", "uguu.artwork.track1", "https://a.uguu.se/uploaded.jpg", int64(9000)).Return(nil) + host.CacheMock.On("SetString", "uguu.artwork.track1", "https://a.uguu.se/uploaded.jpg", uguuCacheTTL).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)) + host.CacheMock.AssertCalled(GinkgoT(), "SetString", "uguu.artwork.track1", "https://a.uguu.se/uploaded.jpg", uguuCacheTTL) }) It("returns empty when artwork data fetch fails", func() { @@ -90,7 +150,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()) }) @@ -103,8 +163,185 @@ var _ = Describe("getImageURL", func() { return req.URL == "https://uguu.se/upload" })).Return(&host.HTTPResponse{StatusCode: 500, Body: []byte(`{"success":false}`)}, nil) - url := getImageURL("testuser", "track1") + url := getImageURL("testuser", scrobbler.TrackInfo{ID: "track1"}) Expect(url).To(BeEmpty()) }) }) + + Describe("CAA enabled", func() { + BeforeEach(func() { + pdk.PDKMock.ExpectedCalls = nil + pdk.PDKMock.On("Log", mock.Anything, mock.Anything).Maybe() + pdk.PDKMock.On("GetConfig", caaEnabledKey).Return("true", true) + pdk.PDKMock.On("GetConfig", uguuEnabledKey).Return("", false) + }) + + It("returns CAA URL when release HEAD succeeds", func() { + host.CacheMock.On("GetString", "caa.artwork.album-id").Return("", false, nil) + host.HTTPMock.On("Send", mock.MatchedBy(func(req host.HTTPRequest) bool { + return req.URL == "https://coverartarchive.org/release/album-id/front-500" + })).Return(&host.HTTPResponse{ + StatusCode: 307, + Headers: map[string]string{"Location": "https://archive.org/art.jpg"}, + }, nil) + host.CacheMock.On("SetString", "caa.artwork.album-id", "https://archive.org/art.jpg", int64(86400)).Return(nil) + + url := getImageURL("testuser", scrobbler.TrackInfo{ID: "track1", MBZAlbumID: "album-id", MBZReleaseGroupID: "rg-id"}) + Expect(url).To(Equal("https://archive.org/art.jpg")) + host.ArtworkMock.AssertNotCalled(GinkgoT(), "GetTrackUrl", mock.Anything, mock.Anything) + host.SubsonicAPIMock.AssertNotCalled(GinkgoT(), "CallRaw", mock.Anything) + }) + + It("falls through to direct when CAA misses and uguu is disabled", func() { + host.CacheMock.On("GetString", "caa.artwork.album-id").Return("", false, nil) + host.HTTPMock.On("Send", mock.MatchedBy(func(req host.HTTPRequest) bool { + return req.URL == "https://coverartarchive.org/release/album-id/front-500" + })).Return(&host.HTTPResponse{StatusCode: 404}, nil) + host.HTTPMock.On("Send", mock.MatchedBy(func(req host.HTTPRequest) bool { + return req.URL == "https://coverartarchive.org/release-group/rg-id/front-500" + })).Return(&host.HTTPResponse{StatusCode: 404}, nil) + host.CacheMock.On("SetString", "caa.artwork.album-id", "", int64(14400)).Return(nil) + host.ArtworkMock.On("GetTrackUrl", "track1", int32(300)).Return("https://example.com/art.jpg", nil) + + url := getImageURL("testuser", scrobbler.TrackInfo{ID: "track1", MBZAlbumID: "album-id", MBZReleaseGroupID: "rg-id"}) + Expect(url).To(Equal("https://example.com/art.jpg")) + }) + + It("falls through to uguu when CAA misses and uguu is enabled", func() { + pdk.PDKMock.ExpectedCalls = nil + pdk.PDKMock.On("Log", mock.Anything, mock.Anything).Maybe() + pdk.PDKMock.On("GetConfig", caaEnabledKey).Return("true", true) + pdk.PDKMock.On("GetConfig", uguuEnabledKey).Return("true", true) + + host.CacheMock.On("GetString", "caa.artwork.rg.rg-id").Return("", false, nil) + host.HTTPMock.On("Send", mock.MatchedBy(func(req host.HTTPRequest) bool { + return req.URL == "https://coverartarchive.org/release-group/rg-id/front-500" + })).Return(&host.HTTPResponse{StatusCode: 404}, nil) + host.CacheMock.On("SetString", "caa.artwork.rg.rg-id", "", int64(14400)).Return(nil) + + host.CacheMock.On("GetString", "uguu.artwork.track1").Return("https://a.uguu.se/cached.jpg", true, nil) + + url := getImageURL("testuser", scrobbler.TrackInfo{ID: "track1", MBZReleaseGroupID: "rg-id"}) + Expect(url).To(Equal("https://a.uguu.se/cached.jpg")) + }) + + It("skips CAA when no MBZ IDs are present", func() { + 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")) + host.HTTPMock.AssertNotCalled(GinkgoT(), "Send", mock.Anything) + }) + }) +}) + +var _ = Describe("getImageViaCoverArt", func() { + BeforeEach(func() { + pdk.ResetMock() + host.CacheMock.ExpectedCalls = nil + host.CacheMock.Calls = nil + host.HTTPMock.ExpectedCalls = nil + host.HTTPMock.Calls = nil + pdk.PDKMock.On("Log", mock.Anything, mock.Anything).Maybe() + }) + + It("returns cached URL on cache hit", func() { + host.CacheMock.On("GetString", "caa.artwork.album-123").Return("https://archive.org/cached.jpg", true, nil) + + result := getImageViaCoverArt("album-123", "rg-456") + Expect(result).To(Equal("https://archive.org/cached.jpg")) + host.HTTPMock.AssertNotCalled(GinkgoT(), "Send", mock.Anything) + }) + + It("returns empty on cache hit with empty string (known miss)", func() { + host.CacheMock.On("GetString", "caa.artwork.album-123").Return("", true, nil) + + result := getImageViaCoverArt("album-123", "rg-456") + Expect(result).To(BeEmpty()) + host.HTTPMock.AssertNotCalled(GinkgoT(), "Send", mock.Anything) + }) + + It("returns release URL on 307 and caches it", func() { + host.CacheMock.On("GetString", "caa.artwork.album-123").Return("", false, nil) + host.HTTPMock.On("Send", mock.MatchedBy(func(req host.HTTPRequest) bool { + return req.URL == "https://coverartarchive.org/release/album-123/front-500" + })).Return(&host.HTTPResponse{ + StatusCode: 307, + Headers: map[string]string{"Location": "https://archive.org/release-art.jpg"}, + }, nil) + host.CacheMock.On("SetString", "caa.artwork.album-123", "https://archive.org/release-art.jpg", int64(86400)).Return(nil) + + result := getImageViaCoverArt("album-123", "rg-456") + Expect(result).To(Equal("https://archive.org/release-art.jpg")) + host.CacheMock.AssertCalled(GinkgoT(), "SetString", "caa.artwork.album-123", "https://archive.org/release-art.jpg", int64(86400)) + }) + + It("falls back to release-group when release returns 404", func() { + host.CacheMock.On("GetString", "caa.artwork.album-123").Return("", false, nil) + host.HTTPMock.On("Send", mock.MatchedBy(func(req host.HTTPRequest) bool { + return req.URL == "https://coverartarchive.org/release/album-123/front-500" + })).Return(&host.HTTPResponse{StatusCode: 404}, nil) + host.HTTPMock.On("Send", mock.MatchedBy(func(req host.HTTPRequest) bool { + return req.URL == "https://coverartarchive.org/release-group/rg-456/front-500" + })).Return(&host.HTTPResponse{ + StatusCode: 307, + Headers: map[string]string{"Location": "https://archive.org/rg-art.jpg"}, + }, nil) + host.CacheMock.On("SetString", "caa.artwork.album-123", "https://archive.org/rg-art.jpg", int64(86400)).Return(nil) + + result := getImageViaCoverArt("album-123", "rg-456") + Expect(result).To(Equal("https://archive.org/rg-art.jpg")) + }) + + It("caches empty string when both release and release-group fail", func() { + host.CacheMock.On("GetString", "caa.artwork.album-123").Return("", false, nil) + host.HTTPMock.On("Send", mock.MatchedBy(func(req host.HTTPRequest) bool { + return req.URL == "https://coverartarchive.org/release/album-123/front-500" + })).Return(&host.HTTPResponse{StatusCode: 404}, nil) + host.HTTPMock.On("Send", mock.MatchedBy(func(req host.HTTPRequest) bool { + return req.URL == "https://coverartarchive.org/release-group/rg-456/front-500" + })).Return(&host.HTTPResponse{StatusCode: 404}, nil) + host.CacheMock.On("SetString", "caa.artwork.album-123", "", int64(14400)).Return(nil) + + result := getImageViaCoverArt("album-123", "rg-456") + Expect(result).To(BeEmpty()) + host.CacheMock.AssertCalled(GinkgoT(), "SetString", "caa.artwork.album-123", "", int64(14400)) + }) + + It("does not cache miss on transient failure", func() { + host.CacheMock.On("GetString", "caa.artwork.album-123").Return("", false, nil) + // Both requests fail with network errors (transient) + host.HTTPMock.On("Send", mock.MatchedBy(func(req host.HTTPRequest) bool { + return req.URL == "https://coverartarchive.org/release/album-123/front-500" + })).Return((*host.HTTPResponse)(nil), errors.New("connection refused")) + host.HTTPMock.On("Send", mock.MatchedBy(func(req host.HTTPRequest) bool { + return req.URL == "https://coverartarchive.org/release-group/rg-456/front-500" + })).Return((*host.HTTPResponse)(nil), errors.New("timeout")) + + result := getImageViaCoverArt("album-123", "rg-456") + Expect(result).To(BeEmpty()) + // Should NOT cache the miss since failures were transient + host.CacheMock.AssertNotCalled(GinkgoT(), "SetString", mock.Anything, mock.Anything, mock.Anything) + }) + + It("tries only release-group when MBZAlbumID is empty", func() { + host.CacheMock.On("GetString", "caa.artwork.rg.rg-456").Return("", false, nil) + host.HTTPMock.On("Send", mock.MatchedBy(func(req host.HTTPRequest) bool { + return req.URL == "https://coverartarchive.org/release-group/rg-456/front-500" + })).Return(&host.HTTPResponse{ + StatusCode: 307, + Headers: map[string]string{"Location": "https://archive.org/rg-art.jpg"}, + }, nil) + host.CacheMock.On("SetString", "caa.artwork.rg.rg-456", "https://archive.org/rg-art.jpg", int64(86400)).Return(nil) + + result := getImageViaCoverArt("", "rg-456") + Expect(result).To(Equal("https://archive.org/rg-art.jpg")) + }) + + It("returns empty when both IDs are empty", func() { + result := getImageViaCoverArt("", "") + Expect(result).To(BeEmpty()) + host.HTTPMock.AssertNotCalled(GinkgoT(), "Send", mock.Anything) + host.CacheMock.AssertNotCalled(GinkgoT(), "GetString", mock.Anything) + }) }) diff --git a/go.mod b/go.mod index 7434ec9..ad204e2 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module discord-rich-presence go 1.25.0 require ( - github.com/navidrome/navidrome/plugins/pdk/go v0.0.0-20260303204839-f03ca44a8ec4 + github.com/navidrome/navidrome/plugins/pdk/go v0.0.0-20260320221607-03844a9a369a github.com/onsi/ginkgo/v2 v2.28.1 github.com/onsi/gomega v1.39.1 github.com/stretchr/testify v1.11.1 diff --git a/go.sum b/go.sum index 3aa11a6..c9cf8be 100644 --- a/go.sum +++ b/go.sum @@ -33,8 +33,8 @@ github.com/maruel/natural v1.3.0 h1:VsmCsBmEyrR46RomtgHs5hbKADGRVtliHTyCOLFBpsg= github.com/maruel/natural v1.3.0/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE= github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A= -github.com/navidrome/navidrome/plugins/pdk/go v0.0.0-20260303204839-f03ca44a8ec4 h1:LgSTogYiu31eQF8BMh3fDuIcZ82chzIZDi/U/HZYYbA= -github.com/navidrome/navidrome/plugins/pdk/go v0.0.0-20260303204839-f03ca44a8ec4/go.mod h1:5aedoevIXlwUFuR7kbd/WkjaiLg87D3XUFRGIwDBroo= +github.com/navidrome/navidrome/plugins/pdk/go v0.0.0-20260320221607-03844a9a369a h1:EHllNfhSpL6F3EqM4M0GDHQZb7DyClw0y7afddd8XPg= +github.com/navidrome/navidrome/plugins/pdk/go v0.0.0-20260320221607-03844a9a369a/go.mod h1:5aedoevIXlwUFuR7kbd/WkjaiLg87D3XUFRGIwDBroo= github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI= github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE= github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= diff --git a/main.go b/main.go index 52871f8..f149285 100644 --- a/main.go +++ b/main.go @@ -29,6 +29,8 @@ const ( usersKey = "users" activityNameKey = "activityname" spotifyLinksKey = "spotifylinks" + caaEnabledKey = "caaenabled" + uguuEnabledKey = "uguuenabled" ) const ( @@ -193,7 +195,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, LargeURL: spotifyURL, SmallImage: navidromeLogoURL, diff --git a/main_test.go b/main_test.go index ce399fb..0f7f2b2 100644 --- a/main_test.go +++ b/main_test.go @@ -122,6 +122,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) pdk.PDKMock.On("GetConfig", spotifyLinksKey).Return("", false) @@ -174,6 +175,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) pdk.PDKMock.On("GetConfig", spotifyLinksKey).Return("", false) diff --git a/manifest.json b/manifest.json index ec9304a..a0a053f 100644 --- a/manifest.json +++ b/manifest.json @@ -14,7 +14,8 @@ "requiredHosts": [ "discord.com", "uguu.se", - "labs.api.listenbrainz.org" + "labs.api.listenbrainz.org", + "coverartarchive.org" ] }, "websocket": { @@ -60,6 +61,12 @@ ], "default": "Default" }, + "caaenabled": { + "type": "boolean", + "title": "Use artwork from Cover Art Archive (for MusicBrainz-tagged music)", + "description": "When enabled, attempts to fetch album artwork from the Cover Art Archive using MusicBrainz IDs. Takes priority over other artwork methods.", + "default": false + }, "uguuenabled": { "type": "boolean", "title": "Upload artwork to uguu.se (enable if Navidrome is not publicly accessible)", @@ -118,6 +125,10 @@ "format": "radio" } }, + { + "type": "Control", + "scope": "#/properties/caaenabled" + }, { "type": "Control", "scope": "#/properties/uguuenabled" From 5f57906aca8092c5932f310367754380ae5c65eb Mon Sep 17 00:00:00 2001 From: deluan Date: Fri, 20 Mar 2026 20:48:12 -0400 Subject: [PATCH 2/7] docs: add Cover Art Archive documentation to README --- README.md | 37 ++++++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 4b483d2..1524a52 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ Based on the [Navicord](https://github.com/logixism/navicord) project. - Displays playback progress with start/end timestamps - Automatic presence clearing when track finishes - Multi-user support with individual Discord tokens +- Optional album art from [Cover Art Archive](https://coverartarchive.org) for MusicBrainz-tagged music - Optional image hosting via [uguu.se](https://uguu.se) for non-public Navidrome instances Discord Rich Presence showing currently playing track with album art, artist, and playback progress @@ -50,6 +51,7 @@ We don't provide instructions for obtaining the token as it may violate Discord' - **Client ID**: Your Discord Application ID from Step 2 - **Activity Name Display**: Choose what to show as the activity name (Default, Track, Album, Artist) - "Default" is recommended to help spread awareness of your favorite music server 😉, but feel free to choose the option that best suits your preferences + - **Use artwork from Cover Art Archive**: Enable this if your music has MusicBrainz tags (see Album Art section below) - **Upload to uguu.se**: Enable this if your Navidrome isn't publicly accessible (see Album Art section below) - **Enable Spotify link-through**: Enable this to make track title and album art clickable links to Spotify - **Users**: Add your Navidrome username and Discord token from Step 3 @@ -83,7 +85,18 @@ For album artwork to display in Discord, Discord needs to be able to access the 2. **Restart Navidrome** (required for ND_BASEURL changes) 3. In plugin settings: **Disable** "Upload to uguu.se" -### Option 2: Private Instance with uguu.se Upload +### Option 2: Cover Art Archive (for MusicBrainz-tagged music) +**Use this if**: Your music is tagged with MusicBrainz IDs + +**Setup**: +1. In plugin settings: **Enable** "Use artwork from Cover Art Archive" +2. No other configuration needed + +**How it works**: The plugin checks the [Cover Art Archive](https://coverartarchive.org) for album artwork using the track's MusicBrainz Release ID. If the specific release has no art, it falls back to the Release Group (which finds art from any edition of the same album). The resolved image URL is passed directly to Discord — no upload needed. Results are cached for 24 hours. + +**Note**: This option takes priority over uguu.se and direct Navidrome URLs when enabled. It only works for tracks that have MusicBrainz IDs in their metadata — tracks without IDs will fall through to the next method. + +### Option 3: Private Instance with uguu.se Upload **Use this if**: Your Navidrome is only accessible locally (home network, behind VPN, etc.) **Setup**: @@ -95,6 +108,7 @@ For album artwork to display in Discord, Discord needs to be able to access the ### Troubleshooting Album Art - **No album art showing**: Check Navidrome logs for errors - **Using public instance**: Verify ND_BASEURL is correct and Navidrome was restarted +- **Using Cover Art Archive**: Verify your music has MusicBrainz IDs (check file tags for `MUSICBRAINZ_ALBUMID`) - **Using uguu.se**: Check that the option is enabled and your server has internet access ## Configuration @@ -119,6 +133,11 @@ 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 music is tagged with MusicBrainz IDs and you want album art from the Cover Art Archive +- **What it does**: Checks the [Cover Art Archive](https://coverartarchive.org) for artwork using MusicBrainz Release ID, with a fallback to Release Group ID. Takes priority over other artwork methods when enabled. +- **When to disable**: Your music isn't tagged with MusicBrainz IDs + #### 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 @@ -150,7 +169,7 @@ The plugin implements three Navidrome capabilities: | Service | Usage | |-----------------|------------------------------------------------------------------------------------------------------| -| **HTTP** | Discord API calls (gateway discovery, external assets registration), ListenBrainz Spotify resolution | +| **HTTP** | Discord API calls (gateway discovery, external assets registration), Cover Art Archive lookups, ListenBrainz Spotify resolution | | **WebSocket** | Persistent connection to Discord gateway | | **Cache** | Sequence numbers, processed image URLs, resolved Spotify URLs | | **Scheduler** | Recurring heartbeats, one-time presence clearing | @@ -177,13 +196,13 @@ Navidrome plugins are stateless - each call creates a fresh instance. This plugi ### Image Processing -Discord requires images to be registered via their external assets API. The plugin: -1. Fetches track artwork URL from Navidrome -2. Registers it with Discord's API to get an `mp:` prefixed URL -3. Caches the result (4 hours for track art, 48 hours for default image) -4. Falls back to a default image if artwork is unavailable +Discord requires images to be registered via their external assets API. The plugin resolves artwork URLs using a priority chain: -**For non-public Navidrome instances**: If your server isn't publicly accessible (e.g., behind a VPN or firewall), enable the "Upload to uguu.se" option. This uploads artwork to a temporary file host so Discord can display it. +1. **Cover Art Archive** (if enabled): HEAD request to check for artwork by MusicBrainz Release ID, with fallback to Release Group ID. The resolved `archive.org` URL is used directly. +2. **uguu.se** (if enabled): Fetches artwork from Navidrome and uploads to temporary hosting. +3. **Direct URL**: Uses the Navidrome artwork URL directly (requires public instance). + +The resolved URL is then registered with Discord's external assets API to get an `mp:` prefixed URL, which is cached (4 hours for track art, 48 hours for default image). Falls back to a default image if artwork is unavailable. ### Spotify Linking @@ -206,7 +225,7 @@ Resolved URLs are cached (30 days for direct track links, 4 hours for search fal |----------------------------------|-------------------------------------------------------------------------------------| | [main.go](main.go) | Plugin entry point, scrobbler and scheduler implementations, Spotify URL resolution | | [rpc.go](rpc.go) | Discord gateway communication, WebSocket handling, activity management | -| [coverart.go](coverart.go) | Artwork URL handling and optional uguu.se image hosting | +| [coverart.go](coverart.go) | Artwork URL handling, Cover Art Archive lookups, and optional uguu.se image hosting | | [manifest.json](manifest.json) | Plugin metadata and permission declarations | | [Makefile](Makefile) | Build automation | From 24615fda7b5f5cd0627e40f165ff7513a58a2f04 Mon Sep 17 00:00:00 2001 From: deluan Date: Fri, 20 Mar 2026 21:05:47 -0400 Subject: [PATCH 3/7] docs: add config screenshot and convert images to WebP --- .github/screenshot.png | Bin 37515 -> 0 bytes .github/ss-config.webp | Bin 0 -> 26492 bytes .github/ss-richpresence.webp | Bin 0 -> 6276 bytes README.md | 4 +++- 4 files changed, 3 insertions(+), 1 deletion(-) delete mode 100644 .github/screenshot.png create mode 100644 .github/ss-config.webp create mode 100644 .github/ss-richpresence.webp diff --git a/.github/screenshot.png b/.github/screenshot.png deleted file mode 100644 index 741ef07f14c98ce016dd657c2b9171da85b47cae..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 37515 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 diff --git a/.github/ss-config.webp b/.github/ss-config.webp new file mode 100644 index 0000000000000000000000000000000000000000..cb99c3dd001b42a579a13b72d01d63aef394e778 GIT binary patch literal 26492 zcmZs?W0YpimM)rhW~FW0w(UyWwr$(Cot3JzZQHiZ_kQR0?tS`n_n$Rl{)&jP9*sF> zC`*ZpyO;w3sf!6Kswr|3TKuzi2m#IorcMA=1L3!2PnRMkE-EBmfCdVHkF>P=a?u8SN$xX%^nJneYbuJTr0c^m=W9p2Hzr{7gM{&xFNf493K;MqIj6%LTu2rlx^0?2&> z)?k+bdH$6E$RBlp`a6T2zR1t(56xHkx4?Bi0PyY$`_Awb|ERwM@C3910AIjAd|!aC z+c$w-fqMYp#~vU6DE_ifGT#D>`j395e1iNKek*+EzX?3*H~LEp^a9oaI6vtxtsB2i z0DHZ+{xJZB2l%hfkKV7{+uc(E+g<-U}_zoDcBd0<5A!A;qYF z4i>do2>n6=qe9HmdgWwiS|Gev`Xpk+JelmrryH4Iz44eb$>B>`;-s8E-;8q zaG~#IS6X6uhq;p*ZH(-n8KC`nsU=d-rA#rvg|)sq(_&+|{U2Az9R53HRjiXwfZ*eA!^>;*pPNHiD?OtR)Q*u?L07Ity$MOucO63w%-+S%wLRyX_cZt{&XuK5_ z7zI9hhcy-yNMp&2iX5Z*O*igTpgYXoLHd79ww-3*+UEY6N3FC=-Xjn9Clj2xl5N!7kSUS5TTFnrqFbf)Mzb9MF1A zSu#CT8*OFa9#$0H+#y@8e1S#jj6{0QC7zMl{{bcIpy!lXb2YSCGq^-=-s|A(q*oG) z81Uo&`0M-O$0c#V#FAw)Dq~W>Gy((2!EC>Ro9Y3~%g}ABB0_DS#78L#yt)=2MvPAX z3!sRvDI!1ThiENonz9y5ohEQ!l!hk4eSLwJa0imHJv_X}85gHhBB<>7p_l^aT$pu)vCv8u5vbyQ#k3JE<#C3h5Wv zqR791)tqh9I?rn)o<|0@5a!&NbQozq01>>-Oqv}jk1C;h0O2|YPe$1p{85`(FYemg zQbzxG*U*|DRMOTRH*ezqhMN+E(d{NHLk0a=dPJ1`^aU_}p93{-oenh9ln8Z<0ki)k z;(zec%+S=Gb{MdKJC?xqpwvs$2X;r$uswfzOG)RCuJ+q2^sBMyYv0EV$`0gB;Oc@- zIC&wV$plX}m6nXeTv7mux4lDy&Ckoh((P%ewVMj^ummUU9qTZM`%6axg6#iMfTc_O zGnc+OlOMwvonmAexBrFD|Bc@NS&~z-k<4~W+HKC(v>Q|fw=T0vTGu2TWO=%)olP^r z5_fp&k-Q{M-6kw}-QG}Cfj1Jy&LUIm(C||*LkG}J+;?z*r<()jLsJ)NqhJ69s}kXs z#4S{7?3tp40WW15JG)sKDNc&ANXDp>k0N>17N;^ckd`lsSZa43v98^U_k9>~a<^Rw zcVHqn>cYGvvf~-o+07ocGDB5$FL+BbY?Y2=^(JR}->N^q6RiTD2A93E7BBYxukLZt zYGOTxhH&16k$Yl?Efu4Pquw~Ub&a7yXC7$|krieDn0}1EMt*#(pShacP`(f+s7i&~ zN1fwN@n(2(+}U^CnFoXZ9%_yu%SQ9J8aDugA{M_JYQ8*s=I#xR9Gl_@dLu`rM()~y zPW(&%_6~<>JQvcwZJ84FwmB<+3*%C(i#gL4tS|2pob!#9%<>?Rnqj!?XCxc*Z+vRe(>h(K4lz)BgfGWr)7R0LkQyKI&7BQNrF|Uu{Y9F9FNBAi^;W z7<8X+4>KtJDW8ozFjd$jkW@jp{;PVey!cvM^o0fz%KoA(sZDT2a*KNNbv_HQ$?7sg~`7?d7U7AI!UKA$eINVlbL-$cP+jyfxVZ&?lWF52 z{a4HS52j>Gn-*FZNQRrEfvmIRqbxh&{(uk?(8EP#7-qg6pg%SMBL_UH1xm;MR`I)1 ziT2irmY783@o^IRmvsFX7t9vF831$PPf{l8gSD2%Dv_u$`= zD#C`;-~#4ocaHN&`~8puT(Itxn&O8~p{7%f|7p1_8)mc2EgA$Y3!+G|`~TBL#R+3^ z%l@Z-{@YIHgSFPj&Jr$@S^fn`&Kt+A!~cs<&db*3@-<1dN+v@J_2D`GHR}j_GUNM* ziiZDU)&Jr$P#s~`FilJ#;Lv|LeG-*>__AZmgO)6xw3}t9_=6_!j2xK_UQu}Z@ZjIu z|J&ThFrxC+fT;7W;Neah61qLl6U34@!v5bx@PD=;pe_u=Xx+{~ZY8(VLeexxYhK+{b;?fvP+~B65&3ZL^r8!i%;>Z9dyU%CCmh~?)9zSl zG}haw}-DeYw?BhRtsRHu0UP$z#W8gobZn;$kzV?E`| zo{DW#A#-C4#$GBGb!n`oUpcMfX4Jipg2e|ey$VJRXmZjSj;#AEeO};W7HR3|fZBaKTHE41h&hcM* zN+aAG7Po|$pg+i%L!Z*ZI(r={5b|$m5+$j|X z6Ann5j#US0URc4UEkK(SLyq>Nq|pEI+v2q*Z%WzZn@6;o!NyuPh=gi!hF)OI2SO|C z<7M_dxIL1jH#xx6O`EMbN0lp+PwFdnp2wm#Dbm>v`6oi6<=u_k4WSK%62t6A5|cc7 zn*Wm|FOmNz3;%1<925ll0W?>V(XmXnjl@|f`iU8Na)#soY5Lj)Fzg~`Q>>@8d7A=J zG^+7--gCm@x{1T2ouDX5(UYq#hygd>{GfiB1cy0nyEYA_S%5Huyr#jOy6FQ=PWQJq zTsw}^3*Flcwk1ja{CL_eG7RcYsLAKXDWJh+DJsU+(Be#+qTIg3GV5X~svfhGHMk`v zf-buKdMp^-ITH1|M_9eQ>GXdKq(hMe#;>KvmD*-x;OgpbYX9g(;Ow7dF^O)C`|F@svq)zS$JOH+2 zr%|co{P+Ybm}R?>JyjEZC5n>DLnH@L85cjKzh~>^cwt(;9vIM{pI0;5?;1R?yqPlX zr#tNd8WhTC?$$mL-1S^H*K3zDZ>)>6I&Sye|6yO5<3(|$*1PLdxZZ`~E13p`luB)E z(?~ZYA;`rnPY%jn1oaN=)O~quIM91rLBHC1Kb{75%h?_FNhuSAnN55|3z~5e45uN; z8{(}ZtBEP{*v_)kU`@&WwD9qbPe5MOnz=EIrrQUW)a+QHSTab(!=gWnc#nWvlaUaI zDgqwV`l~WHcM(T>!aWTLW_jLC6ME$>IX2%kv*-I11&x}nF>5je#tr9#y)(W zU-(H$d6oVa`F}A^V1U5g@1DPSkz)F0cGNAUgG-*onM&Hzq=#$3Ah4lTHReZq`(SpI zDtE3xY7k$`AJ`E`K>SHZV=$2;qYiWN)4kv@)oBW$f;<^lvd2C_Cb*cu{5Ie2Hll|U zmYOzPihLGL;FU>2ZH4Nhq{$s;=Gf%qXQ<-gD&yDZ>2Iex*Qrak6ZBJG+l1N#1X2%1bLXK{-!PGJ%1(zWDIp%F)@06wbP_a?LfnNxVQ^i&NJ!^TMN8ewe>=b}8Mz*1jN?Q@!J* z-^?@Dg>3_f_*~=QciNF_2DmASW}G(ciH*pjt8=_%ogJn~r7CW|EQLkPAHkBDhENX(oD-nIiOJMqOXwJo4dA$6>i1}@>}X26fVGasQJ>U)>8m#8cAe+fRU|l z+-4)L>XKgcEaD4!hY64oe>JQAEt65-fvfhvGK#r!`Sv-|h}LQHS+I|65}lIy zz{Y?gYILMj4Wo*R{*t#PD^+2MWI4D@PC!rC84D?qP)vy-Dzh~B@JAI|CnQ>XU^>!Y zsyzyREyWtJ>wTmf{yRBQrXk2`z#_qe(pCu+kSh7C({l&0G#{I4At#!7fW-*2CLc1R zefvy?#q35-hN>Pe+eO?H!}{E-!Sb1w>qYzzM4O_1iH5L925aifRsVhhB{9i^*8lkk?4N- z5~cAJz~t8=6Dowk0{G(8+f9S4`vTo;1v z{Dyk>GYAjZpB_8ATda6W5`Iv`sAm zk7V(|QF>*v&69-DbqB{`k!i&PX%t8xO*?q!I;{YAFbb=g7n#|oaFO>F zpd7D8r41ge7G{1~)gR0}G#o%6Q>Yh2k(s^3dC_BZ$27S+>Xe4ou+nvh*s~!^?ae(T zlWuR!BpDw+?L4nbaTo53P8!Ao{>=m~iJ7Gm)H}lNrJVhs5p~WlKoTn09q2u=0nETDYO72@~vDU6`^= z#ni_c12X1K!kL)};Kjk9U`gDujde-%K(o8Jgi8;km|Af7n;`~0iq?aBBJKR*yS#6} z$j4O5h|$34Ec<&l5nH#AoZJi-w4sX#BGZQy$OdXB8uhvK+*SbPN^De4qN1|V6W;{` z(ALJYGUeVQ5^spkWl)*wbpAu>wDuH_VuN7#wA2C3TQ)vWt2AvdF>BBk9W=%4h2#Gx zw=NL(xld7SYW_|c?rFFA3|1eVSC1~cb+Q|GwqeaO>L2kOc(HCbFWdGY zzQ0jJPlm#`NciV?QDLFQdp$n7af-mX3Lc}n?`T0cHV@3o`yx^Rovp`+HZ|C}Q6CN+ z`h1l;oDd_~P###_Sy0L)UsS)Oj3k|&jWyg>#g6V6NEH^6Q)L|=QOl>u@|_5Nep)f# zxmml@=O2&)hfFivtf6;qH1x~;7>z^;nBY%D0v5njKS$O`oD>PCSr+|C)TRFSMfzSY zq!HF2w*N`EiY&)^m}HB>!2stKmA~Esl4O!?mHE*<#5%sgOV#Li)rSG<~AFBM$#t(iNpTGALvpLM#PzMxWJs@%Jf$=B!rAU_#kIQy4FOm#x z9vX*QUPHPs=Z*Lw4<};bVe#y_oOeswDVPVN;Yn`AwRvtef*x- zvhV1%phx>SHaCn!m>Hfge$)p$?mFN5BF)_UKgNg;zCZL%fcFuBuk}Bz2~%H&)q_6+ zPn~S9j}!_+UVuQP2?a+}g0}RT+3blgIRqO|6%!gs3){Q*Xw+$+zfC>V)Ay-Um`ia$ z2Dd?>pS3UCbX2IQ0037&Av`B^Z0l(aj#c5rahsKzAQ33XhK>=GQIV#*z;6Vy9y#d- z(A+#eMO;>8G02Ql7hSTF#;#FsgZ`3ORCy$OHHCAsDHRlvoz#UKD@Z3A*l*{8g+Lt* zfeGNw0-MPUwMFHzd2eE(*Sc9QP0*2aF#WR}$gy9jw^N`JQVA8_u6E?Jaeq9F_zocV z0f^$PD)x2l{RF=Qg6*oU;!qPN&z7uK`Y_jfN2=$ZeTZFq*+-oni>h5!y==6fh2#Jj z9%658BL4EH%;tn*sl;lJ+@=#7j?=SUU~gL;wx-s%5a{}LAKq(>t?4dzc+>8475yCT zrmRBWP4kYM*h?&v-kSA(Vj>ZkZ>SO5$maua9LIOUbpl*6W6>IEA3hLJ1n8%e<6_td z(G3||Z#N0v;@0(8*sJ1yT6yJa7rB1Lz7gR1=GbZq$kAQA6MnKhu2^e(6dRe*yh%L9 z4CgDhRzqXl9=xKlhSoBFE2EaKugzF?*kzGC!R7!**LB&S((OR3XYMp`A9kMP`nRmC zzZ$HxEnb2LvnC=?FgL0gM`kiFOnY^5vgwA+WSYODVoOwt#Lk6%8??bn&X&j(?j;7p zp|&@_;>BuRUBewzp-ZvnKa0$*p$N=KFYqQX<_lAc<+qbYq(onTM2S`tIR0qS4~;0| z{m4j+nP0yer@|r9)}ad;pqrk9&@ckznsQ@tM^w$@L;S<8Ns&+JkwL|^|1LQLiJ<@s zYcyHimcuMy(2G)zKCQhUQ*ao83fdQ!54bhN+xZHO0~la~ul_3P*y|Wq2Y>RV(Ty!Nc^E!J z%8$UY6vA_mIAd4Yy*hr^enz@1k6o*pdm7&%D^UhD&EFl5{p;HRk3(EI2A&v7%Tznw zCPyWyolg(__9*8Z<~lkGkdNM!tbl7zKS4&Qu=0Pwh-K)T{gQ__6x(!;l) z@+<8pmWD@(`>ECM;0C4%L~s;iME-+AdFMF<1^^MoE~By|i~60sSTv+=P>yr)Ge)Ft z!`GRISp=y5`cOWbBzK;CplTio3upNzDge3j;?=s~=azvuo#DbK)xE0{BN}qfn9Qg0 zF=~|-W59ifbt4nI2uhtSQ!FYV=KVhAOM;jku;f55o2xlTq_>9O9sRMqLV)DgSX^G; zTE+l>-s!py8OL?D9n~UeNrNbUclEEj%{!ZBW^~FfK{C6{3#8{f<=QMcF&I5oaKL-G z)#ZV(6K5R`kXqx9N(jv2OEqcvf#qMdMqZPzvoB>()D+X;gb`toSI*>b=Aou+Bnba~ zJA&ql@3rWYms&uuH^wI900I4N8g&S&l$Vz72US+l0PBet`7@`e174}iqhtKAEth80 z{kg!@@IlM@CLXF~JDgQm=GBATUcv7BhDuu=f?=Pn+i5ce!dT%ll;#PsQDZCLpsL8u zj*uyZyN1xJwu!hq=QiB~ zl@D5&{JYP#+2m0w8^3O~yYb4Hp9FRd^mi{VdHc99+Fqp*m+=@@Vof*|4Or6m1DhBo zkzp)H+Xk%^@$2_{PK2~ft+)9zQ2WXJpwd$qLd?^bz!H@ZKnnp-W1` zdU|FzJhbUUGc(1RACXE{d63NhhrE*-TWn+^;=SMq+Z)}iB8-x4j6Y>`To-f3>{o9r zeTKcuXR7sYm65QTRin4Re-`IUj#^@EXwvOw1{a2uO`h5@Q&kmIz7OB3Cy)U?ha5_a zcnGU#q_g1RtUekv-CCNd8jFZLhKIN+b>W-sgB&4D3Y82*X9zP-;{jNyVef|lm7Unu zbp=pNYLF$ybXa9x;GH;Zt(KE@AJY^$mNc%x+|;Afzp(iqMSpihpF$eIo53y^qP9SJ z;LS_H^#^2|gr@zGmp}cK#rXhKrV&oC3))B2Q0vjmSh{W%Ea=)<17}9pYew;m1Rl?` z7_i;a9n+p(`$4TP@}th?1JLSB^*{Woqfxs-P3#L;mVy!)G}6Gv9OE*?a(lfszLbIJ z)Hkq1oYLaF)u(^Jc8Gl>5DSGvQfI(+(IoXRqPnyF7C73gewml)lWIUx!L6-?rJ-sg zD6IZk*k`HU@LX#5QvqFbO^xHTwfSCwhrYIlaws@2J_#mdBnqKsuZ=~xpqxv?6+!ZM zlcZVnCXy-IvfElkeyK5AJdSwK+0o>;sg6FMAo#y$GK;Hl963}=qur2 zJrL=?|6-v=gE!N<%$~4br5$1a(F(E+S2G%XsCO_wF~BTM&PLlQxjL6!Z3*S^^qgzD zQ+D8)$>LlRHJz)Fa-D&?4kA^)c>b&aD*9cn1DbN|9`yxLiDf#om+bX8_^rz^-;lUk zPvi$E&NG58dJ>aCY&Lh+qP}Odz7OyJ)hI$mj>s$bApE$8k{pkO2l6ANTOO+5wY@8# zOrltEy+bjr1~)I6O2Jgi&H~!p8dB}e66Rd( zg%0Kqdf(Kf^$zx%J{w@Q)Fr1$E>E3bi9-_17 z?e({2MB$CMDQ7t}a8zn}ZBh@ry)x0ulq$S*i7qJVF@`Z*yb0m+QzRthw_|tQA}z(d zD!c`l`6P7c?MCd^pu^@M3l)_zTv=Vge z1;j`^m2XUNzqU1}xY4uZ<+_~|XHr=ulf)DPZTC+rLNjL6zd~ArSOyVadVDcRVxCRM zPaJerp3e6Fe}(hv?ao=oYXHZZrmCP}=Zt1KVO7noA?X(7{%wl6$dIk%dym}O`_z_3 zBq&}M0wQp*JXC<(p1ZM&FsQ^Uzj_x6lUo$d4J1m>;1bWexEKqkbn&-~&{k9Ie%HF< zljRtW3*L?ywkj`?1K_9INd=H7eN>IOdGPy;m$9&6GM-$yr%UrK=n+wNy~%giKy)R& zQ7ScyQw+U+S-%FJ=m4b+MN$N&5K+kt#<~`9gxC>0M8@*dOY5&iD95Eg4k5ICYc@-$ zb+HWvW2Dc*`(~dSQ5y*}$0W|!BoQVehe7d6S|4+moXss)`hJ-Z8F&wwO92AZ9r|8y z@H#Z~9v=JG$Hxb|ZLRglIR})WMq$JuW=_ac?WNNTC*^dAeK|M6hO7TcK^JItvc3a7 z`HtGg!fuFM&rupSMkXRLTNs7ycO5P9cuhr_cW_t0ReDNN6-0=#gq(f3EF0dhDBO?y z6G|EfEmrp(m}2YOQ)m~SQ`!U73jyQwC=Awgshe@{Jab&yd@whZiLE>Ru3&QBK*qMJ z3R;&~lA`m+W(RY}*+f3mAi{Uov$7pC9zswP6WfHxCMKpDx$1Xs1m2fHf48X~yf1nU zQB7V|IK!_70+nRbsb#Q!rRO8%KaG^%UGmP?kJWl_hsSK`%@SRnce9 zp59@Ehr%lWeFrGtvarvKLzdWNF!lyU0bw1;j z7DsAbS!p^an9#54ULI_1 zFi@&o(-Y8!Zu}L~*lmtQ=G*eJIWSgvj4ZaqspDV^-h|`nK+fKk@+9O`h2w3HKxy?E z2gizDPL*;sB%ky;f;`@zmyfj|`<>K?V=*sG^256bPpuGNxWv}7*+^jUIkbYQ?d!+{ zWjt{0U}uJ!sv+*+v_dm_|JM1!=nJJ0?>thkKnxgCh0A+L*J8jd0!L-W&oBliCX!C) zWQ4Pdt)U53<9Al16?R>8m(^f2l9EaguBhk3T!;UiFI?|QlW(z>a9LWv6A0Toc^nZykP<{(4IcJhE)9#mrb-1fOXk9>=l(4ny?3>67$ zpjDN%fOXKSK+j7%z?8xVgrk~-6}2;I@v+_892m%idD;&R+9z2Uw@|9dyGmn@T*@z| z#YkslMg=|8YZ+3-m-6syK47FJ90fLk4(jqPnj@Pwk7A2_P+1wg#p>m#jY*g>p#R#7!_Wa%&f;43U=oYG7!*rr${R7H9`?Q{#V1gQ9 zfUVh3aHm*M3r>Wfpa8&nP&_AU8Xk}u^=*A`{;RjL>At7~oSN;&q=+P~u(_|MB&G;; z$xq4KSCeI)Zduq?$P%WnlCR*4fRYTQZx*rMF2c3Yc~1WIg^MAhlYPBq^XBTET3^gA`=l3RoL51PeoZ-M z0k)tY>Uc1zmPB8bb==e5neol7Zb?u2<7>i1Ja5cT0 zJUy1Y`^2?9QIKpYTP$-=c`&h~@APCB?d>gb_5}3Eh8@;pbsyDt!CWDit&qsQJ;szU z36pmf#@&NK=yChq)ZQPR)zE%p)b(Kg9Sp_PnIDck-^Fzh2wbN4X9DMiorV{~;)A49 ze4%+fEerV1CeK;oFi!t{8zFaC3r206ckN@hBI(FFLL{Hi1u+|%? zmL3@yuK<;wvXb14?lq6T@lg0ya$c7Jhy3KjC#<)bU|s$F;XvX$Yeo<8vGFN^-!s`> zUCbe2?->LH!!QT~>dU!f;h#o;8`tIGG#DT5KI&~M1j!t|SFtx+Bs4C4-z;pgPzvng zYPYw0d`HEL%6kf;7`zGXyb@rxzi==wWy=QbUBA9gT$bF!4Xp8-Skt=rSEexUH#+Oxu?E1(1_5Ae z94GVR>X#A@+)YtBB`CB{RK%^EOqL7lTLF~Nss@14k>e{tuHz2Vssdg-fb7W_^A8S_ z3eK;KiOkLjh^CK0a%HipbY}6R%Yec9n}cH2+~3lYrz?|O%8$uzn+VqjF%D10yF^1b zYf`gHiHXQ+@{#Cdlm#W>Q@lefS{f^Mxd_i&@LYn%3%y8Erx@PdyNATD;5J{0Ng+P{ zB@2#j_IWSM@IgfepT@o~kWA@B$isH=y3$g~cZ)=D=dinhN`vV*+VP}WKArEuN)^H~ zwX_@21{G+D?-1N_sQiPlx#d5=S<}i_kj7>1Ke9wo!8)m?_%xCm^dC9|egmE=$`#Z& z&uzYi@#$>)4jSEV>h8mhiTMW&3=v?=z`ff<5tAH_rHKS2WVZjtG0|alJnUZ+X*>gc z!g$L5+WwvjQFFuqm+g{VEG@&p*^&lF%-OlmrMNVV$M3K4d^8zcbdRQWwSOa7wEnaw z3E8wP#psToln%4^Yy3=1c`58l+*L+ws};%Id>!MUUr(dm)*a)VwZtbYS&b8m2fQJ< zdyG2D(TdB)T~T#mXl@x^xhqw_diyH5Q)$_Z3;dtSAV6UZ&_KLp+>ivutHC0>ZZR^; z`^maMdn^xqb3K7lTwMP6c8!LQ=01=+eC7%S)2q>y^n8pCUZeOE%n(n`;g{4XJTR5u zZE4#3(ZJIIb4!Tn5~*E3?CqQik4LV#BU0as>v?G;R%Kbt?CeC6t1-5EgCneOqy52+BHWUXikd9+DcBZW!O(k5hgo9rx@j|Ddn zmw$-GGYs5;vGaN@)VTdhbPae(!Y@+rkM-pF-WoF=YlwRoF*>umaQ}oc0;S1YSDVs} z2+<96IWL%RC6D;kqBC0bT{OR835L~3;1H}h7*1)_6QLc@Gd-(59k`1@q8{G579w0{ zP$tHpn7Np#GNAVY(VV!~6yQGJXWN2+(#4(6WS7d1;%4FXuC6c|eZ9{7g_6|k;kFjG zy3I3}-`F(IFSfrDW2rWrRjj6OP$bb;g?(GKi2>_z;Gbx|nU91Sw}a54eU4;%!_#{p zLy6mg0Orkll(fT>c=odR+r8tSeB?gVr?{-ux{q^>V%)LUcturF%})M=!+_%{;dTt1 z@hJIAe>#Z)IWD9}c8?DpzTEaVdIXsg@TVNG#>_PzR6~$b5QBcoN^+JFlGwapWnO_&wz+C2oW!}#AHe~Fv5ddBZzp_NwT|TRNZ2>Jw%Yx(;=G*k z*Q$s@44$RBu*_dWtcyWLC%|A;)FXIyQOjtvw>E>#`e)=a+1|YzgtO3T@~X=98aunsQ?%S68(P z(*J^9l*L8yDtc5+VA6}7Kx29X9exB)PNdG580*ho-ntx3gWrat23v>fJ!V0r)A2S# zve46eSUd^#G^DgR%Sh%Uh-Gzu1_kI#fOhe5Z-^ogyfQli857gYg&T2_V#9omL3>T7 zOgOQ`=_LJhjl}uh>MP=VCRlOQ39UEBA01fC6IHFa!f_~(iV1wEanIImsBdq`@tSlz zP9sahul2y~L@w!@Nw}*2QiX$dEy3?x404&Jc%KYyuc1R?Zrmickw6)=iQDd?LXjsa zx_Q-58@OYh=UlCoy#PZ~4O7iJS|}d?m`IH?bi^X5w7>{Wx!|P6z-%QLh%pFmyDq3} z>Gg8IPs5JX?ZHPm_%O+yP4~f8!e!i$KO8`V2pe0vvv!7vFcIUL;hw{(-T4byl2zkJd|$|<8Zv%c4+?`E;X=?Ba^RYvC7waKVx5TW~xTHljBVV+AoF>1!Cywtxbo|=aw#8gJ2eB! z*ij$w>+>%vItwjhRXfUYsw&+_dYPCzS)`}%hXQO9$f0%SB*B(Z%J*hr?=kL9yX-OO zIz|@12_DI}Q%>NRqC;B^%=%GF12nJV_oOd3D`G)T~h;2+}TFmT%4l-#T8E zxp++9T%#av@$l~qnovNb>Z}d~Yjy3uN{5PkT2;u7`5@|rD^14ady5A-RY!0$_BT~X!TOcBue1Zk1d;!X&=h7B%=vw`_h-v8j&;yryU{}CR$oVQ}UIOR-d zr*eV&g`*WcV5K@pTrRCzJlU9Nymn40&amrPP{7PJg?K>9O%(;2L&}?Y8T5r4X9gK= zB%?g2Y7#1-Kv3k@B!3V+7M2T)X%=QpN=54=x!G6kb{5;vv3Kx1<&F968WJln8?`b5 zO(e3C{9GKuw~_i>31vqAqOReG%xmldq6?d;nnu^;KGy)LmC>qoc}t+obg=&>V*?BE zX>5bb`5JOjjW4ipvAPRkN805y{&H0Jb}QYonk(ndd7wi z1Q%qZf;Jor9D=flt8J!&@P%e1hb^g8-&^Jqk$`Nf)eiT^PBf;`Q6!w|!WMXrtn}Y!g<(;a{5yazPG|OCB->QNocR}61ZF@XX`5+4 zJgZSf7OYe3p^-7wCpEIM`agvpH*WoRz9y1<%7cICQE7#q8(khs9i0W`@YUBhAgU>n z&%=o1GF8|IG7)lAV*%CFPC8AWQ47Tzg}=*Fr6ax%L1xiA8U%o&D;XI7P-Z%R-qQGV z`uohdXW%?zR&1uslvv|XGtB5vd5Oxc7u_0>Q}Jp$v0eRevKJNVBS~NWGX;3=eUZG* zV;iafb1%YaoSe16V z>j0N*&9BMR#u=p~doGY;jf$^F`F0?Qk(3`%6YMD5YKLuUW11XkQoQ24$Fb3EESZf| zu7Bj&a&q3B@6NXm3$CD11uf_zu9`NZYEPt%-QQ{lrYN{!EP@l~QQyMFv}_~uzvPZD z5K2g>{Dq@;8U|$86|3D^WO;-}x$!4)u^J#-yAVdW1+(?ErltS^nCW^6^QtA^?f06R zR=Q0t#?`t54qDY?6BV?M8eMN&GyaSlG&CQXsdK>L?RbW@qjt}Jdu)L|5VoH6l!G>9){{2`u_Y_GFz7`zl@;5SLReA(OV8_lhgDn+)X$k|~?bj?JkG(VC ze#>6tsKNWsi$EVh&Vj;$>~UywgmtsA!U4HJb%;?Q*3W@I*Y*HdCbogu&nh&_1B&GI zqce9sNx@(Fw@5A04gadol4FW+dbQ+mJlQ+hkWzSkU~y?oZT9eKnK(oC11F#Wm+M?U z26e~+ALaHGXl~fXi8TC0FID_H)tfhF7KN0ucak)EL@#RH5)sxYG4_tJ1l!YHvz&B|L+jcXmA_vFshYK(l?|1^EfepruG zb#e4dw%JjCA>(m)`H~RMmB@y|Pt-wzuiP;AsD=)F@YDr*7jfU(a51e$m?NH74_%OR zE)laT7e!ydBe;q)A6x~?%Gfy=CXtVa^OE33o3z4YDQX-I^u4#8SqelXn=fkH;F84h zSE{w>qaPwvP{HC8ghUmOT32)A(w#bjgDK80u>=H|Nz6&#OsB!?*N{<0O8((9YmEIa zs@x@Mj_9b4>dE!ufF`w>6D`+!;oA&HN3S)58g;1^CgC6-aP<2Ci_((r)pguc8*s#g zkQ!oC#wBd2nX9C{7)toE2O9v*c6^{4hsVDUDonq;8iocagf{h;47h$zi~O`jP7&NOyAq0qL^|%Ikc)i?RhBIwE&Fa^BI2q4gFa1 zj$J0iZK{E^S`upo)hHh}T7@jJQN^@B?;txY5$v{C01fgCQdj4l*T?Lg4QZ>OG)qO#eNO;E1-NOf?! zWm_hG8;q}qi?z2rjt;*YAm$KnrU%HCv=FWkQVpW!*E zBh!@u14>e`^T2XHR*OU+eEbp6iFE9bFF~-J8Ayz;_LeS1`OA+Lixdi>-}8nA(t4ur z(aNgfGGECLu$mp{5qD&Do2<(X)1cU3?Zq(y>V-;QM`{d5DE55zAJmvM$loM5Y4ip++&RSxu=q>Pcjfpp{d z94QSmj=+9>{H#@)8N1=#kwJqJ^|y%fhtz9`z8oEbR6D4rMO_*EB#OeFZMC*#mSCLZ zUUaWDr}w(c10C4pBLX3)*bs~wE`%zm5%}{`ZVU`?BO0F3GO1cKJB>L&0f9CYrERG~ zS>*zrZ(q##NDxnae!BoJfcU_{osZ&Gd^y+W`oYdr!|oz=t+MJVmboNiiu`lEL*R0r zr9kgR0&DVL_)A)GUkMD)2(!w~V#)K(n7bQG#bF(c5X+Z?oZj#hTUNXqv=5M7sreF{ zw0B8{8wqaCVe4_}dx@x|zr=i6lX|z#$>$Zhv=jJc3|UyAiHobcfJFslk7y3o1)Oay zHI7lcw?(X_nq_=)5Ln%9Vu@B&&QiinSKg77#^-a}Ziv&(P$|bYSa1h2|0Grg+1quv zo~Uq}&$~m#WTO72b}f6YG9K17}ArI(RfIS?s2m%iB~;kHx$GfiW8z+QBYA?RlAePmjBQ#_o|3rl1n z1sl76!-(l9(~r+HTB(5CTIL3N;Y+s=a z-Bl!?jDnw*=bhxhT1HjtIdEAK$T9G4;T5mF>H-EbhbIEjQ3@N`tFuFrQ!1i`T3|LM z#C>E>X&Kurw4IJs#<7xur#4%_fv@OQLna~#{TsBkRZxtQ8}BqtkqG6tq2^xDNmoJh&G*WZ z7atr0ic%tjM;+uN0fs}f&_562KTV+SDxu}|lkuvB9WjKPLY9coT|X=RrNW9ygb%*8 z%&NtR0`5k2XUqEb;F?&xq};w_22~pvp(%7rf3NYL9lBXI5r~@O7{Z|{flE&V>~cff z_DgKBxGQ2-*%UrksqCGu45z|%e?f=?TW$*dv5GB=B{b4CHDFA;9&nEBg-vQVrIH;p2F*MM(;3eq#A@WfiBD&R6vvAa$P3*tbEex1=-~x*M`g)V4r!i-m^(zW3D7 zU~iSfE2hMS#NZXu$QQwXb9qYH@>kOg3UbK^z7$bc&vS@2TSns2!Yp$pd3F=q!4qGBu}yYNe_}ssEJvY`hd5+4rOYFp%fl~t*f-GO<5DOuAC4duu0UwHAf)i+;^cbrn`J2p{eUxAV_ z<1i{^`J$bawwCNd&&^)4Y>hS+kbW#Lz9U}f6U5l;7u=SD4X(W3rcm zd$Wo~vX^d`KB#*)?2kM*?x4%6r`5Z`^Zyr*C2-o&^AkKzk;Et+(CJ&y)m?!+r(;F< z#qn#!_#ge*ZGd3ugxXfn<0qd2KPy;^oGGpXbcT@Q@n!Qxlh~tfQYjYZ9nCZ%@vT$m zm_Bcl-dP*9;ZfxMo-h$Ay? z0FXdRw3UV*r|3|Q&;!F3)-J@U(X%VCcKDH2F*yFDGNR2JF>82~MQLS_emOiC=%LUh z>`XwJ?}ozlep9dOBrX0m#r5^Ct!0sX#w<|j!C=)C`e$-rCZH-ts~8Tny*Lj=l1>st zK1?6YDmC%A8jtsg`UWQ@olLvi1V=5*>eU0Y5v#{q_+Q&j;-I|+eMn>&z4|aQaV5Tn z`%WDtyDlAF>bzHHw$W?RA9@o<5{oSuH4xNSJ?R6ng`-&Nr5M9b*wR~@NLgU)VeMSr z%%qW1;3g_WV>I7J0ys4`oKM(q;&*sG#5pzwPtUqkW8w9WhFYH!gm-Ycl(yj+Cj2WKV@~|MREcd{FfisdaG9Qal~4FQsC@Z5_IB}T&~2;m zBF^6YVtWKLim9j>R_F2%sCeqb->|#qgcwVO1Q*PxR}ry@!!TlLbCd_W>~;nS96~W$ zXhL1ofU)G*=gke>wue6T?R%9;l@&~%%!{;HR47V;W?dYidduh6l{^d?yRC`Jbviaw z4$0*(4ulR`(v}2+3w0?ct?mE*Yuz4A$bXx@%H(d_Tm6W|0W|?qHCVuPrRl(WFJK{A z*By%{3La@uukgQ<>)ZDE2~9CUsg}^+esxhPP{b8*&=)>p{ON5HPi!kEETsdz7{6-R zb@Mlk0(TwMTtuG|L2WYLyt_bL8N1*1$wJg60!FV(GIVzKMLJ@YMm z=HYurA2S0-h36nc5kcViJDp*eYP%}~cczDd$+C`id*sop!pxD5X<&aar%2W-FJn(9 z#v`Y|m-fS8i0(_18z{U#{Lle6}( z+P$z7OynkgM!&~9xFxo{5lysK=`3XpG zU$e0Q?-P3WJ#wSo2sxp(R%%k>pzT^$YSsdx!(2n%vYb;By00K=q-PfKcHBIdot%+J`D3g5oP{@w@taH!#wd=k zvTpP)+@%3;im44f6O4OHx1Bv*v3>^X#@!JkoV-GVAa*jotFDbxrCXz}ID;Gb zCi$NtF;O`rbAUl36JZ`Dst#Ap@p0(Zb}nvPXiz3N{SWLK3yWV=br9dnp?3dV+P|H~ zXYC*$V%PIFc4XD&LUlwQ=bn0Uh!svA3Uw}Kg+pguzjPxM5+X6TJBog&N%evd=*HSl ziy0KLcKytVUOrmRw;dN&IvmHI52)*CmbFj_Q9RBEaEUI6X)$z29w zZR-(SPb-D>bRfar)~_4y@0#@T^j(Q%W<&9x%x%Yjr!VDY;aY*;1q;dW(CV1J1(_ACqpfqAv>}F|ut=GrE=8O5d2B zU7=ROTa4wEfKQ^@^*BXzcs#tDs907daQS(_$Fu)G#6)*RI7dv-_E&;EDQtXvRx_0E znHq_hw>Ymb9NAI_f%O@_^Gg&;@qQTH9nXyb>g~01Dd8Ic#5FGcyij1Aab!!Q=LQZ} zj6H^7Z?qf&LScb)O#5dF000Bc0Txr5yfk`QTt6v@1p^B&*byz-c$(%k_OCuB6QW?pscbj=`MDC^uS&6gw-b} z14eidrW=?gU?H7C6*7s&lRsk8DA=O55rs83fGT<$9(4I&6#k#zyL7|w zA6Eti+%_CVs0@73x#+E_77+-tX~3~}g#5TweEv^>H#yp5A^$mKlW&dVm8&g8iJVTLE#09yivkOu6f* z9V_(sQ3C$6u2TrG0x~_1^E@@b#DCNR!UBfVlMyYXT`ow`PIf9X$hKhEj#WwK3B^S? zbhx!gFV`RDpN&9srB}rtQo0-ed9))lpkW9-VT?TlO^*#(xQEZe1L~sY#CE&$=ZDC( zal&PoG_g+bi1F2%Hxj$DN@+!nO;6t7CZ=&T3Ipg-e%oG~#5GtP2CT7f`^G(|4~mA$d%9O4ejtZs5ha zIKSnNU9bpw0ifz1phUc+!3^y#l-D>Eb>4BEpR_bh-mt_1Y)b-kc* ziNTAZi5l{Siflu#Xe#=uiX0U6Y24o25-cm90JR1nT3d@Je5j)zQxK_zvWZtBxxq*2 zE4VIw17*KdDcF3@)d6SIz}oVq54iH_dXHk{-Y1YWQXZp(5Dv>JCY;=Kb-Jb?sa%*w zWy@9P)gIfL5Ib08;ampjC2V+Nq6^7^`b3t&LW8_N`5wFf3GO0*Zd5ayb?4@qb0vup z?dtU>zcs**Usv1$pUaR<09s!)xgtGlGe}jg*$hQ#A}$BHBAf#^&7t5j*}WYL&I7X?sN*NNfSr&g9ENDaXXra=vj8^$|oF~+?@ zxx{=f$FDQwpg;ju!CZupyv5pg{nJml!VOnr$o}NpphmsE$D@+oaOx?i3X_Z%7&SuP zAw`-PulOd@{VGYFc(=?xAjx`WMuUG7;1{Gxd_fr)Rj#Ft+pdeGfNtO8@9vCW@~dZL z+_x$P>M@kzTbEDg%&6Gc!Qu8F@0QR&;sAe25Qkkd(Y@+zK0hl1Kje>%8<+Uv{sLjvUs3NH^@G-t+u!ypirlE(oiSIuP%3n`Q2l;h&`v%2n<**P|aD0?pB z3CjWd>;Qq~^DKoj36vv3^Og2g*vrL|-0JboT_;QB855j508p~`ZWh6yQo7f2|- zw$Y&R$l4f4jUS#2`=pIocMPcsHz5cR{m(93iJLdf5f9*zfj(p;04=Dz5Q^{&ou`TD zFWVR9^zih+0031QCvCD!BCj9fyCK`Mpfum5q;+2fs94YpPevKH^am9~olk!PggVR0 zFX9C{BAezwq-o-e3q5ELk+}ew)GDnHKeg0}x9s>c`!uAfPGL2+07BMEB(~>$o={-{ z6phy9dqc>s$X|u;?qW9VKVBZ4!2b9#e_{cF)EENse|d8$4nuBFj~g(4qQy|c&{|qz zPlv_p1=qrq5~XBYNKIX|tVz z*{Vue&G?Cw+0c|(8?ch)bT=VIIWLe)SoGCZ#QA;`tkfGlXys`+h3%_`vd<0 z891z8w@A)D1(W)MSR8-=CXD?uFQd!QGPcj9<)?$&(*H&U_%r(qDJ$2cZKd${5ENhB{D`ms%tkzG=r#!(AuPFunVrkiu<2V+{b;Z>a3lGUCc4!hmGKaJ@2BDI{&H;n zUI9+doPjQ1RSJi2@X3WVP+Y+g-sl2U2oZ)28;!)A;gLfNRHq;5wt~!z6n|xJ_cO{8 zD5O!INgWRqlel@Z8MCS4@(D>9NcPCSy-qzC&jr$C4kear+fOPMndx%ZZiqv4gxmzJvpklYqT01g@> z7aI%{RhXC`gjDvq2wlM#@i1Mk!hx{nXs7bUEdjWeF#%?-16fe^5OR-n%cyppB!n&2^bmq*G1c&P5IJr0JyOYSx6}v zGfUAyOohI;Zj&1$v**jj8!>si2rxML2*1I>aQPfDL42PRYbJzD zYWBJTI$R_@GJlNa5mC8Ju`{4GWs87xF|>UJv6c%9CV8S)PIQBe zqbH~PZsaqgn@p)C_SbXiK7dWy_TdZ%4dXRTI3^i_Tjy7mwhD zo7PK-ycrx96Gnu4kD@c9xtx*i+e!(~kvTO0ebPF>mG0X!94YhabLwe0YrktgFq%vV zBPeLqmgMjt?ZT@^>VF48i|JmQqq&UnVe~k12&Y;r<&pV9KeO=-wu@(E&3ONbD}WWI zeHwftqrs`fgjMtP>V2&eQDeaIyuAZo=a_c28u^K2M~6gXV8g-g?X_eUD z-N(Us71?PM?59j_H2GP({sAKkPX&En!C`e!7tMU2cdE>GW~ z!W0yrE9{_(NFoG9egpc%a*{?+;c7j1k2>hU*@nZu_x9xTdzk0t-9udF6fn8FNA zY|uC?BS353@hu1H$`VSvl&Eh!b8zpWvAEl)vzHbOzTOCAq~PgzICz*QgN_9+4T$gG zgzVyLJg?h9XhSx?7_8?rxd7+PiVHgM_Ie?GSWAEy&0kU}!=lx^*kg9u_x7NqALoU~ zUvtHldN}g!vMJ0XNfx63{)28 z!uzwSip=m59(ahSnS=9?s5btwW=T-#$H?t=HSoS>58LQ=6di5rRXK|JRxvqm4{#=#u#-d~W zP|@~({G8*cG8{@}sXxQ9xSGD-W8;At2xV}`|W0Gfic1xjeTPQl zqno^%a7|+`Wu9~JpirMN6@8TBP(Fgv_%kG1FnUF!3$zS&6~i!)JpQzJI_7s381i;T z5`!?`xWm|+kQu$z6YwJPuQU}hDOhxky}I9Z2-JrXCilyxVWa*hya=8phYGeC-^h@ynqN7RBjcs$$-5#n;Lw(fyq zA(;6rB9!97J3J(^?X=X`UF%9-9KU<1kfk{`lz6L%{u(_H#2@UMnNSgY z@?EUih@sqrkx}f;fEd=Xi7VXVvtu-3v={41KKhbBzyePycJBI?k#D${>aUUYucJ}Y z@)3m%*7L*@Mz87+klf(7(%9xk>Rd=~|wsq|mji^m^)U9h>tB z&$rFHrMRxIUV!Ey!i%e5$P{v05R)|S36K_oCX8@iDRi$FdK!yTN4NyQ$vZ}8-PzA~ z!uiLeW)vO6;0Nq!)$)F5C{gi}3!>i0Jq4xkW<fp8d z&zaQmz(?-w3B@tfY0us4j(ek}f-`&Gco=mVurp#Y_pfrjdK4%%*n$_rWr<9`K#fpH z1A?nd*>-M4X}TVd05iidy(9;uwn`UDBi6%(n0Pp{lfQ6o_sF8=6y@B21Mh``PgoFz z6eh|(nvbufyW5(669C#Z`)u2X0$dB6TD&N$aR-Q)6v`rVB)Kl21k%{8gVF{18`~>! z`W2NCH4SS?AX8?BYZ;Rjjfyb?dX|#Jz(=LXtUie|gw4kAU(uW(G9=bhZKP>I$iwla zi;2sUWt05PdIyhbwpl_*(!QVrXTNl)%jg7u7`$-PRJ6F(!o?M>PcF~3QKtwRrWQI6 z;p#He8EW2;W?!vIg+whodU-EO3k+lS$ z?R+=)SbJIrc5N(Pz;J-^VmQF;vh|F}kqd{)n*%o~J*bAsZkq)Z(xrnZLV0)2mZ+Je zKq^`)#Fb%@j8U@5^(l^5$?t@x{N0PY#7YW}goC`9$gkb~MB8s#ia8C9!vM!6<1a{n z;R0V9&dDM`Y!coyc(F65cvyceRIr45VvC!NL`5yrZt#5ReK&5x88`H~G*Fd2cyF>p z(v^P}PuhF$gY1;8BmKe$d!a3Q40A@WvoMoe9(`7g$8d)5t{@L#xT)$m?*Qbxf~XbSOtEbMHn1GOM_>+!<7OgE^?%kt z--NEVDQk3TyrbS9V%D|H`?02mQo(Y0rNN06fH>zo-ISCFq%0__)_}g!tZ(?xK=f86dj#`mnvbrV=t>GYIZ5WHV0( z2|isk3XH<7BmThCkOH#!fzqHMx?hHz+a|>RMZV*z1+3*VD=rEUPaId=Z1FtjF@X{# zWdt=1Y?pQ0&-j}D^hTZjV~>w_77UXY))q={XnroiXthS5%kH%n-D@wv8=6$mD7BRI zyn}JXUQnnJ`RcymUbCYw;E3raa1}@~K1WzkiWj{-S(Ah3QjRtI($??`AS}c4aFA={ z#rXW33eV>A0x}x*-~bv3OjY%=IVBe=YL1Cx`pGf*^@~6RA+S>2wdG2^!0fouy8ZyL zp+?U!>U&Fo`S7g5iSPhD2kmd^s~A>VILQ4BCS>X-wniS35g>~)8}Yw$cEOb?FX#en zJogo$ric9tns^(yu=HaF@jYZfZt-=Q(2Y` zF;(2E%MJ#~7hnq(xu@wZ=%3*s6Y4T#O7h@qz?;}*9=^>+fYsaDL`dA=460sKtX**M zg$`8L&&P+1oHqcN0Al{LP+(Fg=K<`lMkh#X0t|4e&LHhRIB)Mb&dWOLTQyx%1W!haM5 z7?bkZ1hycPOm$`%=;k~W_S+VS0dhkk0Q^X|JAhZuGa}AfS^-_NECzudUAh$#iWj9q}GkL5ye>xG_XkQ1w)d5!Cx|57Z_$TF|hOo)Gcg5JB?%x z60#PR)!;frLk)2q8|;^x&yIdZ&`#&%$ITD3p30P6W-nm9*gF3tp&>GXfR9h0ys1&c zyNcxMoy9o2UwvK%uYZ-{Jfm`}m{39NL?hiIrT_Gzm%>@f4nWrit{r%{fCuH0!AP8Z z$ICg%Hnu8*syd;&k*iP z(Y0&HE!pFp2de|1*w_cKDp-1FU8MKD;$re6S=T@(U@A(PtE4WEu zO2RKj9`lRXKmsSFk~A~DSJocSA~J9SAuKFAkf9%QO#FTc4vqL-K;fdHr>tM^0}De8 zAb^{Bv(`cSo1Xmq%?0(%bYh1^O8!0joJ9m{-JGrS!&q4j_4psbXV2Ko@s5IkizLWA zu+TM^3tf7<&Ax5K9)YKc|I>s%;HWZhlTt&t=u4lPC-8{svouPh#tMJ6YP)bl(bT`Ds_$~Urwgr3u zQ8%Sn)n9C75Eh3lwLDmTD>T4=3f|&~rX}v@+A1m7AOw^BYe>%>cEz?(SUK@KPOfp! zc_bg{@1qjg88mp*SJ`jFez^9Jgr*xvsHIeGYv3Y-$8`Fd!+%ipQyvDWdzk+V*Cs7P*#6;T-`8fZV>^TQmjkr zJ(G-{Vyn192!Q~Ixj>P*V0?DS2rula9L$JCSgH0#@6xaX;s79&=(nv12@XRec||J)*sMk z?SSEDqB`#W(31oD^i`k8@KoZ8NV{I-r2(G#jei!_(nEPY9Wd|N^Q{2R%#=R^GA(2l zU0)^-vOn_xM^nEJM=vrYrbA9@K`SqZFvs#4Rj+(V| z*CSBpzeq{4FB}Gocr`Fg|MGIMjA~*S)@8MlFVLF4q>o)u7i*AafmLL(tqucS*)imy z9-TGkIMaL^|9RJ6&)aamM7%HoA8ZL6ih+04-N$RyIu#*S3LU+AVoWn=#BH=q}_Js`RWYc2y1XNdJp$tJME`*~|`JXR=k>?8& zhj)?_$cE65ty9XP_^x7tT(|rK9)Aq94qSunyF93K$lY|V+W~npXCo=UT#nOB2i;m~ z$Phk_-)O7eVoa{wwrEzxjaSBSz8BaxnT z0I})7j|g$3`#z!rMgOmjX$BQ1hwlnkQRR-n-m~kU&(V^y<7d4^f|FOv!#(6RfqQ9{ zHdQx(aXPb(gD{I8zC?i@ffJV8*k^^pu5uu*Xc)60Mt4Cg36`Qr?2#fnVm_eTI_fof zqapN?0vd_%?wpC$Ks5BG3zn*E6&kG=-G;0GJ3WY8Rbjcs|Jt%6+fyHus(C6(R!OWb zOz!8%W9X`DFT79dHm^EcZVQ-G_moB|HJ3InVbg38iUt|C4Tee)l+zjgfF{_pdP_pkQ#M~ zGB>usFng3M(gZ=OFqvu2a|F(bK5B}cgKeNC7#ufZN*Vub; zb@PPUh(Y?=YWSX9oKBpa>f7bn1`1~5Jhza$ki) zpJiP&W0tE!4*+?;xOQL6E&SSOW{#3p7I{gdssl2jpFstn%)?*c4>$J?%=#GF#<2W9 zHs6Z;Jxp8D!xOPg8v}*oLJq5o<6^Wh@ap^H5MqMp{qg;&6%ypXC4~(AwWxOL2f|B% zJTH2K0Mt(ZP}e$rz1?Nka>|4Z!K%>QUf zofFt~J~R+^z!k{!$r9n_0}u5eU!A4MUEfgIJ-jS-CGr?}1I_)z!T8Jd6!4H_oLAvc zHD-01uIP!h^;YghZ9azkGO#o6bk9llzrq2yp;|lv@J18E2WxUa}O)-T?D|aP1Gt#R?keJH~=rrRYYHWHlsSJTnvT?R~2^Qi*ReVfNg{HIRc|#;(|{ zm4b9BKYQjZ#i?HpSb$rHH#+_yj$zEFpt+x_TH&dKz|Ep9J9X6YTPP?*H>)iU-Tbi_ zyxRlqPiCT_KT%AxmOtei?T_GI2u(LZW-Aq7i*o=EI6(>Ibih+~K)AT`1v(jmb=>c+ zJE}#b{v@&LCEL8*nfAaCS`5-d4*+?;xOQjI!@wSGfB^pB0g&hSh_os5WvA+@!Ly-L z3fVTLXua+|)6qDa36e2}{X8P81Xs7}Bln-x%kVKQm!aF>x3~quU2hy$Qd(qU&5zTv zvC12NuGELcz}CQCREaweK#i(`V;S6aKT5bQWMP3oOt>a@ax<%cB`DJ2T_n&_$390kPZ=Sp?8M`PoDnM#c9= z^A$;ASGRLtWR7??)8Wm8*p_Xbyz^F5CF1*FBO@o7czH&D_wG|S=trYe(K5%Ed4%>bW@Vo|aI#U9j7107%U~ z3f^ckeOsf?kiYFyf%>Z@d!myo1@27$)}}FIOWCH=@a5b#h4x`*&(L76{m;K^*gF*e zPk{QrS>+jK%28bWwg3SUC;`#HtHptpJ}$8e9nck*56Vsz+Uv}T#kXld5~A!eG=Lnx zn&NK6-HE#sb|&mi*qgC8Vs6CU8OjHD6{egmIn2B^c|G#QVRC-6w@5d>hV2E$qzIB& z)fCXdyO(P?Fn##$31t+8qlGK?xc!$Jzwr4P>wuDSRM!f}Fzf+?>c-my-msggcmHGw zR`_IsW@v7OViubz&iA3f>WP0``^;D8@;hMB4+)<#N{FmkOXK`VF*qPB7m`K6#$m78 zEX9*c{0o4XO@y^KhZr{?fh*gn5t4J&*6oVD;WJO7`t^SJuWp?@?|zW{yI6e@!36MedN_d6BLS&`VMX z-65}I(2663QLwyQou`%G)rOm1--^>lso5L_pXXT%|men1I>58PM#tb0^yK}buC?Ah^C2Ky)+cBfF!?UX=y2R*Vcpobd_LhHb zW?I<_0w9K(@@LgXecAq~!Zb-Vx_?45`h~DTgnAt}*)l~CxnPDw`{#oN6z&6PX(5!) zCP3Wj=6^dF{beoyN+P9S7|yyHQe#zee$2~&v(!BhDf+T_pSx%c6{$yNNsq@l z)68lbiU6RAl@fy@B&}>+^=f41r&7wB<>k}u=MF_I(-161Qw(x^00>*4t-;J^9?ZSB zl#nIqp$m^n=jC9CNuA-G(%_O)MDkoPg_DBAJorRMMD`{O66iK}K<;$5UjZbXZ~!-U zMRi5K5Ggb?liX|Zu$D=204^9HdwpECHbjJmMR}%e7x$f^i-M*(?%L_Nq-#up_Y+C00y6to4uJ~}E zrBxPe*IGM0H1zFY&lIMr*T45xvHi4KNg8F>@T1mrra_eD#cMi4C|vC2o%E34J=5>S z1^fl5Rl{t~!@d1BO;6k@3QCq=j8%JU zDn9rI>IauTMHa(Y^uPjhy1XxGhIzO4B>@ASf!;DIi;2g*ZnuV`jbL&YIf+Y~Bujj* zx?gfwZMoX~v>SAo&x;t3V2TzBgi_=RF1B4I{W-UYyl-k6X?uh1Mz(FQ%W1kW8>N2C z&weP#8m)>IJZv=$2J>4@tD$xiNDxQW(f-4)UiR%IxXraHMFs@`R$NKIc@Vzx&T@fJ>XO&jt1lXm?Y}3!e54f{* zTaw{gPaph+(u7hK4Kc5Pe!hPQ5Kd);2ol4r@j4dXiI=iE{soM!OBFP)Cl`The0LlD z@g-8M`+o)BX_?E(m!kwIpMUjF+lk-mnim14y?-i+bst>zGd{aMhx)*T#vJuS$MR=b z&Z5~Zp z(o_{5-F-%~7e|3hEa&;rbT1Su^sC!ipx>Vj`t+$=xB{-6V9O#ZS*?B5^9#SBXE9YKSsKG9 z6I9$DCmzrx{c)j7qgozAr;KV*+=KDc&9L3RUUoD3Z^O?)aCunk>g?IvSTCPsXA%3` z3);uf=UhbCS4z8%EkjR#cjpt?)H!ApF(Br2|DfRrac{aVzhXv z)14Eox2H{wYSfUaYfj;Z8i-JHgs>b&C`@pJdJ^&zK||4h*o?&I>xLbp43<>5P#$5h zw6?#4aqGOy)UsGiI^u9w5P$`Zep^O#HcSe+l_~_~h1S*VxkI{QvJ>ZTFQ&dJNrJ6r zud({Jh0dy8f*PD1HZfTO$qqRw6ZBSYC{LW)M3PzmxNLmU^H#TS9V+2v+USs9y#<`1 zR8#(y>v+sLV{JPlTOp*s1OH(!nK5B3U!7TCOjPGQG&GHYMR{pi47KTL1ZuMMGn#nh zvOQ@faH&;2gtrogpZIN>RDf!T`1wjr2KtG!$p4G@ZWhz(1ESIEcyHkB&h7^v&O#%# zhVrM0Uua1Yav-J@v8WEkK|)#tDJ2^)Lb#$=nY6$Z3~k$DQF9sx2Q4C7{)?ex0BBjuc_CUUqpaeBZ zKCQ4BK5)*1PFWO|TTA~VmJqiDpKn^bIG9T&Ekpzn72eZEgF1ei^pgtkUkIc>Omj4c zw-U|PE?XDl;5~^38_7EdoxY(toiZ*?06tbLxFKJfiGp#&TjeN}zwS}xj?3Ke;U7P( z?`JX@Q$Ggn3X2n=^m<4yW+R$0xHz8E=)&jzY`_+Kf%hPcKlqEl{f2e!LpF z)X-$`Q#|`H{scRsiYwKg{|R4X3pzAAMnQqmBz^m<2i6Oec4zQJU{Tpn6K3Cd+r3lZ zaf>-m9j_X%lXTQa_CrG7pDhCbPNw z6@wKctWA<~;T|q%e z*724?Lt9{X69qmt!gNj6?8~+haM97IBsL{2lav;|7(dt_PRU{k*LK`9P!QYSt@VzCB_nPhF3`p2diebw4`~# zFkOy!490ZSFh@~XheCi;CA)*UYH`K)J0qRp+x*)NJUrN+z)%c(fFe6w3$-xJ>DCp> z)v>IuZkGNT)5lIm8guvFMrgS6Hg7hX$1=Wu<~zRff6r81P7&XZ5Jy_&|CA0=9_gqw zgw2woJcWTOXrV5*T0lY4!oRiZ1yvxJ1%3*o=YDL!}Sjovqo>Z(={oP zB~>m=^w2~@?;c@lD44VCA=R^&BNlgtC;J{g?fuA?Xe(enGEcYXu-pv8If5T6IM@58 zBf`_z##z59*RXDF(YC>1bW=6+|JeH9TtDNaVPxvTTG?v3+b#K|UVudi8}j0q4qnzW%q%|(&}`ute^d?>3dz;JeZ z_w)mACUk2h-C3y(zm+-lPzbEurW%=NHyrcKXjx5LU)AW?N#f#Lqa`ilS&db*_f#6T z?<^))hf#|5;%WEv>dKKf6!?z~dFdGUuacTKqVg9sgF32ahwnrcFqO@=!`@ld7em5{ z?@4O-$1u&OvIP$w`z`cDOuDQ~-&YIeY_oaQphTO)e6h$a8@81E5&>k#pbn`~MrqUN85Z*j!oGri0FYWxB z6CrYKd^`M$&&vJ@jr3l^=0mK}KT~1ILAj0C#~2e0pQ#FHqO-cL=a6R49-Ewk+v|7P zjWnk*Mf^-;=To<8%35VB^%^7MqSVpI{jDm8DdH?21?NA9z|XRp z#`kx_jB&e6;ha)}o;}6-O{4mgX<1&TsVwP~_BweEYY7`|a`QR7z>3>FV1zTF#m9Lb zJ_Vxe`2c_YH|BcYr(EJp((_T2Nhs&xi#R?!gP}>bHa&r>g)N)Y&J3bHIS>Y>;>LL$ z5?2#X32aK1(x#o(!!J5z%^A@(=Rx?5i$kwmD1d&?&HkATDuf(uVcIQFyX6Axscc(- z_ubE+l+6DJHUBbe$GoD_lm9ECT7m=8SXK$}bX$u;x>jzAtV9qZiSt5v4qrox zq-nw^H1RF!#}|q&_@5G7$d@Djchrhm>Sxw3{#LtVA^4 z`G)L`am&k-AfH__>r*tS#r$(BS+mg(GJTaGow{>r5N>RI@xtJJ!m}}U^knzX0AI=& zWao>_%wg}J_g{FF9UL{BQjgEMw4~7y8UENC!T%$B)5NG)FYTZ_&39vZ|GjHs-7~2cU+)gNd+=MIWFrbSKmZWTLPbDEkn5^{5h~aV4l}+r_{lEsF%u1g9yjr( uPfj%^Vo!OFur +Discord Rich Presence showing currently playing track with album art, artist, and playback progress ## Installation @@ -115,6 +115,8 @@ For album artwork to display in Discord, Discord needs to be able to access the Access the plugin configuration in Navidrome: **Settings > Plugins > Discord Rich Presence** +Plugin configuration panel showing all available settings + ### Configuration Fields #### Client ID From 323bf7089ab0bb6c9833bcb1f23535e3ab990ea2 Mon Sep 17 00:00:00 2001 From: deluan Date: Fri, 20 Mar 2026 21:32:25 -0400 Subject: [PATCH 4/7] ci: update release workflow to use beta versioning instead of prerelease --- .github/workflows/create-release.yml | 15 ++++++++------- Makefile | 6 +++--- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index 9f438a5..318e776 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -7,11 +7,11 @@ on: description: "Release version (e.g., 1.2.3, without the 'v' prefix)" required: true type: string - prerelease: - description: "Mark this as a pre-release" + beta: + description: "Beta number (1, 2, 3...). Leave empty for stable release" required: false - type: boolean - default: false + type: string + default: "" permissions: contents: write @@ -33,8 +33,9 @@ jobs: - name: Compute full version run: | VERSION="${{ inputs.version }}" - if [[ "${{ inputs.prerelease }}" == "true" ]]; then - VERSION="${VERSION}-prerelease" + BETA="${{ inputs.beta }}" + if [[ -n "$BETA" && "$BETA" != "0" ]]; then + VERSION="${VERSION}-beta-${BETA}" fi echo "VERSION=${VERSION}" >> "$GITHUB_ENV" @@ -83,6 +84,6 @@ jobs: with: tag_name: v${{ env.VERSION }} draft: true - prerelease: ${{ inputs.prerelease }} + prerelease: ${{ inputs.beta != '' && inputs.beta != '0' }} files: discord-rich-presence.ndp generate_release_notes: true diff --git a/Makefile b/Makefile index 3781b0e..8a0cd7b 100644 --- a/Makefile +++ b/Makefile @@ -23,7 +23,7 @@ clean: rm -f $(WASM_FILE) $(PLUGIN_NAME).ndp release: - @if [[ ! "${V}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$$ ]]; then echo "Usage: make release V=X.X.X [PRE=true]"; exit 1; fi - gh workflow run create-release.yml -f version=${V} -f prerelease=$(if $(filter true,$(PRE)),true,false) - @echo "Release v${V}$(if $(filter true,$(PRE)),-prerelease,) workflow triggered. Check progress: gh run list --workflow=create-release.yml" + @if [[ ! "$${V}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$$ ]]; then echo "Usage: make release V=X.X.X [BETA=N]"; exit 1; fi + gh workflow run create-release.yml -f version=$${V} -f beta=$(BETA) + @echo "Release v$${V}$$(if [ -n "$(BETA)" ] && [ "$(BETA)" != "0" ]; then echo -beta-$(BETA); fi) workflow triggered. Check progress: gh run list --workflow=create-release.yml" .PHONY: release From 47b444d72a66a537b56d09e34b7ade44a9dfc215 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 21 Mar 2026 01:35:12 +0000 Subject: [PATCH 5/7] Release v1.0.0-beta-1 --- manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manifest.json b/manifest.json index a0a053f..a41461d 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "$schema": "https://raw.githubusercontent.com/navidrome/navidrome/refs/heads/master/plugins/manifest-schema.json", "name": "Discord Rich Presence", "author": "Navidrome Team", - "version": "1.0.0-prerelease", + "version": "1.0.0-beta-1", "description": "Discord Rich Presence integration for Navidrome", "website": "https://github.com/navidrome/discord-rich-presence-plugin", "permissions": { From e0f336105136fc941aca027fa3e0852daa42e22f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 31 Mar 2026 23:25:33 +0000 Subject: [PATCH 6/7] Release v1.0.0 --- manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manifest.json b/manifest.json index a41461d..c3473df 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "$schema": "https://raw.githubusercontent.com/navidrome/navidrome/refs/heads/master/plugins/manifest-schema.json", "name": "Discord Rich Presence", "author": "Navidrome Team", - "version": "1.0.0-beta-1", + "version": "1.0.0", "description": "Discord Rich Presence integration for Navidrome", "website": "https://github.com/navidrome/discord-rich-presence-plugin", "permissions": { From 4e0f98aa51f447da0af474ba6ccb5b0167662f17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deluan=20Quint=C3=A3o?= Date: Tue, 31 Mar 2026 19:34:06 -0400 Subject: [PATCH 7/7] Update Navidrome version requirement to 0.61.0 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d1e6c6d..fe6a314 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Build](https://github.com/navidrome/discord-rich-presence-plugin/actions/workflows/build.yml/badge.svg)](https://github.com/navidrome/discord-rich-presence-plugin/actions/workflows/build.yml) [![Latest](https://img.shields.io/github/v/release/navidrome/discord-rich-presence-plugin)](https://github.com/navidrome/discord-rich-presence-plugin/releases/latest/download/discord-rich-presence.ndp) -**Attention: This plugin requires Navidrome 0.60.2 or later.** +**Attention: This plugin requires Navidrome 0.61.0 or later.** This plugin integrates Navidrome with Discord Rich Presence, displaying your currently playing track in your Discord status. The goal is to demonstrate the capabilities of Navidrome's plugin system by implementing a real-time presence feature using Discord's Gateway API.