fix(jellyfin): let net/http sniff image Content-Type instead of forcing jpeg

This commit is contained in:
Deluan 2026-07-05 16:27:25 -04:00
commit 926a36acfa
2 changed files with 42 additions and 10 deletions

View file

@ -2,11 +2,13 @@ package jellyfin
import (
"context"
"errors"
"io"
"net/http"
"strconv"
"github.com/go-chi/chi/v5"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
)
@ -17,12 +19,17 @@ func (api *Router) getItemImage(w http.ResponseWriter, r *http.Request) {
artID := api.resolveArtworkID(ctx, itemId)
reader, _, err := api.artwork.GetOrPlaceholder(ctx, artID, size, false)
if err != nil {
switch {
case errors.Is(err, context.Canceled):
return
case err != nil:
log.Warn(ctx, "Error retrieving artwork", "id", itemId, err)
http.Error(w, "Not Found", http.StatusNotFound)
return
}
defer reader.Close()
w.Header().Set("Content-Type", "image/jpeg")
// Content-Type is left unset so net/http sniffs it from the first bytes written
// (placeholders are WebP; unresized covers can be PNG/WebP/etc, not always JPEG).
_, _ = io.Copy(w, reader)
}

View file

@ -1,11 +1,11 @@
package jellyfin
import (
"bytes"
"context"
"io"
"net/http"
"net/http/httptest"
"strings"
"time"
"github.com/go-chi/chi/v5"
@ -19,11 +19,26 @@ import (
type fakeArtwork struct {
artwork.Artwork
recvId string
data []byte
}
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
data := f.data
if data == nil {
data = []byte("IMG")
}
return io.NopCloser(bytes.NewReader(data)), time.Now(), nil
}
func newImageRequest(itemId string) (*httptest.ResponseRecorder, *http.Request) {
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/Items/"+itemId+"/Images/Primary", nil)
rctx := chi.NewRouteContext()
rctx.URLParams.Add("itemId", itemId)
rctx.URLParams.Add("type", "Primary")
r = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, rctx))
return w, r
}
var _ = Describe("Images", func() {
@ -33,16 +48,26 @@ var _ = Describe("Images", func() {
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))
w, r := newImageRequest("a1")
api.getItemImage(w, r)
Expect(w.Code).To(Equal(http.StatusOK))
Expect(w.Body.String()).To(Equal("IMG"))
Expect(fa.recvId).To(ContainSubstring("a1"))
})
It("sniffs the Content-Type instead of hardcoding it", func() {
ds := &tests.MockDataStore{}
ds.Album(context.Background()).(*tests.MockAlbumRepo).SetData(model.Albums{{ID: "a1", Name: "One"}})
png := append([]byte{0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n'}, make([]byte, 512)...)
fa := &fakeArtwork{data: png}
api := &Router{ds: ds, artwork: fa}
w, r := newImageRequest("a1")
api.getItemImage(w, r)
Expect(w.Code).To(Equal(http.StatusOK))
Expect(w.Header().Get("Content-Type")).To(Equal("image/png"))
})
})