feat: add configuration option to select Activity Name based on currently playing track (#11)
This commit is contained in:
11
README.md
11
README.md
@@ -14,6 +14,7 @@ Based on the [Navicord](https://github.com/logixism/navicord) project.
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Shows currently playing track with title, artist, and album art
|
- 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
|
- Displays playback progress with start/end timestamps
|
||||||
- Automatic presence clearing when track finishes
|
- Automatic presence clearing when track finishes
|
||||||
- Multi-user support with individual Discord tokens
|
- 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
|
3. Copy the "Application ID" from the General Information page
|
||||||
- **Example**: `1234567890123456789`
|
- **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
|
#### Upload to uguu.se
|
||||||
- **When to enable**: Your Navidrome instance is NOT publicly accessible from the internet
|
- **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
|
- **What it does**: Automatically uploads album artwork to uguu.se (temporary hosting) so Discord can display it
|
||||||
|
|||||||
23
main.go
23
main.go
@@ -27,6 +27,15 @@ import (
|
|||||||
const (
|
const (
|
||||||
clientIDKey = "clientid"
|
clientIDKey = "clientid"
|
||||||
usersKey = "users"
|
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
|
// 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
|
startTime := (now - int64(input.Position)) * 1000
|
||||||
endTime := startTime + int64(input.Track.Duration)*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
|
// Send activity update
|
||||||
if err := rpc.sendActivity(clientID, input.Username, userToken, activity{
|
if err := rpc.sendActivity(clientID, input.Username, userToken, activity{
|
||||||
Application: clientID,
|
Application: clientID,
|
||||||
Name: "Navidrome",
|
Name: activityName,
|
||||||
Type: 2, // Listening
|
Type: 2, // Listening
|
||||||
Details: input.Track.Title,
|
Details: input.Track.Title,
|
||||||
State: input.Track.Artist,
|
State: input.Track.Artist,
|
||||||
|
|||||||
61
main_test.go
61
main_test.go
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/navidrome/navidrome/plugins/pdk/go/host"
|
"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", clientIDKey).Return("test-client-id", true)
|
||||||
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)
|
||||||
|
|
||||||
// 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"))
|
||||||
@@ -169,6 +171,65 @@ var _ = Describe("discordPlugin", func() {
|
|||||||
})
|
})
|
||||||
Expect(err).ToNot(HaveOccurred())
|
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() {
|
Describe("Scrobble", func() {
|
||||||
|
|||||||
@@ -47,6 +47,13 @@
|
|||||||
"maxLength": 20,
|
"maxLength": 20,
|
||||||
"pattern": "^[0-9]+$"
|
"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": {
|
"uguuenabled": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"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)",
|
||||||
@@ -92,6 +99,13 @@
|
|||||||
"type": "Control",
|
"type": "Control",
|
||||||
"scope": "#/properties/clientid"
|
"scope": "#/properties/clientid"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "Control",
|
||||||
|
"scope": "#/properties/activityname",
|
||||||
|
"options": {
|
||||||
|
"format": "radio"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "Control",
|
"type": "Control",
|
||||||
"scope": "#/properties/uguuenabled"
|
"scope": "#/properties/uguuenabled"
|
||||||
|
|||||||
Reference in New Issue
Block a user