Custom Activity Name Templates #23

Merged
atridadl merged 4 commits from atridadl/main into main 2026-04-02 15:48:21 -06:00
4 changed files with 93 additions and 6 deletions
Showing only changes of commit 24fb4cf752 - Show all commits
+2 -1
View File
@@ -1,4 +1,5 @@
*.wasm *.wasm
*.ndp *.ndp
tmp tmp
discord-rich-presence discord-rich-presence
.DS_Store
+14 -4
View File
@@ -25,10 +25,11 @@ import (
// Configuration keys // Configuration keys
const ( const (
clientIDKey = "clientid" clientIDKey = "clientid"
usersKey = "users" usersKey = "users"
activityNameKey = "activityname" activityNameKey = "activityname"
spotifyLinksKey = "spotifylinks" activityNameTemplateKey = "activitynametemplate"
spotifyLinksKey = "spotifylinks"
) )
const ( const (
@@ -45,6 +46,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
@@ -168,6 +170,14 @@ func (p *discordPlugin) NowPlaying(input scrobbler.NowPlayingRequest) error {
case activityNameArtist: case activityNameArtist:
gemini-code-assist[bot] commented 2026-03-09 18:20:10 -06:00 (Migrated from github.com)
Outdated
Review

medium

The current implementation uses multiple strings.ReplaceAll calls to substitute placeholders in the template. This can be inefficient as it creates a new string for each replacement. For better performance and cleaner code, you can use strings.NewReplacer. It builds a more efficient replacer and performs all substitutions in a single pass. This is a common Go idiom for multiple replacements.

			r := strings.NewReplacer(
				"{track}", input.Track.Title,
				"{artist}", input.Track.Artist,
				"{album}", input.Track.Album,
			)
			activityName = r.Replace(template)
![medium](https://www.gstatic.com/codereviewagent/medium-priority.svg) The current implementation uses multiple `strings.ReplaceAll` calls to substitute placeholders in the template. This can be inefficient as it creates a new string for each replacement. For better performance and cleaner code, you can use `strings.NewReplacer`. It builds a more efficient replacer and performs all substitutions in a single pass. This is a common Go idiom for multiple replacements. ```go r := strings.NewReplacer( "{track}", input.Track.Title, "{artist}", input.Track.Artist, "{album}", input.Track.Album, ) activityName = r.Replace(template) ```
atridadl commented 2026-03-09 23:28:19 -06:00 (Migrated from github.com)
Outdated
Review

Sure thing clanker

Sure thing clanker
activityName = input.Track.Artist activityName = input.Track.Artist
statusDisplayType = statusDisplayName statusDisplayType = statusDisplayName
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)
}
} }
// Resolve Spotify URLs if enabled // Resolve Spotify URLs if enabled
+56
View File
@@ -223,6 +223,62 @@ var _ = Describe("discordPlugin", func() {
Entry("uses track album when configured", "Album", true, "Test Album", 0), Entry("uses track album when configured", "Album", true, "Test Album", 0),
Entry("uses track artist when configured", "Artist", true, "Test Artist", 0), Entry("uses track artist when configured", "Artist", true, "Test Artist", 0),
) )
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)
pdk.PDKMock.On("GetConfig", spotifyLinksKey).Return("", false)
// Connect mocks
host.CacheMock.On("GetInt", "discord.seq.testuser").Return(int64(0), false, errors.New("not found"))
gatewayResp := []byte(`{"url":"wss://gateway.discord.gg"}`)
host.HTTPMock.On("Send", mock.MatchedBy(func(req host.HTTPRequest) bool {
return req.Method == "GET" && req.URL == "https://discord.com/api/gateway"
})).Return(&host.HTTPResponse{StatusCode: 200, Body: gatewayResp}, nil)
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", discordImageKey).Return("", false, nil)
host.CacheMock.On("SetString", discordImageKey, mock.Anything, mock.Anything).Return(nil)
host.ArtworkMock.On("GetTrackUrl", "track1", int32(300)).Return("https://example.com/art.jpg", nil)
host.HTTPMock.On("Send", externalAssetsReq).Return(&host.HTTPResponse{StatusCode: 200, Body: []byte(`{}`)}, nil)
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() {
+21 -1
View File
@@ -56,10 +56,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)",
@@ -118,6 +125,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"