Merge pull request 'Custom Activity Name Templates' (#23) from atridadl/main into main
Test / Test (push) Has been cancelled

Reviewed-on: #23
This commit was merged in pull request #23.
This commit is contained in:
2026-04-02 15:48:21 -06:00
7 changed files with 159 additions and 7 deletions
+1
View File
@@ -0,0 +1 @@
flake-profile-1-link
+1
View File
@@ -0,0 +1 @@
/nix/store/vvmz1kzp0iwsxsmlh0ss6snzn6mx9g4j-nix-shell-env
+2
View File
@@ -1,4 +1,6 @@
*.wasm *.wasm
*.ndp *.ndp
tmp tmp
discordrome discordrome
.DS_Store
Generated
+61
View File
@@ -0,0 +1,61 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1775036866,
"narHash": "sha256-ZojAnPuCdy657PbTq5V0Y+AHKhZAIwSIT2cb8UgAz/U=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "6201e203d09599479a3b3450ed24fa81537ebc4e",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}
+18 -6
View File
@@ -25,12 +25,13 @@ import (
// Configuration keys // Configuration keys
const ( const (
clientIDKey = "clientid" clientIDKey = "clientid"
usersKey = "users" usersKey = "users"
activityNameKey = "activityname" activityNameKey = "activityname"
spotifyLinksKey = "spotifylinks" activityNameTemplateKey = "activitynametemplate"
caaEnabledKey = "caaenabled" spotifyLinksKey = "spotifylinks"
uguuEnabledKey = "uguuenabled" caaEnabledKey = "caaenabled"
uguuEnabledKey = "uguuenabled"
) )
const ( const (
@@ -47,6 +48,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
@@ -170,6 +172,16 @@ func (p *discordPlugin) NowPlaying(input scrobbler.NowPlayingRequest) error {
case activityNameArtist: case activityNameArtist:
activityName = input.Track.Artist activityName = input.Track.Artist
statusDisplayType = statusDisplayName statusDisplayType = statusDisplayName
case activityNameCustom:
template, _ := pdk.GetConfig(activityNameTemplateKey)
if template != "" {
r := strings.NewReplacer(
"{track}", input.Track.Title,
"{artist}", input.Track.Artist,
"{album}", input.Track.Album,
)
activityName = r.Replace(template)
}
} }
// Resolve Spotify URLs if enabled // Resolve Spotify URLs if enabled
+56
View File
@@ -225,6 +225,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() {
+20 -1
View File
@@ -51,9 +51,15 @@
"type": "string", "type": "string",
"title": "Activity Name Display", "title": "Activity Name Display",
"description": "Choose what to display as the activity name in Discord Rich Presence", "description": "Choose what to display as the activity name in Discord Rich Presence",
"enum": ["Default", "Track", "Album", "Artist"], "enum": ["Default", "Track", "Album", "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}"
},
"caaenabled": { "caaenabled": {
"type": "boolean", "type": "boolean",
"title": "Use artwork from Cover Art Archive (for MusicBrainz-tagged music)", "title": "Use artwork from Cover Art Archive (for MusicBrainz-tagged music)",
@@ -112,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/caaenabled" "scope": "#/properties/caaenabled"