mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-14 05:17:13 +02:00
fix(jellyfin): add the /Items/Latest route, and scope it by ParentId (#6090)
Jellify's Discover tab calls GET /Items/Latest and got a 404: we only routed the
/Users/{userId}/Items/Latest form, which real Jellyfin marks [Obsolete] and hides
from its OpenAPI spec, so SDK-generated clients never see it. Add the current
route alongside the legacy one, both served by the same handler.
getLatest also ignored ParentId, so browsing a library or an artist returned the
newest albums across everything the user can see. It now scopes to the library
when ParentId names one, and filters to that artist's albums otherwise, which
also makes a stale id return nothing instead of silently widening back to the
full library set. A malformed ParentId 404s, matching /Items and the contract
decodeFilterParam documents.
This commit is contained in:
parent
546302576a
commit
c534aedd0c
3 changed files with 50 additions and 1 deletions
|
|
@ -118,6 +118,7 @@ func (api *Router) routes() http.Handler {
|
|||
r.Use(throttleStreams(conf.Server.Jellyfin.MaxConcurrentStreams))
|
||||
r.Get("/items", api.getItems)
|
||||
r.Get("/users/{userId}/items", api.getItems)
|
||||
r.Get("/items/latest", api.getLatest)
|
||||
r.Get("/users/{userId}/items/latest", api.getLatest)
|
||||
r.Get("/artists", api.getArtists)
|
||||
r.Get("/artists/albumartists", api.getAlbumArtists)
|
||||
|
|
|
|||
|
|
@ -563,6 +563,43 @@ var _ = Describe("Browsing", func() {
|
|||
})
|
||||
})
|
||||
|
||||
Describe("GET /Items/Latest", func() {
|
||||
// Jellyfin marks the /Users/{userId} form obsolete and hides it from the OpenAPI spec, so
|
||||
// SDK-generated clients (Jellify) only ever call this one.
|
||||
It("serves the same response as the legacy /Users/{userId} route", func() {
|
||||
Expect(get("/Items/Latest?Limit=3").Body.String()).
|
||||
To(Equal(get("/Users/admin-1/Items/Latest?Limit=3").Body.String()))
|
||||
})
|
||||
|
||||
It("scopes to ParentId when it names a library", func() {
|
||||
var items []dto.BaseItemDto
|
||||
parseInto(get("/Items/Latest?ParentId="+dto.EncodeLibraryID(1)), &items)
|
||||
Expect(names(items)).To(ConsistOf("Abbey Road", "Help!", "IV", "Kind of Blue", "Singles"))
|
||||
})
|
||||
|
||||
It("scopes to ParentId when it names an artist", func() {
|
||||
var items []dto.BaseItemDto
|
||||
parseInto(get("/Items/Latest?ParentId="+enc(artistID("The Beatles"))), &items)
|
||||
Expect(names(items)).To(ConsistOf("Abbey Road", "Help!"))
|
||||
})
|
||||
|
||||
It("returns nothing for a library the user cannot access", func() {
|
||||
var items []dto.BaseItemDto
|
||||
parseInto(get("/Items/Latest?ParentId="+dto.EncodeLibraryID(99)), &items)
|
||||
Expect(items).To(BeEmpty())
|
||||
})
|
||||
|
||||
It("returns nothing for an id that is neither a library nor an artist", func() {
|
||||
var items []dto.BaseItemDto
|
||||
parseInto(get("/Items/Latest?ParentId="+enc(testID("does-not-exist"))), &items)
|
||||
Expect(items).To(BeEmpty())
|
||||
})
|
||||
|
||||
It("404s a malformed ParentId, like every other filtered endpoint", func() {
|
||||
Expect(get("/Items/Latest?ParentId=not-a-valid-id").Code).To(Equal(http.StatusNotFound))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("GET /Artists and /Genres", func() {
|
||||
It("lists album artists only on /Artists/AlbumArtists (excludes performer-only artists)", func() {
|
||||
names := names(queryResult(get("/Artists/AlbumArtists")).Items)
|
||||
|
|
|
|||
|
|
@ -938,7 +938,18 @@ func (api *Router) getLatest(w http.ResponseWriter, r *http.Request) {
|
|||
fields := dto.ParseFields(p.Strings("fields")...)
|
||||
opts := filter.AlbumsByNewest()
|
||||
opts.Max = p.IntOr("limit", 20)
|
||||
opts = filter.ApplyLibraryFilter(opts, accessibleLibraryIDs(ctx))
|
||||
parentID, ok := decodeFilterParam(p.StringOr("parentid", ""))
|
||||
if !ok {
|
||||
http.Error(w, "Not Found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
// A ParentId naming neither a library nor an artist (a stale id, an album) narrows to nothing
|
||||
// rather than widening back to every library.
|
||||
scopeIDs, isLibrary := resolveLibraryScope(ctx, parentID)
|
||||
if parentID != "" && !isLibrary {
|
||||
opts.Filters = squirrel.And{opts.Filters, filter.AlbumsByArtistID(parentID).Filters}
|
||||
}
|
||||
opts = filter.ApplyLibraryFilter(opts, scopeIDs)
|
||||
repo := api.ds.Album(ctx)
|
||||
open := streamCursor(func() (func(func(model.Album, error) bool), error) {
|
||||
return repo.GetCursor(opts)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue