feat: filter NowPlaying entries by user's accessible libraries

Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
Deluan 2025-12-15 19:57:01 -05:00
parent 27d81ffd96
commit 7c13c8182a
2 changed files with 142 additions and 7 deletions

View File

@ -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
}

View File

@ -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)