mirror of
https://github.com/navidrome/navidrome.git
synced 2026-07-09 17:18:45 +00:00
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. Frontend changes update the AppBar component to conditionally render NowPlayingPanel based on both the enabled state and the admin-only permission check. Server-side enforcement of the AdminOnly setting is added in a follow-up commit. Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
parent
05105e91d9
commit
08e632d918
9 changed files with 36 additions and 12 deletions
|
|
@ -95,7 +95,7 @@ type configOptions struct {
|
|||
UICoverArtSize int
|
||||
EnableReplayGain bool
|
||||
EnableCoverAnimation bool
|
||||
EnableNowPlaying bool
|
||||
NowPlaying nowPlayingOptions `json:",omitzero"`
|
||||
UIPlaybackReportInterval time.Duration
|
||||
GATrackingID string
|
||||
EnableLogRedacting bool
|
||||
|
|
@ -236,6 +236,11 @@ type jukeboxOptions struct {
|
|||
AdminOnly bool
|
||||
}
|
||||
|
||||
type nowPlayingOptions struct {
|
||||
Enabled bool
|
||||
AdminOnly bool
|
||||
}
|
||||
|
||||
type backupOptions struct {
|
||||
Count int
|
||||
Path Dir
|
||||
|
|
@ -332,6 +337,7 @@ func Load(noConfigDump bool) {
|
|||
mapDeprecatedOption("CoverJpegQuality", "CoverArtQuality")
|
||||
mapDeprecatedOption("SimilarSongsMatchThreshold", "Matcher.FuzzyThreshold")
|
||||
mapDeprecatedOption("EnableTranscodingCancellation", "Transcoding.EnableCancellation")
|
||||
mapDeprecatedOption("EnableNowPlaying", "NowPlaying.Enabled")
|
||||
|
||||
err := viper.Unmarshal(&Server, viper.DecodeHook(
|
||||
mapstructure.ComposeDecodeHookFunc(
|
||||
|
|
@ -458,6 +464,7 @@ func Load(noConfigDump bool) {
|
|||
logDeprecatedOptions("CoverJpegQuality", "CoverArtQuality")
|
||||
logDeprecatedOptions("SimilarSongsMatchThreshold", "Matcher.FuzzyThreshold")
|
||||
logDeprecatedOptions("EnableTranscodingCancellation", "Transcoding.EnableCancellation")
|
||||
logDeprecatedOptions("EnableNowPlaying", "NowPlaying.Enabled")
|
||||
|
||||
// Removed options
|
||||
logRemovedOptions("Spotify.ID", "Spotify.Secret")
|
||||
|
|
@ -789,7 +796,8 @@ func setViperDefaults() {
|
|||
viper.SetDefault("uicoverartsize", consts.DefaultUICoverArtSize)
|
||||
viper.SetDefault("enablereplaygain", true)
|
||||
viper.SetDefault("enablecoveranimation", true)
|
||||
viper.SetDefault("enablenowplaying", true)
|
||||
viper.SetDefault("nowplaying.enabled", true)
|
||||
viper.SetDefault("nowplaying.adminonly", false)
|
||||
viper.SetDefault("uiplaybackreportinterval", consts.DefaultUIPlaybackReportInterval)
|
||||
viper.SetDefault("enableartworkupload", true)
|
||||
viper.SetDefault("maximageuploadsize", consts.DefaultMaxImageUploadSize)
|
||||
|
|
|
|||
|
|
@ -198,7 +198,7 @@ var staticData = sync.OnceValue(func() insights.Data {
|
|||
data.Config.EnableWebPEncoding = conf.Server.EnableWebPEncoding
|
||||
data.Config.UICoverArtSize = conf.Server.UICoverArtSize
|
||||
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
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ func newPlayTracker(ds model.DataStore, broker events.Broker, pluginManager Plug
|
|||
prSignal: make(chan struct{}, 1),
|
||||
prWorkerDone: make(chan struct{}),
|
||||
}
|
||||
enableNowPlaying := conf.Server.EnableNowPlaying
|
||||
enableNowPlaying := conf.Server.NowPlaying.Enabled
|
||||
m.OnExpiration(func(_ string, info PlaybackSession) {
|
||||
log.Debug("PlaybackSession expired", "clientId", info.PlayerId, "mediaId", info.MediaFile.ID, "state",
|
||||
info.State, "username", info.Username, "userId", info.UserId)
|
||||
|
|
@ -367,7 +367,7 @@ func (p *playTracker) ReportPlayback(ctx context.Context, params ReportPlaybackP
|
|||
p.playMap.Remove(clientId)
|
||||
}
|
||||
|
||||
if conf.Server.EnableNowPlaying {
|
||||
if conf.Server.NowPlaying.Enabled {
|
||||
p.broker.SendBroadcastMessage(ctx, &events.NowPlayingCount{Count: p.playMap.Len()})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -148,7 +148,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 := PlaybackSession{MediaFile: track, Start: time.Now(), Username: "user"}
|
||||
_ = tracker.playMap.AddWithTTL("player-2", info, 10*time.Millisecond)
|
||||
|
|
@ -455,8 +455,8 @@ var _ = Describe("PlayTracker", func() {
|
|||
Expect(evts[3].(*events.NowPlayingCount).Count).To(Equal(0))
|
||||
})
|
||||
|
||||
It("does NOT broadcast when EnableNowPlaying is false", func() {
|
||||
conf.Server.EnableNowPlaying = false
|
||||
It("does NOT broadcast when NowPlaying is disabled", func() {
|
||||
conf.Server.NowPlaying.Enabled = false
|
||||
tracker = newPlayTracker(ds, eventBroker, nil)
|
||||
tracker.builtinScrobblers["fake"] = fake
|
||||
|
||||
|
|
|
|||
|
|
@ -57,7 +57,8 @@ func serveIndex(ds model.DataStore, fs fs.FS, shareInfo *model.Share) http.Handl
|
|||
"uiSearchDebounceMs": conf.Server.UISearchDebounceMs,
|
||||
"uiCoverArtSize": conf.Server.UICoverArtSize,
|
||||
"enableCoverAnimation": conf.Server.EnableCoverAnimation,
|
||||
"enableNowPlaying": conf.Server.EnableNowPlaying,
|
||||
"enableNowPlaying": conf.Server.NowPlaying.Enabled,
|
||||
"nowPlayingAdminOnly": conf.Server.NowPlaying.AdminOnly,
|
||||
"playbackReportIntervalMs": conf.Server.UIPlaybackReportInterval.Milliseconds(),
|
||||
"gaTrackingId": conf.Server.GATrackingID,
|
||||
"losslessFormats": strings.ToUpper(strings.Join(mime.LosslessFormats, ",")),
|
||||
|
|
|
|||
|
|
@ -88,7 +88,8 @@ var _ = Describe("serveIndex", func() {
|
|||
Entry("uiSearchDebounceMs", func() { conf.Server.UISearchDebounceMs = 500 }, "uiSearchDebounceMs", float64(500)),
|
||||
Entry("uiCoverArtSize", func() { conf.Server.UICoverArtSize = 300 }, "uiCoverArtSize", float64(300)),
|
||||
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),
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ const defaultConfig = {
|
|||
enableCoverAnimation: true,
|
||||
enableNowPlaying: true,
|
||||
playbackReportIntervalMs: 60000,
|
||||
nowPlayingAdminOnly: false,
|
||||
devShowArtistPage: true,
|
||||
devUIShowConfig: true,
|
||||
devNewEventStream: false,
|
||||
|
|
|
|||
|
|
@ -121,8 +121,10 @@ const CustomUserMenu = ({ onClick, ...rest }) => {
|
|||
return (
|
||||
<>
|
||||
{config.devActivityPanel &&
|
||||
permissions === 'admin' &&
|
||||
config.enableNowPlaying && <NowPlayingPanel />}
|
||||
config.enableNowPlaying &&
|
||||
(!config.nowPlayingAdminOnly || permissions === 'admin') && (
|
||||
<NowPlayingPanel />
|
||||
)}
|
||||
{config.devActivityPanel && permissions === 'admin' && <ActivityPanel />}
|
||||
<UserMenu {...rest}>
|
||||
<PersonalMenu sidebarIsOpen={true} onClick={onClick} />
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ describe('<AppBar />', () => {
|
|||
beforeEach(() => {
|
||||
config.devActivityPanel = true
|
||||
config.enableNowPlaying = true
|
||||
config.nowPlayingAdminOnly = true
|
||||
store = createStore(combineReducers({ activity: activityReducer }), {
|
||||
activity: { nowPlayingCount: 0 },
|
||||
})
|
||||
|
|
@ -62,4 +63,14 @@ describe('<AppBar />', () => {
|
|||
)
|
||||
expect(screen.queryByTestId('now-playing-panel')).toBeNull()
|
||||
})
|
||||
|
||||
it('shows NowPlayingPanel to all users when adminOnly is false', () => {
|
||||
config.nowPlayingAdminOnly = false
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<AppBar />
|
||||
</Provider>,
|
||||
)
|
||||
expect(screen.getByTestId('now-playing-panel')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue