diff --git a/plugins/examples/discord-rich-presence-rs/README.md b/plugins/examples/discord-rich-presence-rs/README.md index 6dff5f0b7..64e8fd932 100644 --- a/plugins/examples/discord-rich-presence-rs/README.md +++ b/plugins/examples/discord-rich-presence-rs/README.md @@ -31,10 +31,12 @@ This plugin implements three capabilities to demonstrate the nd-host library: Configure in the Navidrome UI (Settings → Plugins → discord-rich-presence): -| Key | Description | Example | -|------------|----------------------------------------------------------|--------------------------------| -| `clientid` | Your Discord application ID | `123456789012345678` | -| `users` | Comma-separated list of `username:token` pairs | `alice:token123,bob:token456` | +| Key | Description | Example | +|---------------|-------------------------------------------|--------------------------------| +| `clientid` | Your Discord application ID | `123456789012345678` | +| `user.` | Discord token for the specified user | `user.alice` = `token123` | + +Each user is configured as a separate key with the `user.` prefix. ### Getting Configuration Values @@ -43,9 +45,10 @@ Configure in the Navidrome UI (Settings → Plugins → discord-rich-presence): 2. **Discord Token**: This requires extracting your user token from Discord (not recommended for security reasons) -3. **Multiple Users**: Separate user mappings with commas: +3. **Multiple Users**: Add multiple user keys: ```properties - users = "user1:token1,user2:token2" + user.user1 = "token1" + user.user2 = "token2" ``` ## Building diff --git a/plugins/examples/discord-rich-presence-rs/src/lib.rs b/plugins/examples/discord-rich-presence-rs/src/lib.rs index f31669eef..c4005a9ca 100644 --- a/plugins/examples/discord-rich-presence-rs/src/lib.rs +++ b/plugins/examples/discord-rich-presence-rs/src/lib.rs @@ -11,7 +11,8 @@ //! ```toml //! [PluginConfig.discord-rich-presence-rs] //! clientid = "YOUR_DISCORD_APPLICATION_ID" -//! users = "username1:discord_token1,username2:discord_token2" +//! "user.username1" = "discord_token1" +//! "user.username2" = "discord_token2" //! ``` //! //! **WARNING**: This plugin is for demonstration purposes only. Storing Discord tokens @@ -47,7 +48,7 @@ nd_pdk::register_websocket_close!(DiscordPlugin); // ============================================================================ const CLIENT_ID_KEY: &str = "clientid"; -const USERS_KEY: &str = "users"; +const USER_KEY_PREFIX: &str = "user."; const PAYLOAD_HEARTBEAT: &str = "heartbeat"; const PAYLOAD_CLEAR_ACTIVITY: &str = "clear-activity"; @@ -68,15 +69,14 @@ fn get_config() -> Result<(String, std::collections::HashMap), E .filter(|s| !s.is_empty()) .ok_or_else(|| Error::msg("missing clientid in configuration"))?; - let users_config = config::get(USERS_KEY)? - .filter(|s| !s.is_empty()) - .unwrap_or_default(); + // Get all user keys with the "user." prefix + let user_keys = config::keys(USER_KEY_PREFIX)?; let mut users = std::collections::HashMap::new(); - for user in users_config.split(',') { - let parts: Vec<&str> = user.split(':').collect(); - if parts.len() == 2 { - users.insert(parts[0].trim().to_string(), parts[1].trim().to_string()); + for key in user_keys { + let username = key.strip_prefix(USER_KEY_PREFIX).unwrap_or(&key); + if let Some(token) = config::get(&key)?.filter(|s| !s.is_empty()) { + users.insert(username.to_string(), token); } } diff --git a/plugins/examples/discord-rich-presence/README.md b/plugins/examples/discord-rich-presence/README.md index 34d7d74ac..bb4d1070a 100644 --- a/plugins/examples/discord-rich-presence/README.md +++ b/plugins/examples/discord-rich-presence/README.md @@ -65,10 +65,12 @@ To work within this model the plugin stores no in-memory state. Connections are Configure in the Navidrome UI (Settings → Plugins → discord-rich-presence): -| Key | Description | Example | -|------------|----------------------------------------------------------|--------------------------------| -| `clientid` | Your Discord application ID | `123456789012345678` | -| `users` | Comma-separated list of `username:token` pairs | `alice:token123,bob:token456` | +| Key | Description | Example | +|---------------|-------------------------------------------|--------------------------------| +| `clientid` | Your Discord application ID | `123456789012345678` | +| `user.` | Discord token for the specified user | `user.alice` = `token123` | + +Each user is configured as a separate key with the `user.` prefix. ## Building diff --git a/plugins/examples/discord-rich-presence/main.go b/plugins/examples/discord-rich-presence/main.go index 55c6e6be2..bd578ef40 100644 --- a/plugins/examples/discord-rich-presence/main.go +++ b/plugins/examples/discord-rich-presence/main.go @@ -24,8 +24,8 @@ import ( // Configuration keys const ( - clientIDKey = "clientid" - usersKey = "users" + clientIDKey = "clientid" + userKeyPrefix = "user." ) // discordPlugin implements the scrobbler and scheduler interfaces. @@ -49,20 +49,27 @@ func getConfig() (clientID string, users map[string]string, err error) { return "", nil, nil } - cfgUsers, ok := pdk.GetConfig(usersKey) - if !ok || cfgUsers == "" { + // Get all user keys with the "user." prefix + userKeys := host.ConfigKeys(userKeyPrefix) + if len(userKeys) == 0 { pdk.Log(pdk.LogWarn, "no users configured") return clientID, nil, nil } users = make(map[string]string) - for _, user := range strings.Split(cfgUsers, ",") { - tuple := strings.Split(user, ":") - if len(tuple) != 2 { - return clientID, nil, fmt.Errorf("invalid user config: %s", user) + for _, key := range userKeys { + username := strings.TrimPrefix(key, userKeyPrefix) + token, exists := host.ConfigGet(key) + if exists && token != "" { + users[username] = token } - users[strings.TrimSpace(tuple[0])] = strings.TrimSpace(tuple[1]) } + + if len(users) == 0 { + pdk.Log(pdk.LogWarn, "no users configured") + return clientID, nil, nil + } + return clientID, users, nil } diff --git a/plugins/examples/discord-rich-presence/main_test.go b/plugins/examples/discord-rich-presence/main_test.go index 01f79b02e..fd35ad929 100644 --- a/plugins/examples/discord-rich-presence/main_test.go +++ b/plugins/examples/discord-rich-presence/main_test.go @@ -28,6 +28,8 @@ var _ = Describe("discordPlugin", func() { pdk.ResetMock() host.CacheMock.ExpectedCalls = nil host.CacheMock.Calls = nil + host.ConfigMock.ExpectedCalls = nil + host.ConfigMock.Calls = nil host.WebSocketMock.ExpectedCalls = nil host.WebSocketMock.Calls = nil host.SchedulerMock.ExpectedCalls = nil @@ -39,7 +41,9 @@ var _ = Describe("discordPlugin", func() { Describe("getConfig", func() { It("returns config values when properly set", func() { pdk.PDKMock.On("GetConfig", clientIDKey).Return("test-client-id", true) - pdk.PDKMock.On("GetConfig", usersKey).Return("user1:token1,user2:token2", true) + host.ConfigMock.On("Keys", userKeyPrefix).Return([]string{"user.user1", "user.user2"}) + host.ConfigMock.On("Get", "user.user1").Return("token1", true) + host.ConfigMock.On("Get", "user.user2").Return("token2", true) pdk.PDKMock.On("Log", mock.Anything, mock.Anything).Maybe() clientID, users, err := getConfig() @@ -62,7 +66,7 @@ var _ = Describe("discordPlugin", func() { It("returns nil users when users not configured", func() { pdk.PDKMock.On("GetConfig", clientIDKey).Return("test-client-id", true) - pdk.PDKMock.On("GetConfig", usersKey).Return("", false) + host.ConfigMock.On("Keys", userKeyPrefix).Return([]string{}) pdk.PDKMock.On("Log", mock.Anything, mock.Anything).Maybe() clientID, users, err := getConfig() @@ -70,16 +74,6 @@ var _ = Describe("discordPlugin", func() { Expect(clientID).To(Equal("test-client-id")) Expect(users).To(BeNil()) }) - - It("returns error for invalid user format", func() { - pdk.PDKMock.On("GetConfig", clientIDKey).Return("test-client-id", true) - pdk.PDKMock.On("GetConfig", usersKey).Return("invalid-format", true) - pdk.PDKMock.On("Log", mock.Anything, mock.Anything).Maybe() - - _, _, err := getConfig() - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("invalid user config")) - }) }) Describe("IsAuthorized", func() { @@ -89,7 +83,8 @@ var _ = Describe("discordPlugin", func() { It("returns true for authorized user", func() { pdk.PDKMock.On("GetConfig", clientIDKey).Return("test-client-id", true) - pdk.PDKMock.On("GetConfig", usersKey).Return("testuser:token123", true) + host.ConfigMock.On("Keys", userKeyPrefix).Return([]string{"user.testuser"}) + host.ConfigMock.On("Get", "user.testuser").Return("token123", true) authorized, err := plugin.IsAuthorized(scrobbler.IsAuthorizedRequest{ Username: "testuser", @@ -100,7 +95,8 @@ var _ = Describe("discordPlugin", func() { It("returns false for unauthorized user", func() { pdk.PDKMock.On("GetConfig", clientIDKey).Return("test-client-id", true) - pdk.PDKMock.On("GetConfig", usersKey).Return("otheruser:token123", true) + host.ConfigMock.On("Keys", userKeyPrefix).Return([]string{"user.otheruser"}) + host.ConfigMock.On("Get", "user.otheruser").Return("token123", true) authorized, err := plugin.IsAuthorized(scrobbler.IsAuthorizedRequest{ Username: "testuser", @@ -108,16 +104,6 @@ var _ = Describe("discordPlugin", func() { Expect(err).ToNot(HaveOccurred()) Expect(authorized).To(BeFalse()) }) - - It("returns error when config parsing fails", func() { - pdk.PDKMock.On("GetConfig", clientIDKey).Return("test-client-id", true) - pdk.PDKMock.On("GetConfig", usersKey).Return("invalid-format", true) - - _, err := plugin.IsAuthorized(scrobbler.IsAuthorizedRequest{ - Username: "testuser", - }) - Expect(err).To(HaveOccurred()) - }) }) Describe("NowPlaying", func() { @@ -125,21 +111,10 @@ var _ = Describe("discordPlugin", func() { pdk.PDKMock.On("Log", mock.Anything, mock.Anything).Maybe() }) - It("returns error when config parsing fails", func() { - pdk.PDKMock.On("GetConfig", clientIDKey).Return("test-client-id", true) - pdk.PDKMock.On("GetConfig", usersKey).Return("invalid-format", true) - - err := plugin.NowPlaying(scrobbler.NowPlayingRequest{ - Username: "testuser", - Track: scrobbler.TrackInfo{Title: "Test Song"}, - }) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to get config")) - }) - It("returns not authorized error when user not in config", func() { pdk.PDKMock.On("GetConfig", clientIDKey).Return("test-client-id", true) - pdk.PDKMock.On("GetConfig", usersKey).Return("otheruser:token", true) + host.ConfigMock.On("Keys", userKeyPrefix).Return([]string{"user.otheruser"}) + host.ConfigMock.On("Get", "user.otheruser").Return("token", true) err := plugin.NowPlaying(scrobbler.NowPlayingRequest{ Username: "testuser", @@ -151,7 +126,8 @@ var _ = Describe("discordPlugin", func() { It("successfully sends now playing update", func() { pdk.PDKMock.On("GetConfig", clientIDKey).Return("test-client-id", true) - pdk.PDKMock.On("GetConfig", usersKey).Return("testuser:test-token", true) + host.ConfigMock.On("Keys", userKeyPrefix).Return([]string{"user.testuser"}) + host.ConfigMock.On("Get", "user.testuser").Return("test-token", true) // Connect mocks (isConnected check via heartbeat) host.CacheMock.On("GetInt", "discord.seq.testuser").Return(int64(0), false, errors.New("not found"))