From 6b5ca1a54f065d4562140772fadf44b425a9b293 Mon Sep 17 00:00:00 2001 From: Daniel Stefani <46500918+OpenSrcerer@users.noreply.github.com> Date: Sun, 8 Feb 2026 18:50:20 +0100 Subject: [PATCH] feat: add configuration option to select Activity Name based on currently playing track (#11) --- README.md | 11 ++++++++++ main.go | 27 ++++++++++++++++++++--- main_test.go | 61 +++++++++++++++++++++++++++++++++++++++++++++++++++ manifest.json | 14 ++++++++++++ 4 files changed, 110 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 03f1720..454793d 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ Based on the [Navicord](https://github.com/logixism/navicord) project. ## Features - Shows currently playing track with title, artist, and album art +- Customizable activity name: "Navidrome" is default, but can be configured to display track title, artist, or album - Displays playback progress with start/end timestamps - Automatic presence clearing when track finishes - Multi-user support with individual Discord tokens @@ -163,6 +164,16 @@ Access the plugin configuration in Navidrome: **Settings > Plugins > Discord Ric 3. Copy the "Application ID" from the General Information page - **Example**: `1234567890123456789` +#### Activity Name Display +- **What it is**: Choose what information to display as the activity name in Discord Rich Presence +- **Options**: + - **Default**: Shows "Navidrome" (static app name) + - **Track**: Shows the currently playing track title + - **Album**: Shows the currently playing track's album name + - **Artist**: Shows the currently playing track's artist name +- **Default**: "Default" +- **Use case**: Choose "Track" or "Artist" for more dynamic, music-focused presence that changes with each song + #### 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/main.go b/main.go index 4813ace..be5a81e 100644 --- a/main.go +++ b/main.go @@ -25,8 +25,17 @@ import ( // Configuration keys const ( - clientIDKey = "clientid" - usersKey = "users" + clientIDKey = "clientid" + usersKey = "users" + activityNameKey = "activityname" +) + +// Activity name display options +const ( + activityNameDefault = "Default" + activityNameTrack = "Track" + activityNameArtist = "Artist" + activityNameAlbum = "Album" ) // userToken represents a user-token mapping from the config @@ -136,10 +145,22 @@ func (p *discordPlugin) NowPlaying(input scrobbler.NowPlayingRequest) error { startTime := (now - int64(input.Position)) * 1000 endTime := startTime + int64(input.Track.Duration)*1000 + // Resolve the activity name based on configuration + activityName := "Navidrome" + activityNameOption, _ := pdk.GetConfig(activityNameKey) + switch activityNameOption { + case activityNameTrack: + activityName = input.Track.Title + case activityNameAlbum: + activityName = input.Track.Album + case activityNameArtist: + activityName = input.Track.Artist + } + // Send activity update if err := rpc.sendActivity(clientID, input.Username, userToken, activity{ Application: clientID, - Name: "Navidrome", + Name: activityName, Type: 2, // Listening Details: input.Track.Title, State: input.Track.Artist, diff --git a/main_test.go b/main_test.go index dfe944b..cd9aa90 100644 --- a/main_test.go +++ b/main_test.go @@ -2,6 +2,7 @@ package main import ( "errors" + "fmt" "strings" "github.com/navidrome/navidrome/plugins/pdk/go/host" @@ -119,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", activityNameKey).Return("", false) // Connect mocks (isConnected check via heartbeat) host.CacheMock.On("GetInt", "discord.seq.testuser").Return(int64(0), false, errors.New("not found")) @@ -169,6 +171,65 @@ var _ = Describe("discordPlugin", func() { }) Expect(err).ToNot(HaveOccurred()) }) + + DescribeTable("activity name configuration", + func(configValue string, configExists bool, expectedName string) { + 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", activityNameKey).Return(configValue, configExists) + + // Connect mocks + host.CacheMock.On("GetInt", "discord.seq.testuser").Return(int64(0), false, errors.New("not found")) + gatewayResp := []byte(`{"url":"wss://gateway.discord.gg"}`) + gatewayReq := &pdk.HTTPRequest{} + pdk.PDKMock.On("NewHTTPRequest", pdk.MethodGet, "https://discord.com/api/gateway").Return(gatewayReq).Once() + pdk.PDKMock.On("Send", gatewayReq).Return(pdk.NewStubHTTPResponse(200, nil, gatewayResp)).Once() + host.WebSocketMock.On("Connect", mock.MatchedBy(func(url string) bool { + return strings.Contains(url, "gateway.discord.gg") + }), mock.Anything, "testuser").Return("testuser", nil) + + // Capture the activity payload sent to Discord + var sentPayload string + host.WebSocketMock.On("SendText", "testuser", mock.Anything).Run(func(args mock.Arguments) { + sentPayload = args.Get(1).(string) + }).Return(nil) + host.SchedulerMock.On("ScheduleRecurring", mock.Anything, payloadHeartbeat, "testuser").Return("testuser", nil) + host.SchedulerMock.On("CancelSchedule", "testuser-clear").Return(nil) + + // Image mocks + host.CacheMock.On("GetString", mock.MatchedBy(func(key string) bool { + return strings.HasPrefix(key, "discord.image.") + })).Return("", false, 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) + assetsReq := &pdk.HTTPRequest{} + pdk.PDKMock.On("NewHTTPRequest", pdk.MethodPost, mock.MatchedBy(func(url string) bool { + return strings.Contains(url, "external-assets") + })).Return(assetsReq) + pdk.PDKMock.On("Send", assetsReq).Return(pdk.NewStubHTTPResponse(200, nil, []byte(`{"key":"test-key"}`))) + host.SchedulerMock.On("ScheduleOneTime", mock.Anything, payloadClearActivity, "testuser-clear").Return("testuser-clear", nil) + + err := plugin.NowPlaying(scrobbler.NowPlayingRequest{ + Username: "testuser", + Position: 10, + Track: scrobbler.TrackInfo{ + ID: "track1", + Title: "Test Song", + Artist: "Test Artist", + Album: "Test Album", + Duration: 180, + }, + }) + Expect(err).ToNot(HaveOccurred()) + Expect(sentPayload).To(ContainSubstring(fmt.Sprintf(`"name":"%s"`, expectedName))) + }, + Entry("defaults to Navidrome when not configured", "", false, "Navidrome"), + Entry("defaults to Navidrome with explicit default value", "Default", true, "Navidrome"), + Entry("uses track title when configured", "Track", true, "Test Song"), + Entry("uses track album when configured", "Album", true, "Test Album"), + Entry("uses track artist when configured", "Artist", true, "Test Artist"), + ) }) Describe("Scrobble", func() { diff --git a/manifest.json b/manifest.json index bf2d3d1..7d825d5 100644 --- a/manifest.json +++ b/manifest.json @@ -47,6 +47,13 @@ "maxLength": 20, "pattern": "^[0-9]+$" }, + "activityname": { + "type": "string", + "title": "Activity Name Display", + "description": "Choose what to display as the activity name in Discord Rich Presence", + "enum": ["Default", "Track", "Album", "Artist"], + "default": "Default" + }, "uguuenabled": { "type": "boolean", "title": "Upload artwork to uguu.se (enable if Navidrome is not publicly accessible)", @@ -92,6 +99,13 @@ "type": "Control", "scope": "#/properties/clientid" }, + { + "type": "Control", + "scope": "#/properties/activityname", + "options": { + "format": "radio" + } + }, { "type": "Control", "scope": "#/properties/uguuenabled"