fix(jellyfin): display playlist cover art

Uploaded playlist covers never showed in clients for two reasons: the
playlist BaseItemDto advertised no Primary ImageTag (so clients didn't
know to fetch a cover), and the public image endpoint resolved artwork
under the request's anonymous context, so a private playlist failed its
visibility filter and fell back to the placeholder. Advertise the Primary
image tag/blurhash on playlists, and resolve artwork under an elevated
context (as core/artwork's cache warmer does).
This commit is contained in:
Deluan 2026-07-05 23:22:37 -04:00
commit d24ba9b5ea
4 changed files with 34 additions and 3 deletions

View file

@ -191,6 +191,8 @@ func PlaylistToBaseItem(p model.Playlist) BaseItemDto {
MediaType: "Audio",
ChildCount: new(p.SongCount),
RunTimeTicks: TicksFromSeconds(p.Duration),
ImageTags: map[string]string{"Primary": p.ID},
ImageBlurHashes: map[string]map[string]string{"Primary": {p.ID: blurHash(p.ID)}},
BackdropImageTags: []string{},
}
}

View file

@ -166,5 +166,8 @@ var _ = Describe("mappers", func() {
Expect(item.MediaType).To(Equal("Audio"))
Expect(*item.ChildCount).To(Equal(7))
Expect(item.RunTimeTicks).To(Equal(int64(1_200_000_000)))
Expect(item.ImageTags["Primary"]).To(Equal("pl-1"))
Expect(item.ImageBlurHashes["Primary"]).To(HaveKey("pl-1"))
Expect(item.ImageBlurHashes["Primary"]["pl-1"]).To(HaveLen(6))
})
})

View file

@ -13,11 +13,16 @@ import (
"github.com/go-chi/chi/v5"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/request"
"github.com/navidrome/navidrome/server/jellyfin/dto"
)
func (api *Router) getItemImage(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// This endpoint is intentionally public (see routes), so the request carries no user. Artwork
// resolution for user-scoped items (private playlists) would otherwise fail its visibility
// filter and fall back to the placeholder, so resolve under an elevated context — the same
// approach core/artwork's cache warmer uses to pre-generate every item's cover.
ctx := request.WithUser(r.Context(), model.User{IsAdmin: true})
itemId := dto.DecodeID(chi.URLParam(r, "itemId"))
size, _ := strconv.Atoi(r.URL.Query().Get("maxWidth"))

View file

@ -13,6 +13,7 @@ import (
"github.com/go-chi/chi/v5"
"github.com/navidrome/navidrome/core/artwork"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/request"
"github.com/navidrome/navidrome/server/jellyfin/dto"
"github.com/navidrome/navidrome/tests"
. "github.com/onsi/ginkgo/v2"
@ -21,12 +22,14 @@ import (
type fakeArtwork struct {
artwork.Artwork
recvId string
data []byte
recvId string
recvCtx context.Context
data []byte
}
func (f *fakeArtwork) GetOrPlaceholder(ctx context.Context, id string, size int, square bool) (io.ReadCloser, time.Time, error) {
f.recvId = id
f.recvCtx = ctx
data := f.data
if data == nil {
data = []byte("IMG")
@ -86,6 +89,24 @@ var _ = Describe("Images", func() {
Expect(w.Code).To(Equal(http.StatusOK))
Expect(fa.recvId).To(ContainSubstring("pl1"))
})
// This endpoint is public (no user in the request), so artwork must be resolved under an
// elevated context; otherwise a private playlist's cover fails its visibility filter and
// silently falls back to the placeholder.
It("resolves artwork under an elevated admin context", func() {
ds := &tests.MockDataStore{}
ds.Album(context.Background()).(*tests.MockAlbumRepo).SetData(model.Albums{{ID: "a1", Name: "One"}})
fa := &fakeArtwork{}
api := &Router{ds: ds, artwork: fa}
w, r := newImageRequest(dto.EncodeID("a1"))
api.getItemImage(w, r)
Expect(w.Code).To(Equal(http.StatusOK))
u, ok := request.UserFrom(fa.recvCtx)
Expect(ok).To(BeTrue())
Expect(u.IsAdmin).To(BeTrue())
})
})
var _ = Describe("postItemImage", func() {