mirror of
https://github.com/navidrome/navidrome.git
synced 2026-11-01 07:57:56 +01:00
fix(jellyfin): resolve library-view ids in getItem so clients can load the library
This commit is contained in:
parent
da51d3f1c6
commit
4e277b1929
4 changed files with 80 additions and 12 deletions
|
|
@ -3,6 +3,7 @@ package jellyfin
|
|||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/Masterminds/squirrel"
|
||||
|
|
@ -233,13 +234,30 @@ func (api *Router) listPlaylists(ctx context.Context, opts model.QueryOptions, f
|
|||
return result(slice.Map(playlists, dto.PlaylistToBaseItem), int(total), opts.Offset), nil
|
||||
}
|
||||
|
||||
// getItem fetches a single entity by id, trying album, artist and song in turn. For albums
|
||||
// and songs (which each belong to exactly one library) it 404s if the current user lacks
|
||||
// access to that library, so an id can't be used to probe content outside the user's libraries.
|
||||
// getItem fetches a single entity by id, trying library view, album, artist and song in turn.
|
||||
// For albums and songs (which each belong to exactly one library) it 404s if the current user
|
||||
// lacks access to that library, so an id can't be used to probe content outside the user's
|
||||
// libraries.
|
||||
func (api *Router) getItem(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
id := chi.URLParam(r, "itemId")
|
||||
u, _ := request.UserFrom(ctx)
|
||||
// Finamp resolves a /UserViews entry (Id=library id) by fetching it as a plain item; without
|
||||
// this, the home screen and every library tab 404 trying to probe it as an album/artist/song.
|
||||
if libID, err := strconv.Atoi(id); err == nil && u.HasLibraryAccess(libID) {
|
||||
for _, lib := range u.Libraries {
|
||||
if lib.ID == libID {
|
||||
api.ok(w, r, libraryView(lib))
|
||||
return
|
||||
}
|
||||
}
|
||||
// Admin bypass: Libraries is empty but access is granted to every library, so fetch the
|
||||
// real one instead of returning a placeholder.
|
||||
if lib, err := api.ds.Library(ctx).Get(libID); err == nil {
|
||||
api.ok(w, r, libraryView(*lib))
|
||||
return
|
||||
}
|
||||
}
|
||||
if al, err := api.ds.Album(ctx).Get(id); err == nil {
|
||||
if !u.HasLibraryAccess(al.LibraryID) {
|
||||
http.Error(w, "Not Found", http.StatusNotFound)
|
||||
|
|
|
|||
|
|
@ -400,6 +400,47 @@ var _ = Describe("Items", func() {
|
|||
Expect(json.Unmarshal(w.Body.Bytes(), &item)).To(Succeed())
|
||||
Expect(item.Id).To(Equal("a1"))
|
||||
})
|
||||
|
||||
// Finamp fetches a /UserViews entry (Id=library id) as a plain item to resolve the
|
||||
// library node before it can load the home screen or any library tab.
|
||||
It("resolves a library-view id (from /UserViews) as a CollectionFolder item", func() {
|
||||
w := httptest.NewRecorder()
|
||||
libs := model.Libraries{{ID: 1, Name: "Music Library"}}
|
||||
r := httptest.NewRequest("GET", "/Items/1", nil).WithContext(ctxUserWithLibraries(libs))
|
||||
r = withChiURLParam(r, "itemId", "1")
|
||||
api.getItem(w, r)
|
||||
Expect(w.Code).To(Equal(http.StatusOK))
|
||||
var item dto.BaseItemDto
|
||||
Expect(json.Unmarshal(w.Body.Bytes(), &item)).To(Succeed())
|
||||
Expect(item.Id).To(Equal("1"))
|
||||
Expect(item.Name).To(Equal("Music Library"))
|
||||
Expect(item.Type).To(Equal("CollectionFolder"))
|
||||
Expect(item.CollectionType).To(Equal("music"))
|
||||
Expect(item.IsFolder).To(BeTrue())
|
||||
})
|
||||
|
||||
It("does not resolve a library-view id the user has no access to", func() {
|
||||
w := httptest.NewRecorder()
|
||||
libs := model.Libraries{{ID: 2, Name: "Other"}} // no access to library 1
|
||||
r := httptest.NewRequest("GET", "/Items/1", nil).WithContext(ctxUserWithLibraries(libs))
|
||||
r = withChiURLParam(r, "itemId", "1")
|
||||
api.getItem(w, r)
|
||||
Expect(w.Code).To(Equal(http.StatusNotFound))
|
||||
})
|
||||
|
||||
It("resolves a library-view id for an admin even though their Libraries slice is empty", func() {
|
||||
ds.Library(context.Background()).(*tests.MockLibraryRepo).SetData(model.Libraries{{ID: 1, Name: "Music Library"}})
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("GET", "/Items/1", nil).WithContext(ctxAdmin())
|
||||
r = withChiURLParam(r, "itemId", "1")
|
||||
api.getItem(w, r)
|
||||
Expect(w.Code).To(Equal(http.StatusOK))
|
||||
var item dto.BaseItemDto
|
||||
Expect(json.Unmarshal(w.Body.Bytes(), &item)).To(Succeed())
|
||||
Expect(item.Id).To(Equal("1"))
|
||||
Expect(item.Name).To(Equal("Music Library"))
|
||||
Expect(item.Type).To(Equal("CollectionFolder"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("getLatest", func() {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,9 @@ import (
|
|||
"context"
|
||||
"strconv"
|
||||
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/model/request"
|
||||
"github.com/navidrome/navidrome/server/jellyfin/dto"
|
||||
)
|
||||
|
||||
// accessibleLibraryIDs returns the ids of the libraries the current user can access. An
|
||||
|
|
@ -30,3 +32,18 @@ func resolveLibraryScope(ctx context.Context, parentId string) (scopeIDs []int,
|
|||
}
|
||||
return accessibleLibraryIDs(ctx), false
|
||||
}
|
||||
|
||||
// libraryView builds the CollectionFolder BaseItemDto Jellyfin clients use to represent a
|
||||
// library as a top-level browsing node. It's shared by getUserViews (which lists every
|
||||
// accessible library) and getItem (which resolves a single one), since Finamp fetches a
|
||||
// UserView's id as a plain item to load the library.
|
||||
func libraryView(lib model.Library) dto.BaseItemDto {
|
||||
return dto.BaseItemDto{
|
||||
Id: strconv.Itoa(lib.ID),
|
||||
Name: lib.Name,
|
||||
Type: "CollectionFolder",
|
||||
CollectionType: "music",
|
||||
IsFolder: true,
|
||||
BackdropImageTags: []string{},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package jellyfin
|
|||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/navidrome/navidrome/model/request"
|
||||
"github.com/navidrome/navidrome/server/jellyfin/dto"
|
||||
|
|
@ -15,14 +14,7 @@ func (api *Router) getUserViews(w http.ResponseWriter, r *http.Request) {
|
|||
u, _ := request.UserFrom(r.Context())
|
||||
views := make([]dto.BaseItemDto, 0, len(u.Libraries))
|
||||
for _, lib := range u.Libraries {
|
||||
views = append(views, dto.BaseItemDto{
|
||||
Id: strconv.Itoa(lib.ID),
|
||||
Name: lib.Name,
|
||||
Type: "CollectionFolder",
|
||||
CollectionType: "music",
|
||||
IsFolder: true,
|
||||
BackdropImageTags: []string{},
|
||||
})
|
||||
views = append(views, libraryView(lib))
|
||||
}
|
||||
api.ok(w, r, dto.QueryResult{Items: views, TotalRecordCount: len(views), StartIndex: 0})
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue