feat(jellyfin): item image endpoint via artwork service

This commit is contained in:
Deluan 2026-07-05 16:20:39 -04:00
commit 6e4a180feb
3 changed files with 95 additions and 0 deletions

View file

@ -50,6 +50,11 @@ func (api *Router) routes() http.Handler {
r.Post("/Users/AuthenticateByName", api.authenticateByName)
r.Get("/Users/Public", api.getPublicUsers)
// Images are intentionally public and not library-scoped: artwork isn't sensitive media
// content, and clients (e.g. Finamp) load it via <img> tags with only ?api_key= in the URL.
r.Get("/Items/{itemId}/Images/{type}", api.getItemImage)
r.Get("/Items/{itemId}/Images/{type}/{index}", api.getItemImage)
r.Group(func(r chi.Router) {
r.Use(api.authenticate)
r.Get("/UserViews", api.getUserViews)

42
server/jellyfin/images.go Normal file
View file

@ -0,0 +1,42 @@
package jellyfin
import (
"context"
"io"
"net/http"
"strconv"
"github.com/go-chi/chi/v5"
"github.com/navidrome/navidrome/model"
)
func (api *Router) getItemImage(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
itemId := chi.URLParam(r, "itemId")
size, _ := strconv.Atoi(r.URL.Query().Get("maxWidth"))
artID := api.resolveArtworkID(ctx, itemId)
reader, _, err := api.artwork.GetOrPlaceholder(ctx, artID, size, false)
if err != nil {
http.Error(w, "Not Found", http.StatusNotFound)
return
}
defer reader.Close()
w.Header().Set("Content-Type", "image/jpeg")
_, _ = io.Copy(w, reader)
}
// resolveArtworkID maps a bare Jellyfin item id to a Navidrome ArtworkID string
// by probing album -> artist -> media file (music items only).
func (api *Router) resolveArtworkID(ctx context.Context, itemId string) string {
if al, err := api.ds.Album(ctx).Get(itemId); err == nil {
return al.CoverArtID().String()
}
if ar, err := api.ds.Artist(ctx).Get(itemId); err == nil {
return ar.CoverArtID().String()
}
if mf, err := api.ds.MediaFile(ctx).Get(itemId); err == nil {
return mf.CoverArtID().String()
}
return (model.ArtworkID{}).String()
}

View file

@ -0,0 +1,48 @@
package jellyfin
import (
"context"
"io"
"net/http"
"net/http/httptest"
"strings"
"time"
"github.com/go-chi/chi/v5"
"github.com/navidrome/navidrome/core/artwork"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/tests"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
type fakeArtwork struct {
artwork.Artwork
recvId string
}
func (f *fakeArtwork) GetOrPlaceholder(ctx context.Context, id string, size int, square bool) (io.ReadCloser, time.Time, error) {
f.recvId = id
return io.NopCloser(strings.NewReader("IMG")), time.Now(), nil
}
var _ = Describe("Images", func() {
It("streams album artwork", 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 := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/Items/a1/Images/Primary", nil)
rctx := chi.NewRouteContext()
rctx.URLParams.Add("itemId", "a1")
rctx.URLParams.Add("type", "Primary")
r = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, rctx))
api.getItemImage(w, r)
Expect(w.Code).To(Equal(http.StatusOK))
Expect(w.Body.String()).To(Equal("IMG"))
Expect(fa.recvId).To(ContainSubstring("a1"))
})
})