diff --git a/server/subsonic/album_lists.go b/server/subsonic/album_lists.go
index b3902b65c..5cc319cef 100644
--- a/server/subsonic/album_lists.go
+++ b/server/subsonic/album_lists.go
@@ -3,6 +3,7 @@ package subsonic
import (
"context"
"net/http"
+ "slices"
"strconv"
"time"
@@ -218,6 +219,19 @@ func (api *Router) GetNowPlaying(r *http.Request) (*responses.Subsonic, error) {
return nil, err
}
+ // Optionally restrict to specific libraries via the standard Subsonic musicFolderId param.
+ // When absent, all entries are returned, per the getNowPlaying spec.
+ requestedFolderIds, _ := req.Params(r).Ints("musicFolderId")
+ if len(requestedFolderIds) > 0 {
+ folderIds, ferr := selectedMusicFolderIds(r, false)
+ if ferr != nil {
+ return nil, ferr
+ }
+ npInfo = slice.Filter(npInfo, func(np scrobbler.PlaybackSession) bool {
+ return slices.Contains(folderIds, np.MediaFile.LibraryID)
+ })
+ }
+
var i int32
response.NowPlaying.Entry = slice.Map(npInfo, func(np scrobbler.PlaybackSession) responses.NowPlayingEntry {
i++
diff --git a/server/subsonic/album_lists_test.go b/server/subsonic/album_lists_test.go
index 1c8863dd5..efd1362da 100644
--- a/server/subsonic/album_lists_test.go
+++ b/server/subsonic/album_lists_test.go
@@ -591,6 +591,35 @@ var _ = Describe("Album Lists", func() {
Expect(resp.NowPlaying.Entry[1].Title).To(Equal("Track 2"))
})
+ It("should filter entries by the musicFolderId parameter when provided", func() {
+ mockPlayTracker.NowPlayingData = []scrobbler.PlaybackSession{
+ {
+ MediaFile: model.MediaFile{ID: "1", Title: "Track 1", LibraryID: 1},
+ Start: time.Now(),
+ Username: "user1",
+ PlayerId: "player1",
+ PlayerName: "Player 1",
+ },
+ {
+ MediaFile: model.MediaFile{ID: "2", Title: "Track 2", LibraryID: 2},
+ Start: time.Now(),
+ Username: "user2",
+ PlayerId: "player2",
+ PlayerName: "Player 2",
+ },
+ }
+ router := New(ds, nil, nil, nil, nil, nil, nil, nil, nil, mockPlayTracker, nil, nil, nil, nil, nil, nil)
+ ctx := request.WithUser(context.Background(), user)
+ r := newGetRequest("musicFolderId=1")
+ r = r.WithContext(ctx)
+
+ resp, err := router.GetNowPlaying(r)
+
+ Expect(err).ToNot(HaveOccurred())
+ Expect(resp.NowPlaying.Entry).To(HaveLen(1))
+ Expect(resp.NowPlaying.Entry[0].Title).To(Equal("Track 1"))
+ })
+
Context("when NowPlaying.AdminOnly is enabled", func() {
BeforeEach(func() {
DeferCleanup(configtest.SetupConfig())
diff --git a/ui/src/layout/NowPlayingPanel.jsx b/ui/src/layout/NowPlayingPanel.jsx
index 509263b42..5c8cafc55 100644
--- a/ui/src/layout/NowPlayingPanel.jsx
+++ b/ui/src/layout/NowPlayingPanel.jsx
@@ -21,6 +21,7 @@ import {
import { FaRegCirclePlay, FaPause } from 'react-icons/fa6'
import subsonic from '../subsonic'
import { useInterval } from '../common'
+import { useSelectedLibraries } from '../common/useLibrarySelection'
import { nowPlayingCountSync } from '../actions'
import { formatDuration } from '../utils'
import config from '../config'
@@ -370,6 +371,9 @@ const NowPlayingPanel = () => {
const serverUp = useSelector(
(state) => !!state.activity.serverStart.startTime,
)
+ // Limit results to libraries the user can access (explicit picker selection,
+ // or all accessible libraries when nothing is narrowed).
+ const libraryIds = useSelectedLibraries()
const translate = useTranslate()
const notify = useNotify()
const theme = useTheme()
@@ -406,7 +410,7 @@ const NowPlayingPanel = () => {
const doFetchRef = useRef()
doFetchRef.current = () =>
subsonic
- .getNowPlaying()
+ .getNowPlaying(libraryIds)
.then((resp) => resp.json['subsonic-response'])
.then((data) => {
if (data.status === 'ok') {
diff --git a/ui/src/layout/NowPlayingPanel.test.jsx b/ui/src/layout/NowPlayingPanel.test.jsx
index ea4a3568b..a469edb08 100644
--- a/ui/src/layout/NowPlayingPanel.test.jsx
+++ b/ui/src/layout/NowPlayingPanel.test.jsx
@@ -55,7 +55,7 @@ vi.mock('@material-ui/core/styles/useTheme', () => ({
}))
describe('', () => {
- const createMockStore = (overrides = {}) => {
+ const createMockStore = (overrides = {}, libraryOverrides = {}) => {
const defaultState = {
activity: {
nowPlayingCount: 1,
@@ -63,9 +63,17 @@ describe('', () => {
streamReconnected: 0,
...overrides,
},
+ library: {
+ userLibraries: [],
+ selectedLibraries: [],
+ ...libraryOverrides,
+ },
}
return createStore(
- combineReducers({ activity: activityReducer }),
+ combineReducers({
+ activity: activityReducer,
+ library: (state = defaultState.library) => state,
+ }),
defaultState,
)
}
@@ -123,6 +131,38 @@ describe('', () => {
})
})
+ it('requests all accessible libraries when no explicit selection', async () => {
+ const store = createMockStore(
+ {},
+ { userLibraries: [{ id: 1 }, { id: 2 }], selectedLibraries: [] },
+ )
+ render(
+
+
+ ,
+ )
+
+ await vi.advanceTimersByTimeAsync(500)
+
+ expect(subsonic.getNowPlaying).toHaveBeenCalledWith([1, 2])
+ })
+
+ it('requests only the selected libraries when narrowed', async () => {
+ const store = createMockStore(
+ {},
+ { userLibraries: [{ id: 1 }, { id: 2 }], selectedLibraries: [2] },
+ )
+ render(
+
+
+ ,
+ )
+
+ await vi.advanceTimersByTimeAsync(500)
+
+ expect(subsonic.getNowPlaying).toHaveBeenCalledWith([2])
+ })
+
it('displays player name after username', async () => {
const store = createMockStore()
render(
diff --git a/ui/src/subsonic/index.js b/ui/src/subsonic/index.js
index 7d93972e0..c623c9de4 100644
--- a/ui/src/subsonic/index.js
+++ b/ui/src/subsonic/index.js
@@ -70,7 +70,14 @@ const startScan = (options) => httpClient(url('startScan', null, options))
const getScanStatus = () => httpClient(url('getScanStatus'))
-const getNowPlaying = () => httpClient(url('getNowPlaying'))
+const getNowPlaying = (musicFolderId) =>
+ httpClient(
+ url(
+ 'getNowPlaying',
+ null,
+ musicFolderId?.length ? { musicFolderId } : undefined,
+ ),
+ )
const getAvatarUrl = (username, size) =>
baseUrl(