add custom activity name template with placeholder support

This commit is contained in:
2026-03-09 17:10:28 -06:00
parent 4700e15a3c
commit 39293031d9
4 changed files with 97 additions and 5 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
*.wasm *.wasm
*.ndp *.ndp
tmp tmp
.DS_Store

16
main.go
View File

@@ -25,9 +25,10 @@ import (
// Configuration keys // Configuration keys
const ( const (
clientIDKey = "clientid" clientIDKey = "clientid"
usersKey = "users" usersKey = "users"
activityNameKey = "activityname" activityNameKey = "activityname"
activityNameTemplateKey = "activitynametemplate"
) )
// Activity name display options // Activity name display options
@@ -36,6 +37,7 @@ const (
activityNameTrack = "Track" activityNameTrack = "Track"
activityNameArtist = "Artist" activityNameArtist = "Artist"
activityNameAlbum = "Album" activityNameAlbum = "Album"
activityNameCustom = "Custom"
) )
// userToken represents a user-token mapping from the config // userToken represents a user-token mapping from the config
@@ -155,6 +157,14 @@ func (p *discordPlugin) NowPlaying(input scrobbler.NowPlayingRequest) error {
activityName = input.Track.Album activityName = input.Track.Album
case activityNameArtist: case activityNameArtist:
activityName = input.Track.Artist activityName = input.Track.Artist
case activityNameCustom:
template, _ := pdk.GetConfig(activityNameTemplateKey)
if template != "" {
activityName = template
activityName = strings.ReplaceAll(activityName, "{track}", input.Track.Title)
activityName = strings.ReplaceAll(activityName, "{artist}", input.Track.Artist)
activityName = strings.ReplaceAll(activityName, "{album}", input.Track.Album)
}
} }
// Send activity update // Send activity update

View File

@@ -230,6 +230,67 @@ var _ = Describe("discordPlugin", func() {
Entry("uses track album when configured", "Album", true, "Test Album"), Entry("uses track album when configured", "Album", true, "Test Album"),
Entry("uses track artist when configured", "Artist", true, "Test Artist"), Entry("uses track artist when configured", "Artist", true, "Test Artist"),
) )
DescribeTable("custom activity name template",
func(template string, templateExists 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("Custom", true)
pdk.PDKMock.On("GetConfig", activityNameTemplateKey).Return(template, templateExists)
// 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("uses custom template with all placeholders", "{artist} - {track} ({album})", true, "Test Artist - Test Song (Test Album)"),
Entry("uses custom template with only track", "{track}", true, "Test Song"),
Entry("uses custom template with only artist", "{artist}", true, "Test Artist"),
Entry("uses custom template with only album", "{album}", true, "Test Album"),
Entry("uses custom template with plain text", "Now Playing", true, "Now Playing"),
Entry("falls back to Navidrome when template is empty", "", false, "Navidrome"),
)
}) })
Describe("Scrobble", func() { Describe("Scrobble", func() {

View File

@@ -55,10 +55,17 @@
"Default", "Default",
"Track", "Track",
"Album", "Album",
"Artist" "Artist",
"Custom"
], ],
"default": "Default" "default": "Default"
}, },
"activitynametemplate": {
"type": "string",
"title": "Custom Activity Name Template",
"description": "Template for the activity name. Available placeholders: {track}, {artist}, {album}",
"default": "{artist} - {track}"
},
"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)",
@@ -111,6 +118,19 @@
"format": "radio" "format": "radio"
} }
}, },
{
"type": "Control",
"scope": "#/properties/activitynametemplate",
"rule": {
"effect": "SHOW",
"condition": {
"scope": "#/properties/activityname",
"schema": {
"const": "Custom"
}
}
}
},
{ {
"type": "Control", "type": "Control",
"scope": "#/properties/uguuenabled" "scope": "#/properties/uguuenabled"