From 2ff5379b0b2ecdf7bc0f89a9460f69d7d0d2a209 Mon Sep 17 00:00:00 2001 From: Deluan Date: Mon, 15 Dec 2025 12:37:59 -0500 Subject: [PATCH] feat: add configurable visibility control for NowPlaying feature Replaces the boolean EnableNowPlaying option with a more flexible NowPlaying configuration structure containing Enabled and AdminOnly flags. This allows three visibility modes: disabled, admin-only, and all users. The new configuration uses nowplayingOptions struct similar to jukeboxOptions, with the following defaults: - NowPlaying.Enabled: true (feature enabled) - NowPlaying.AdminOnly: false (visible to all users) The old EnableNowPlaying option is deprecated and automatically migrated to NowPlaying.Enabled with a warning message. Backend filtering ensures NowPlaying data respects the AdminOnly setting, returning empty results for non-admin users when enabled. Frontend changes update the AppBar component to conditionally render NowPlayingPanel based on both the enabled state and admin-only permission check. Signed-off-by: Deluan --- conf/configuration.go | 12 ++++++++++-- core/metrics/insights.go | 2 +- core/scrobbler/play_tracker.go | 4 ++-- core/scrobbler/play_tracker_test.go | 4 ++-- server/serve_index.go | 3 ++- server/serve_index_test.go | 3 ++- ui/src/config.js | 1 + ui/src/layout/AppBar.jsx | 6 ++++-- ui/src/layout/AppBar.test.jsx | 11 +++++++++++ 9 files changed, 35 insertions(+), 11 deletions(-) diff --git a/conf/configuration.go b/conf/configuration.go index 07e04e965..1eec036bd 100644 --- a/conf/configuration.go +++ b/conf/configuration.go @@ -81,7 +81,7 @@ type configOptions struct { DefaultUIVolume int EnableReplayGain bool EnableCoverAnimation bool - EnableNowPlaying bool + NowPlaying nowPlayingOptions `json:",omitzero"` GATrackingID string EnableLogRedacting bool AuthRequestLimit int @@ -207,6 +207,11 @@ type jukeboxOptions struct { AdminOnly bool } +type nowPlayingOptions struct { + Enabled bool + AdminOnly bool +} + type backupOptions struct { Count int Path string @@ -258,6 +263,7 @@ func Load(noConfigDump bool) { mapDeprecatedOption("ReverseProxyWhitelist", "ExtAuth.TrustedSources") mapDeprecatedOption("ReverseProxyUserHeader", "ExtAuth.UserHeader") mapDeprecatedOption("HTTPSecurityHeaders.CustomFrameOptionsValue", "HTTPHeaders.FrameOptions") + mapDeprecatedOption("EnableNowPlaying", "NowPlaying.Enabled") err := viper.Unmarshal(&Server) if err != nil { @@ -374,6 +380,7 @@ func Load(noConfigDump bool) { logDeprecatedOptions("ReverseProxyWhitelist", "ExtAuth.TrustedSources") logDeprecatedOptions("ReverseProxyUserHeader", "ExtAuth.UserHeader") logDeprecatedOptions("HTTPSecurityHeaders.CustomFrameOptionsValue", "HTTPHeaders.FrameOptions") + logDeprecatedOptions("EnableNowPlaying", "NowPlaying.Enabled") // Call init hooks for _, hook := range hooks { @@ -574,7 +581,8 @@ func setViperDefaults() { viper.SetDefault("defaultuivolume", consts.DefaultUIVolume) viper.SetDefault("enablereplaygain", true) viper.SetDefault("enablecoveranimation", true) - viper.SetDefault("enablenowplaying", true) + viper.SetDefault("nowplaying.enabled", true) + viper.SetDefault("nowplaying.adminonly", false) viper.SetDefault("enablesharing", false) viper.SetDefault("shareurl", "") viper.SetDefault("defaultshareexpiration", 8760*time.Hour) diff --git a/core/metrics/insights.go b/core/metrics/insights.go index 411bc9ac1..e536f4377 100644 --- a/core/metrics/insights.go +++ b/core/metrics/insights.go @@ -199,7 +199,7 @@ var staticData = sync.OnceValue(func() insights.Data { data.Config.DefaultBackgroundURLSet = conf.Server.UILoginBackgroundURL == consts.DefaultUILoginBackgroundURL data.Config.EnableArtworkPrecache = conf.Server.EnableArtworkPrecache data.Config.EnableCoverAnimation = conf.Server.EnableCoverAnimation - data.Config.EnableNowPlaying = conf.Server.EnableNowPlaying + data.Config.EnableNowPlaying = conf.Server.NowPlaying.Enabled data.Config.EnableDownloads = conf.Server.EnableDownloads data.Config.EnableSharing = conf.Server.EnableSharing data.Config.EnableStarRating = conf.Server.EnableStarRating diff --git a/core/scrobbler/play_tracker.go b/core/scrobbler/play_tracker.go index 49c1dd87b..aab908415 100644 --- a/core/scrobbler/play_tracker.go +++ b/core/scrobbler/play_tracker.go @@ -88,7 +88,7 @@ func newPlayTracker(ds model.DataStore, broker events.Broker, pluginManager Plug shutdown: make(chan struct{}), workerDone: make(chan struct{}), } - if conf.Server.EnableNowPlaying { + if conf.Server.NowPlaying.Enabled { m.OnExpiration(func(_ string, _ NowPlayingInfo) { broker.SendBroadcastMessage(context.Background(), &events.NowPlayingCount{Count: m.Len()}) }) @@ -216,7 +216,7 @@ func (p *playTracker) NowPlaying(ctx context.Context, playerId string, playerNam // Add 5 seconds buffer to ensure the NowPlaying info is available slightly longer than the track duration. ttl := time.Duration(remaining+5) * time.Second _ = p.playMap.AddWithTTL(playerId, info, ttl) - if conf.Server.EnableNowPlaying { + if conf.Server.NowPlaying.Enabled { p.broker.SendBroadcastMessage(ctx, &events.NowPlayingCount{Count: p.playMap.Len()}) } player, _ := request.PlayerFrom(ctx) diff --git a/core/scrobbler/play_tracker_test.go b/core/scrobbler/play_tracker_test.go index 839590e6b..c067a0441 100644 --- a/core/scrobbler/play_tracker_test.go +++ b/core/scrobbler/play_tracker_test.go @@ -165,7 +165,7 @@ var _ = Describe("PlayTracker", func() { }) It("does not send event when disabled", func() { - conf.Server.EnableNowPlaying = false + conf.Server.NowPlaying.Enabled = false err := tracker.NowPlaying(ctx, "player-1", "player-one", "123", 0) Expect(err).ToNot(HaveOccurred()) Expect(eventBroker.getEvents()).To(BeEmpty()) @@ -221,7 +221,7 @@ var _ = Describe("PlayTracker", func() { }) It("does not send event when disabled", func() { - conf.Server.EnableNowPlaying = false + conf.Server.NowPlaying.Enabled = false tracker = newPlayTracker(ds, eventBroker, nil) info := NowPlayingInfo{MediaFile: track, Start: time.Now(), Username: "user"} _ = tracker.(*playTracker).playMap.AddWithTTL("player-2", info, 10*time.Millisecond) diff --git a/server/serve_index.go b/server/serve_index.go index 38e646982..38da87e80 100644 --- a/server/serve_index.go +++ b/server/serve_index.go @@ -55,7 +55,8 @@ func serveIndex(ds model.DataStore, fs fs.FS, shareInfo *model.Share) http.Handl "defaultLanguage": conf.Server.DefaultLanguage, "defaultUIVolume": conf.Server.DefaultUIVolume, "enableCoverAnimation": conf.Server.EnableCoverAnimation, - "enableNowPlaying": conf.Server.EnableNowPlaying, + "enableNowPlaying": conf.Server.NowPlaying.Enabled, + "nowPlayingAdminOnly": conf.Server.NowPlaying.AdminOnly, "gaTrackingId": conf.Server.GATrackingID, "losslessFormats": strings.ToUpper(strings.Join(mime.LosslessFormats, ",")), "devActivityPanel": conf.Server.DevActivityPanel, diff --git a/server/serve_index_test.go b/server/serve_index_test.go index 4f179f22a..69fd32a2f 100644 --- a/server/serve_index_test.go +++ b/server/serve_index_test.go @@ -86,7 +86,8 @@ var _ = Describe("serveIndex", func() { Entry("defaultLanguage", func() { conf.Server.DefaultLanguage = "pt" }, "defaultLanguage", "pt"), Entry("defaultUIVolume", func() { conf.Server.DefaultUIVolume = 45 }, "defaultUIVolume", float64(45)), Entry("enableCoverAnimation", func() { conf.Server.EnableCoverAnimation = true }, "enableCoverAnimation", true), - Entry("enableNowPlaying", func() { conf.Server.EnableNowPlaying = true }, "enableNowPlaying", true), + Entry("enableNowPlaying", func() { conf.Server.NowPlaying.Enabled = true }, "enableNowPlaying", true), + Entry("nowPlayingAdminOnly", func() { conf.Server.NowPlaying.AdminOnly = true }, "nowPlayingAdminOnly", true), Entry("gaTrackingId", func() { conf.Server.GATrackingID = "UA-12345" }, "gaTrackingId", "UA-12345"), Entry("defaultDownloadableShare", func() { conf.Server.DefaultDownloadableShare = true }, "defaultDownloadableShare", true), Entry("devSidebarPlaylists", func() { conf.Server.DevSidebarPlaylists = true }, "devSidebarPlaylists", true), diff --git a/ui/src/config.js b/ui/src/config.js index a53a97de7..1e4e31e96 100644 --- a/ui/src/config.js +++ b/ui/src/config.js @@ -30,6 +30,7 @@ const defaultConfig = { enableExternalServices: true, enableCoverAnimation: true, enableNowPlaying: true, + nowPlayingAdminOnly: false, devShowArtistPage: true, devUIShowConfig: true, devNewEventStream: false, diff --git a/ui/src/layout/AppBar.jsx b/ui/src/layout/AppBar.jsx index 561701dce..510af7eaa 100644 --- a/ui/src/layout/AppBar.jsx +++ b/ui/src/layout/AppBar.jsx @@ -121,8 +121,10 @@ const CustomUserMenu = ({ onClick, ...rest }) => { return ( <> {config.devActivityPanel && - permissions === 'admin' && - config.enableNowPlaying && } + config.enableNowPlaying && + (!config.nowPlayingAdminOnly || permissions === 'admin') && ( + + )} {config.devActivityPanel && permissions === 'admin' && } diff --git a/ui/src/layout/AppBar.test.jsx b/ui/src/layout/AppBar.test.jsx index f39dd75cb..3b3015f2f 100644 --- a/ui/src/layout/AppBar.test.jsx +++ b/ui/src/layout/AppBar.test.jsx @@ -39,6 +39,7 @@ describe('', () => { beforeEach(() => { config.devActivityPanel = true config.enableNowPlaying = true + config.nowPlayingAdminOnly = true store = createStore(combineReducers({ activity: activityReducer }), { activity: { nowPlayingCount: 0 }, }) @@ -62,4 +63,14 @@ describe('', () => { ) expect(screen.queryByTestId('now-playing-panel')).toBeNull() }) + + it('shows NowPlayingPanel to all users when adminOnly is false', () => { + config.nowPlayingAdminOnly = false + render( + + + , + ) + expect(screen.getByTestId('now-playing-panel')).toBeInTheDocument() + }) })