feat: add Spotify link-through option and remove option to disable ND logo overlay

This commit is contained in:
deluan
2026-02-23 21:03:34 -05:00
parent 3d7d131b9f
commit 480a8a18d7
3 changed files with 25 additions and 26 deletions
+13 -14
View File
@@ -25,10 +25,10 @@ import (
// Configuration keys // Configuration keys
const ( const (
clientIDKey = "clientid" clientIDKey = "clientid"
usersKey = "users" usersKey = "users"
activityNameKey = "activityname" activityNameKey = "activityname"
navLogoOverlayKey = "navlogooverlay" spotifyLinksKey = "spotifylinks"
) )
// navidromeLogoURL is the small overlay image shown in the bottom-right of the album art. // navidromeLogoURL is the small overlay image shown in the bottom-right of the album art.
@@ -162,16 +162,15 @@ func (p *discordPlugin) NowPlaying(input scrobbler.NowPlayingRequest) error {
activityName = input.Track.Artist activityName = input.Track.Artist
} }
// Navidrome logo overlay: shown by default; disabled only when explicitly set to "false" // Resolve Spotify URLs if enabled
navLogoOption, _ := pdk.GetConfig(navLogoOverlayKey) var spotifyURL, artistSearchURL string
smallImage, smallText := "", "" spotifyLinksOption, _ := pdk.GetConfig(spotifyLinksKey)
if navLogoOption != "false" { if spotifyLinksOption == "true" {
smallImage = navidromeLogoURL spotifyURL = resolveSpotifyURL(input.Track)
smallText = "Navidrome" artistSearchURL = spotifySearch(input.Track.Artist)
} }
// Send activity update // Send activity update
spotifyURL := resolveSpotifyURL(input.Track)
if err := rpc.sendActivity(clientID, input.Username, userToken, activity{ if err := rpc.sendActivity(clientID, input.Username, userToken, activity{
Application: clientID, Application: clientID,
Name: activityName, Name: activityName,
@@ -179,7 +178,7 @@ func (p *discordPlugin) NowPlaying(input scrobbler.NowPlayingRequest) error {
Details: input.Track.Title, Details: input.Track.Title,
DetailsURL: spotifyURL, DetailsURL: spotifyURL,
State: input.Track.Artist, State: input.Track.Artist,
StateURL: spotifySearch(input.Track.Artist), StateURL: artistSearchURL,
StatusDisplayType: 2, StatusDisplayType: 2,
Timestamps: activityTimestamps{ Timestamps: activityTimestamps{
Start: startTime, Start: startTime,
@@ -189,8 +188,8 @@ func (p *discordPlugin) NowPlaying(input scrobbler.NowPlayingRequest) error {
LargeImage: getImageURL(input.Username, input.Track.ID), LargeImage: getImageURL(input.Username, input.Track.ID),
LargeText: input.Track.Album, LargeText: input.Track.Album,
LargeURL: spotifyURL, LargeURL: spotifyURL,
SmallImage: smallImage, SmallImage: navidromeLogoURL,
SmallText: smallText, SmallText: "Navidrome",
}, },
}); err != nil { }); err != nil {
return fmt.Errorf("%w: failed to send activity: %v", scrobbler.ScrobblerErrorRetryLater, err) return fmt.Errorf("%w: failed to send activity: %v", scrobbler.ScrobblerErrorRetryLater, err)
+7 -7
View File
@@ -121,7 +121,7 @@ var _ = Describe("discordPlugin", func() {
pdk.PDKMock.On("GetConfig", usersKey).Return(`[{"username":"testuser","token":"test-token"}]`, true) pdk.PDKMock.On("GetConfig", usersKey).Return(`[{"username":"testuser","token":"test-token"}]`, true)
pdk.PDKMock.On("GetConfig", uguuEnabledKey).Return("", false) pdk.PDKMock.On("GetConfig", uguuEnabledKey).Return("", false)
pdk.PDKMock.On("GetConfig", activityNameKey).Return("", false) pdk.PDKMock.On("GetConfig", activityNameKey).Return("", false)
pdk.PDKMock.On("GetConfig", navLogoOverlayKey).Return("", false) pdk.PDKMock.On("GetConfig", spotifyLinksKey).Return("", false)
// Connect mocks (isConnected check via heartbeat) // Connect mocks (isConnected check via heartbeat)
host.CacheMock.On("GetInt", "discord.seq.testuser").Return(int64(0), false, errors.New("not found")) host.CacheMock.On("GetInt", "discord.seq.testuser").Return(int64(0), false, errors.New("not found"))
@@ -142,15 +142,15 @@ var _ = Describe("discordPlugin", func() {
// Cancel existing clear schedule (may or may not exist) // Cancel existing clear schedule (may or may not exist)
host.SchedulerMock.On("CancelSchedule", "testuser-clear").Return(nil) host.SchedulerMock.On("CancelSchedule", "testuser-clear").Return(nil)
// Cache mocks (Spotify URL resolution + Discord image processing) // Cache mocks (Discord image processing)
host.CacheMock.On("GetString", mock.Anything).Return("", false, nil) host.CacheMock.On("GetString", mock.Anything).Return("", false, nil)
host.CacheMock.On("SetString", mock.Anything, mock.Anything, mock.Anything).Return(nil) host.CacheMock.On("SetString", mock.Anything, mock.Anything, mock.Anything).Return(nil)
host.ArtworkMock.On("GetTrackUrl", "track1", int32(300)).Return("https://example.com/art.jpg", nil) host.ArtworkMock.On("GetTrackUrl", "track1", int32(300)).Return("https://example.com/art.jpg", nil)
// Mock HTTP POST requests (ListenBrainz + Discord external assets API) // Mock HTTP POST requests (Discord external assets API)
postReq := &pdk.HTTPRequest{} postReq := &pdk.HTTPRequest{}
pdk.PDKMock.On("NewHTTPRequest", pdk.MethodPost, mock.Anything).Return(postReq) pdk.PDKMock.On("NewHTTPRequest", pdk.MethodPost, mock.Anything).Return(postReq)
pdk.PDKMock.On("Send", postReq).Return(pdk.NewStubHTTPResponse(200, nil, []byte(`[{"spotify_track_ids":[]}]`))) pdk.PDKMock.On("Send", postReq).Return(pdk.NewStubHTTPResponse(200, nil, []byte(`{}`)))
// Schedule clear activity callback // Schedule clear activity callback
host.SchedulerMock.On("ScheduleOneTime", mock.Anything, payloadClearActivity, "testuser-clear").Return("testuser-clear", nil) host.SchedulerMock.On("ScheduleOneTime", mock.Anything, payloadClearActivity, "testuser-clear").Return("testuser-clear", nil)
@@ -175,7 +175,7 @@ var _ = Describe("discordPlugin", func() {
pdk.PDKMock.On("GetConfig", usersKey).Return(`[{"username":"testuser","token":"test-token"}]`, true) pdk.PDKMock.On("GetConfig", usersKey).Return(`[{"username":"testuser","token":"test-token"}]`, true)
pdk.PDKMock.On("GetConfig", uguuEnabledKey).Return("", false) pdk.PDKMock.On("GetConfig", uguuEnabledKey).Return("", false)
pdk.PDKMock.On("GetConfig", activityNameKey).Return(configValue, configExists) pdk.PDKMock.On("GetConfig", activityNameKey).Return(configValue, configExists)
pdk.PDKMock.On("GetConfig", navLogoOverlayKey).Return("", false) pdk.PDKMock.On("GetConfig", spotifyLinksKey).Return("", false)
// Connect mocks // Connect mocks
host.CacheMock.On("GetInt", "discord.seq.testuser").Return(int64(0), false, errors.New("not found")) host.CacheMock.On("GetInt", "discord.seq.testuser").Return(int64(0), false, errors.New("not found"))
@@ -195,13 +195,13 @@ var _ = Describe("discordPlugin", func() {
host.SchedulerMock.On("ScheduleRecurring", mock.Anything, payloadHeartbeat, "testuser").Return("testuser", nil) host.SchedulerMock.On("ScheduleRecurring", mock.Anything, payloadHeartbeat, "testuser").Return("testuser", nil)
host.SchedulerMock.On("CancelSchedule", "testuser-clear").Return(nil) host.SchedulerMock.On("CancelSchedule", "testuser-clear").Return(nil)
// Cache mocks (Spotify URL resolution + Discord image processing) // Cache mocks (Discord image processing)
host.CacheMock.On("GetString", mock.Anything).Return("", false, nil) host.CacheMock.On("GetString", mock.Anything).Return("", false, nil)
host.CacheMock.On("SetString", mock.Anything, mock.Anything, mock.Anything).Return(nil) host.CacheMock.On("SetString", mock.Anything, mock.Anything, mock.Anything).Return(nil)
host.ArtworkMock.On("GetTrackUrl", "track1", int32(300)).Return("https://example.com/art.jpg", nil) host.ArtworkMock.On("GetTrackUrl", "track1", int32(300)).Return("https://example.com/art.jpg", nil)
postReq := &pdk.HTTPRequest{} postReq := &pdk.HTTPRequest{}
pdk.PDKMock.On("NewHTTPRequest", pdk.MethodPost, mock.Anything).Return(postReq) pdk.PDKMock.On("NewHTTPRequest", pdk.MethodPost, mock.Anything).Return(postReq)
pdk.PDKMock.On("Send", postReq).Return(pdk.NewStubHTTPResponse(200, nil, []byte(`[{"spotify_track_ids":[]}]`))) pdk.PDKMock.On("Send", postReq).Return(pdk.NewStubHTTPResponse(200, nil, []byte(`{}`)))
host.SchedulerMock.On("ScheduleOneTime", mock.Anything, payloadClearActivity, "testuser-clear").Return("testuser-clear", nil) host.SchedulerMock.On("ScheduleOneTime", mock.Anything, payloadClearActivity, "testuser-clear").Return("testuser-clear", nil)
err := plugin.NowPlaying(scrobbler.NowPlayingRequest{ err := plugin.NowPlaying(scrobbler.NowPlayingRequest{
+5 -5
View File
@@ -65,11 +65,11 @@
"title": "Upload artwork to uguu.se (enable if Navidrome is not publicly accessible)", "title": "Upload artwork to uguu.se (enable if Navidrome is not publicly accessible)",
"default": false "default": false
}, },
"navlogooverlay": { "spotifylinks": {
"type": "boolean", "type": "boolean",
"title": "Show Navidrome logo overlay on album art", "title": "Enable Spotify link-through",
"description": "Displays the Navidrome logo as a small overlay in the corner of the album artwork in Discord", "description": "When enabled, clicking the track title or album art in Discord opens the corresponding Spotify page",
"default": true "default": false
}, },
"users": { "users": {
"type": "array", "type": "array",
@@ -124,7 +124,7 @@
}, },
{ {
"type": "Control", "type": "Control",
"scope": "#/properties/navlogooverlay" "scope": "#/properties/spotifylinks"
}, },
{ {
"type": "Control", "type": "Control",