mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-31 07:30:32 +00:00
Implement folder-based getIndexes per Subsonic spec
Signed-off-by: Patrik Wallström <pawal@amplitut.de>
This commit is contained in:
parent
03f43c1f60
commit
235632f3e5
@ -4,6 +4,8 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
@ -99,10 +101,13 @@ func (api *Router) getArtistIndexID3(r *http.Request, libIds []int, ifModifiedSi
|
||||
|
||||
func (api *Router) GetIndexes(r *http.Request) (*responses.Subsonic, error) {
|
||||
p := req.Params(r)
|
||||
musicFolderIds, _ := selectedMusicFolderIds(r, false)
|
||||
musicFolderIds, err := selectedMusicFolderIds(r, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ifModifiedSince := p.TimeOr("ifModifiedSince", time.Time{})
|
||||
|
||||
res, err := api.getArtistIndex(r, musicFolderIds, ifModifiedSince)
|
||||
res, err := api.getFolderIndex(r.Context(), musicFolderIds, ifModifiedSince)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -112,6 +117,85 @@ func (api *Router) GetIndexes(r *http.Request) (*responses.Subsonic, error) {
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (api *Router) getFolderIndex(ctx context.Context, musicFolderIds []int, ifModifiedSince time.Time) (*responses.Indexes, error) {
|
||||
lastScanStr, err := api.ds.Property(ctx).DefaultGet(consts.LastScanStartTimeKey, "")
|
||||
if err != nil {
|
||||
log.Error(ctx, "Error retrieving last scan start time", err)
|
||||
return nil, err
|
||||
}
|
||||
lastScan := time.Now()
|
||||
if lastScanStr != "" {
|
||||
if t, parseErr := time.Parse(time.RFC3339, lastScanStr); parseErr == nil {
|
||||
lastScan = t
|
||||
}
|
||||
}
|
||||
|
||||
res := &responses.Indexes{
|
||||
IgnoredArticles: conf.Server.IgnoredArticles,
|
||||
LastModified: lastScan.UnixMilli(),
|
||||
}
|
||||
|
||||
if !lastScan.After(ifModifiedSince) {
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// Filter accessible libraries down to the requested ones
|
||||
libIdSet := make(map[int]bool, len(musicFolderIds))
|
||||
for _, id := range musicFolderIds {
|
||||
libIdSet[id] = true
|
||||
}
|
||||
var libraries []model.Library
|
||||
for _, lib := range getUserAccessibleLibraries(ctx) {
|
||||
if len(libIdSet) == 0 || libIdSet[lib.ID] {
|
||||
libraries = append(libraries, lib)
|
||||
}
|
||||
}
|
||||
|
||||
// Collect top-level folders (direct children of each library's root)
|
||||
var allFolders []model.Folder
|
||||
for _, lib := range libraries {
|
||||
rootID := model.FolderID(lib, ".")
|
||||
folders, err := api.ds.Folder(ctx).GetAll(filter.FoldersByParent(rootID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
allFolders = append(allFolders, folders...)
|
||||
}
|
||||
|
||||
// Sort by name (case-insensitive)
|
||||
sort.Slice(allFolders, func(i, j int) bool {
|
||||
return strings.ToLower(allFolders[i].Name) < strings.ToLower(allFolders[j].Name)
|
||||
})
|
||||
|
||||
// Group by first letter; non-alpha goes under "#"
|
||||
indexMap := make(map[string]*responses.Index)
|
||||
var indexOrder []string
|
||||
for _, f := range allFolders {
|
||||
letter := "#"
|
||||
if runes := []rune(f.Name); len(runes) > 0 {
|
||||
up := strings.ToUpper(string(runes[0]))
|
||||
if up >= "A" && up <= "Z" {
|
||||
letter = up
|
||||
}
|
||||
}
|
||||
if _, exists := indexMap[letter]; !exists {
|
||||
indexMap[letter] = &responses.Index{Name: letter}
|
||||
indexOrder = append(indexOrder, letter)
|
||||
}
|
||||
indexMap[letter].Artists = append(indexMap[letter].Artists, responses.Artist{
|
||||
Id: f.ID,
|
||||
Name: f.Name,
|
||||
CoverArt: f.CoverArtID().String(),
|
||||
})
|
||||
}
|
||||
|
||||
res.Index = make([]responses.Index, len(indexOrder))
|
||||
for i, letter := range indexOrder {
|
||||
res.Index[i] = *indexMap[letter]
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (api *Router) GetArtists(r *http.Request) (*responses.Subsonic, error) {
|
||||
musicFolderIds, _ := selectedMusicFolderIds(r, false)
|
||||
|
||||
|
||||
@ -190,51 +190,77 @@ var _ = Describe("Browsing", func() {
|
||||
})
|
||||
|
||||
Describe("GetIndexes", func() {
|
||||
It("should validate user access to the specified musicFolderId", func() {
|
||||
// Create mock user with access to library 1 only
|
||||
var lib1 model.Library
|
||||
|
||||
BeforeEach(func() {
|
||||
lib1 = model.Library{ID: 1, Name: "Test Library 1", Path: "/music/library1"}
|
||||
})
|
||||
|
||||
It("should return error when musicFolderId is not accessible", func() {
|
||||
ctx = contextWithUser(ctx, "user-id", 1)
|
||||
|
||||
// Create request with musicFolderId=2 (not accessible)
|
||||
r := httptest.NewRequest("GET", "/rest/getIndexes?musicFolderId=2", nil)
|
||||
r = r.WithContext(ctx)
|
||||
|
||||
// Call endpoint
|
||||
response, err := api.GetIndexes(r)
|
||||
|
||||
// Should return error due to lack of access
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(response).To(BeNil())
|
||||
})
|
||||
|
||||
It("should default to first accessible library when no musicFolderId specified", func() {
|
||||
// Create mock user with access to libraries 2 and 3
|
||||
ctx = contextWithUser(ctx, "user-id", 2, 3)
|
||||
It("returns an empty index when the library has no top-level folders", func() {
|
||||
ctx = contextWithUser(ctx, "user-id", 1)
|
||||
// Folder repo returns nothing — no top-level folders
|
||||
|
||||
// Setup minimal mock library data for working tests
|
||||
mockLibRepo := ds.Library(ctx).(*tests.MockLibraryRepo)
|
||||
mockLibRepo.SetData(model.Libraries{
|
||||
{ID: 2, Name: "Test Library 2", Path: "/music/library2"},
|
||||
{ID: 3, Name: "Test Library 3", Path: "/music/library3"},
|
||||
})
|
||||
|
||||
// Setup mock artist data
|
||||
mockArtistRepo := ds.Artist(ctx).(*tests.MockArtistRepo)
|
||||
mockArtistRepo.SetData(model.Artists{
|
||||
{ID: "1", Name: "Test Artist 1"},
|
||||
{ID: "2", Name: "Test Artist 2"},
|
||||
})
|
||||
|
||||
// Create request without musicFolderId
|
||||
r := httptest.NewRequest("GET", "/rest/getIndexes", nil)
|
||||
r = r.WithContext(ctx)
|
||||
|
||||
// Call endpoint
|
||||
response, err := api.GetIndexes(r)
|
||||
|
||||
// Should succeed and use first accessible library (2)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response).ToNot(BeNil())
|
||||
Expect(response.Indexes).ToNot(BeNil())
|
||||
Expect(response.Indexes.Index).To(BeEmpty())
|
||||
})
|
||||
|
||||
It("returns top-level folders grouped by first letter", func() {
|
||||
ctx = contextWithUser(ctx, "user-id", 1)
|
||||
rootID := model.FolderID(lib1, ".")
|
||||
ds.Folder(ctx).(*tests.MockFolderRepo).SetData([]model.Folder{
|
||||
{ID: "f-jazz", ParentID: rootID, Name: "Jazz"},
|
||||
{ID: "f-rock", ParentID: rootID, Name: "Rock"},
|
||||
{ID: "f-blues", ParentID: rootID, Name: "Blues"},
|
||||
})
|
||||
|
||||
r := httptest.NewRequest("GET", "/rest/getIndexes", nil)
|
||||
r = r.WithContext(ctx)
|
||||
|
||||
response, err := api.GetIndexes(r)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response.Indexes.Index).To(HaveLen(3)) // B, J, R
|
||||
// Find the "J" index
|
||||
Expect(response.Indexes.Index).To(ContainElement(
|
||||
And(
|
||||
HaveField("Name", "J"),
|
||||
HaveField("Artists", ContainElement(
|
||||
And(HaveField("Id", "f-jazz"), HaveField("Name", "Jazz")),
|
||||
)),
|
||||
),
|
||||
))
|
||||
})
|
||||
|
||||
It("groups non-alpha folder names under '#'", func() {
|
||||
ctx = contextWithUser(ctx, "user-id", 1)
|
||||
rootID := model.FolderID(lib1, ".")
|
||||
ds.Folder(ctx).(*tests.MockFolderRepo).SetData([]model.Folder{
|
||||
{ID: "f-90s", ParentID: rootID, Name: "90s Hip-Hop"},
|
||||
})
|
||||
|
||||
r := httptest.NewRequest("GET", "/rest/getIndexes", nil)
|
||||
r = r.WithContext(ctx)
|
||||
|
||||
response, err := api.GetIndexes(r)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response.Indexes.Index).To(HaveLen(1))
|
||||
Expect(response.Indexes.Index[0].Name).To(Equal("#"))
|
||||
Expect(response.Indexes.Index[0].Artists[0].Id).To(Equal("f-90s"))
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user