From 8f82a8deaa3b827d9815565874d79d29162a9c0b Mon Sep 17 00:00:00 2001 From: Deluan Date: Fri, 31 Jul 2026 18:42:27 -0400 Subject: [PATCH] refactor(model): keep the blurhash off native JSON and fold its specs Nothing on the native API consumes blurHash: the web UI reads thumbHash, and Jellyfin's mappers take the Go field directly rather than through this serialization. It was shipping ~40 unused bytes on every row of every list response, on a surface that accretes clients once published. The JSON specs were five marshal-and-check-keys blocks over the same struct; they collapse to one populated case, one bare case, and a guard that the blurhash stays off the wire. --- model/artwork.go | 7 ++-- model/artwork_test.go | 95 ++++++++++++++++--------------------------- 2 files changed, 38 insertions(+), 64 deletions(-) diff --git a/model/artwork.go b/model/artwork.go index 37e13dbd8..4a0ed007b 100644 --- a/model/artwork.go +++ b/model/artwork.go @@ -20,9 +20,10 @@ const ImageTypePrimary = "primary" type ItemImage struct { ImageHash string `structs:"-" json:"imageHash,omitempty"` ImageAbsent bool `structs:"-" json:"imageAbsent,omitempty"` - BlurHash string `structs:"-" json:"blurHash,omitempty"` - ThumbHash string `structs:"-" json:"thumbHash,omitempty"` - // A blurhash carries no aspect ratio, so clients need these to shape the placeholder. + // BlurHash is Jellyfin's; its mappers read this field directly, so it stays off native JSON. + BlurHash string `structs:"-" json:"-"` + ThumbHash string `structs:"-" json:"thumbHash,omitempty"` + // A thumbhash's own aspect is quantised, so clients need these to shape the placeholder exactly. ImageWidth int `structs:"-" json:"imageWidth,omitempty"` ImageHeight int `structs:"-" json:"imageHeight,omitempty"` } diff --git a/model/artwork_test.go b/model/artwork_test.go index 2c755e095..baf741e68 100644 --- a/model/artwork_test.go +++ b/model/artwork_test.go @@ -8,64 +8,49 @@ import ( . "github.com/onsi/gomega" ) +func jsonOf(v any) map[string]any { + GinkgoHelper() + data, err := json.Marshal(v) + Expect(err).ToNot(HaveOccurred()) + var out map[string]any + Expect(json.Unmarshal(data, &out)).To(Succeed()) + return out +} + var _ = Describe("ItemImage JSON", func() { - It("exposes artwork state on an album", func() { + It("exposes the artwork state a client needs to render a placeholder", func() { al := model.Album{ID: "al-1", Name: "Album"} al.ImageHash = "0123456789abcdef" - al.BlurHash = "LEHV6nWB2yk8" - - var out map[string]any - data, err := json.Marshal(al) - Expect(err).ToNot(HaveOccurred()) - Expect(json.Unmarshal(data, &out)).To(Succeed()) - - Expect(out).To(HaveKeyWithValue("imageHash", "0123456789abcdef")) - Expect(out).To(HaveKeyWithValue("blurHash", "LEHV6nWB2yk8")) - }) - - It("exposes the thumbhash, and omits it when the entity has none", func() { - al := model.Album{ID: "al-4", Name: "Album"} al.ThumbHash = "1QcSHQRn" - - var out map[string]any - data, err := json.Marshal(al) - Expect(err).ToNot(HaveOccurred()) - Expect(json.Unmarshal(data, &out)).To(Succeed()) - Expect(out).To(HaveKeyWithValue("thumbHash", "1QcSHQRn")) - - var bare map[string]any - data, err = json.Marshal(model.Album{ID: "al-5", Name: "Album"}) - Expect(err).ToNot(HaveOccurred()) - Expect(json.Unmarshal(data, &bare)).To(Succeed()) - Expect(bare).ToNot(HaveKey("thumbHash")) - }) - - // Without the dimensions, clients cannot know the placeholder's shape and default to a square. - It("exposes the image dimensions alongside the blurhash", func() { - al := model.Album{ID: "al-3", Name: "Album"} - al.BlurHash = "LEHV6nWB2yk8" al.ImageWidth, al.ImageHeight = 1200, 800 - var out map[string]any - data, err := json.Marshal(al) - Expect(err).ToNot(HaveOccurred()) - Expect(json.Unmarshal(data, &out)).To(Succeed()) - - Expect(out).To(HaveKeyWithValue("imageWidth", BeNumerically("==", 1200))) - Expect(out).To(HaveKeyWithValue("imageHeight", BeNumerically("==", 800))) + Expect(jsonOf(al)).To(SatisfyAll( + HaveKeyWithValue("imageHash", "0123456789abcdef"), + HaveKeyWithValue("thumbHash", "1QcSHQRn"), + HaveKeyWithValue("imageWidth", BeNumerically("==", 1200)), + HaveKeyWithValue("imageHeight", BeNumerically("==", 800)), + )) }) - It("omits artwork state when the entity has none", func() { - var out map[string]any - data, err := json.Marshal(model.Album{ID: "al-2", Name: "Album"}) - Expect(err).ToNot(HaveOccurred()) - Expect(json.Unmarshal(data, &out)).To(Succeed()) + It("keeps the blurhash off the native API, where nothing consumes it", func() { + al := model.Album{ID: "al-1", Name: "Album"} + al.BlurHash = "LEHV6nWB2yk8" + Expect(jsonOf(al)).ToNot(HaveKey("blurHash")) + }) - Expect(out).ToNot(HaveKey("imageHash")) - Expect(out).ToNot(HaveKey("blurHash")) - Expect(out).ToNot(HaveKey("imageAbsent")) - Expect(out).ToNot(HaveKey("imageWidth")) - Expect(out).ToNot(HaveKey("imageHeight")) + It("omits every artwork field when the entity has none", func() { + out := jsonOf(model.Album{ID: "al-2", Name: "Album"}) + for _, key := range []string{ + "imageHash", "blurHash", "thumbHash", "imageAbsent", "imageWidth", "imageHeight", + } { + Expect(out).ToNot(HaveKey(key)) + } + }) + + It("exposes known-absent artwork so clients can skip the request", func() { + ar := model.Artist{ID: "ar-1", Name: "Artist"} + ar.ImageAbsent = true + Expect(jsonOf(ar)).To(HaveKeyWithValue("imageAbsent", true)) }) Describe("AspectRatio", func() { @@ -85,16 +70,4 @@ var _ = Describe("ItemImage JSON", func() { Expect(img.AspectRatio()).To(BeNil()) }) }) - - It("exposes known-absent artwork so clients can skip the request", func() { - ar := model.Artist{ID: "ar-1", Name: "Artist"} - ar.ImageAbsent = true - - var out map[string]any - data, err := json.Marshal(ar) - Expect(err).ToNot(HaveOccurred()) - Expect(json.Unmarshal(data, &out)).To(Succeed()) - - Expect(out).To(HaveKeyWithValue("imageAbsent", true)) - }) })