From 7c13c8182a2884ec5f2c7dfdbc998194f2344b92 Mon Sep 17 00:00:00 2001 From: Deluan Date: Mon, 15 Dec 2025 19:57:01 -0500 Subject: [PATCH] feat: filter NowPlaying entries by user's accessible libraries Signed-off-by: Deluan --- server/subsonic/album_lists.go | 20 +++-- server/subsonic/album_lists_test.go | 129 ++++++++++++++++++++++++++++ 2 files changed, 142 insertions(+), 7 deletions(-) diff --git a/server/subsonic/album_lists.go b/server/subsonic/album_lists.go index 56cf469c5..6bc2111ae 100644 --- a/server/subsonic/album_lists.go +++ b/server/subsonic/album_lists.go @@ -3,10 +3,10 @@ package subsonic import ( "context" "net/http" + "slices" "strconv" "time" - "github.com/navidrome/navidrome/core/scrobbler" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/server/subsonic/filter" @@ -209,18 +209,24 @@ func (api *Router) GetNowPlaying(r *http.Request) (*responses.Subsonic, error) { return nil, err } + // Get user's accessible library IDs for filtering + accessibleLibraryIds, _ := selectedMusicFolderIds(r, false) + response := newResponse() response.NowPlaying = &responses.NowPlaying{} - var i int32 - response.NowPlaying.Entry = slice.Map(npInfo, func(np scrobbler.NowPlayingInfo) responses.NowPlayingEntry { - return responses.NowPlayingEntry{ + // Filter entries to only include tracks from libraries the user has access to + for i, np := range npInfo { + if !slices.Contains(accessibleLibraryIds, np.MediaFile.LibraryID) { + continue + } + response.NowPlaying.Entry = append(response.NowPlaying.Entry, responses.NowPlayingEntry{ Child: childFromMediaFile(ctx, np.MediaFile), UserName: np.Username, MinutesAgo: int32(time.Since(np.Start).Minutes()), - PlayerId: i + 1, // Fake numeric playerId, it does not seem to be used for anything + PlayerId: int32(i), // Fake numeric playerId, it does not seem to be used for anything PlayerName: np.PlayerName, - } - }) + }) + } return response, nil } diff --git a/server/subsonic/album_lists_test.go b/server/subsonic/album_lists_test.go index 63c2614cd..326cd0d32 100644 --- a/server/subsonic/album_lists_test.go +++ b/server/subsonic/album_lists_test.go @@ -4,8 +4,10 @@ import ( "context" "errors" "net/http/httptest" + "time" "github.com/navidrome/navidrome/core/auth" + "github.com/navidrome/navidrome/core/scrobbler" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/model/request" @@ -539,4 +541,131 @@ var _ = Describe("Album Lists", func() { }) }) }) + + Describe("GetNowPlaying", func() { + var mockPlayTracker *mockPlayTrackerForAlbumLists + var user model.User + + BeforeEach(func() { + mockPlayTracker = &mockPlayTrackerForAlbumLists{} + user = model.User{ + ID: "test-user", + Libraries: []model.Library{ + {ID: 1, Name: "Library 1"}, + {ID: 2, Name: "Library 2"}, + }, + } + }) + + It("should filter entries by user's accessible libraries", func() { + mockPlayTracker.NowPlayingData = []scrobbler.NowPlayingInfo{ + { + 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: 3}, // Library 3 not accessible to user + Start: time.Now(), + Username: "user2", + PlayerId: "player2", + PlayerName: "Player 2", + }, + { + MediaFile: model.MediaFile{ID: "3", Title: "Track 3", LibraryID: 2}, + Start: time.Now(), + Username: "user3", + PlayerId: "player3", + PlayerName: "Player 3", + }, + } + router := New(ds, nil, nil, nil, nil, nil, nil, nil, nil, mockPlayTracker, nil, nil, nil) + ctx := request.WithUser(context.Background(), user) + r := newGetRequest() + r = r.WithContext(ctx) + + resp, err := router.GetNowPlaying(r) + + Expect(err).ToNot(HaveOccurred()) + Expect(resp.NowPlaying.Entry).To(HaveLen(2)) + // Should only include tracks from libraries 1 and 2, not library 3 + Expect(resp.NowPlaying.Entry[0].Title).To(Equal("Track 1")) + Expect(resp.NowPlaying.Entry[1].Title).To(Equal("Track 3")) + }) + + It("should return empty list when user has no accessible libraries", func() { + mockPlayTracker.NowPlayingData = []scrobbler.NowPlayingInfo{ + { + MediaFile: model.MediaFile{ID: "1", Title: "Track 1", LibraryID: 5}, // Library not accessible + Start: time.Now(), + Username: "user1", + PlayerId: "player1", + PlayerName: "Player 1", + }, + } + router := New(ds, nil, nil, nil, nil, nil, nil, nil, nil, mockPlayTracker, nil, nil, nil) + userWithNoLibraries := model.User{ID: "no-lib-user", Libraries: []model.Library{}} + ctx := request.WithUser(context.Background(), userWithNoLibraries) + r := newGetRequest() + r = r.WithContext(ctx) + + resp, err := router.GetNowPlaying(r) + + Expect(err).ToNot(HaveOccurred()) + Expect(resp.NowPlaying.Entry).To(HaveLen(0)) + }) + + It("should return all entries when user has access to all libraries", func() { + mockPlayTracker.NowPlayingData = []scrobbler.NowPlayingInfo{ + { + 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) + ctx := request.WithUser(context.Background(), user) + r := newGetRequest() + r = r.WithContext(ctx) + + resp, err := router.GetNowPlaying(r) + + Expect(err).ToNot(HaveOccurred()) + Expect(resp.NowPlaying.Entry).To(HaveLen(2)) + }) + }) }) + +// mockPlayTrackerForAlbumLists is a minimal mock implementing scrobbler.PlayTracker for GetNowPlaying tests +type mockPlayTrackerForAlbumLists struct { + NowPlayingData []scrobbler.NowPlayingInfo + Error error +} + +func (m *mockPlayTrackerForAlbumLists) NowPlaying(_ context.Context, _ string, _ string, _ string, _ int) error { + return m.Error +} + +func (m *mockPlayTrackerForAlbumLists) GetNowPlaying(_ context.Context) ([]scrobbler.NowPlayingInfo, error) { + if m.Error != nil { + return nil, m.Error + } + return m.NowPlayingData, nil +} + +func (m *mockPlayTrackerForAlbumLists) Submit(_ context.Context, _ []scrobbler.Submission) error { + return m.Error +} + +var _ scrobbler.PlayTracker = (*mockPlayTrackerForAlbumLists)(nil)