mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-28 22:27:10 +01:00
fix(jellyfin): emit per-image ImageBlurHashes so clients de-dupe images and stop warning
This commit is contained in:
parent
cb7b938fe6
commit
f80ee0e41b
5 changed files with 87 additions and 5 deletions
37
server/jellyfin/dto/blurhash.go
Normal file
37
server/jellyfin/dto/blurhash.go
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
package dto
|
||||
|
||||
import "hash/fnv"
|
||||
|
||||
// base83Alphabet is the blurhash spec's base83 encoding alphabet; order is part of the spec.
|
||||
const base83Alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~"
|
||||
|
||||
// base83 encodes value as a fixed-width, big-endian base83 string of the given length.
|
||||
func base83(value, length int) string {
|
||||
b := make([]byte, length)
|
||||
for i := 1; i <= length; i++ {
|
||||
digit := (value / pow83(length-i)) % 83
|
||||
b[i-1] = base83Alphabet[digit]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func pow83(n int) int {
|
||||
result := 1
|
||||
for range n {
|
||||
result *= 83
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// blurHash returns a valid 6-char blurhash encoding a solid color deterministically derived
|
||||
// from seed. We can't afford to compute real blurhashes from cover art, but Finamp only needs a
|
||||
// well-formed, per-image-tag-stable value: it uses this as a de-dup key for image downloads and
|
||||
// a blur placeholder, so a solid color unique to the tag satisfies both without decoding images.
|
||||
func blurHash(seed string) string {
|
||||
h := fnv.New32a()
|
||||
_, _ = h.Write([]byte(seed))
|
||||
sum := h.Sum(nil)
|
||||
r, g, b := int(sum[0]), int(sum[1]), int(sum[2])
|
||||
dc := (r << 16) | (g << 8) | b
|
||||
return "00" + base83(dc, 4)
|
||||
}
|
||||
27
server/jellyfin/dto/blurhash_test.go
Normal file
27
server/jellyfin/dto/blurhash_test.go
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
package dto
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("blurHash", func() {
|
||||
It("returns a 6-char valid blurhash starting with the 1x1 component prefix", func() {
|
||||
h := blurHash("x")
|
||||
Expect(h).To(HaveLen(6))
|
||||
Expect(h).To(HavePrefix("00"))
|
||||
for _, c := range h {
|
||||
Expect(strings.ContainsRune(base83Alphabet, c)).To(BeTrue(), "unexpected char %q", c)
|
||||
}
|
||||
})
|
||||
|
||||
It("is deterministic for the same seed", func() {
|
||||
Expect(blurHash("cover-tag-1")).To(Equal(blurHash("cover-tag-1")))
|
||||
})
|
||||
|
||||
It("differs for different seeds", func() {
|
||||
Expect(blurHash("cover-tag-1")).ToNot(Equal(blurHash("cover-tag-2")))
|
||||
})
|
||||
})
|
||||
|
|
@ -65,11 +65,15 @@ type BaseItemDto struct {
|
|||
SongCount *int `json:"SongCount,omitempty"`
|
||||
AlbumCount *int `json:"AlbumCount,omitempty"`
|
||||
ImageTags map[string]string `json:"ImageTags,omitempty"`
|
||||
BackdropImageTags []string `json:"BackdropImageTags"`
|
||||
UserData *UserItemDataDto `json:"UserData,omitempty"`
|
||||
MediaSources []MediaSourceInfo `json:"MediaSources,omitempty"`
|
||||
Container string `json:"Container,omitempty"`
|
||||
CanDownload bool `json:"CanDownload"`
|
||||
// ImageBlurHashes is keyed by image type (e.g. "Primary") then image tag. Finamp uses it both
|
||||
// to render a placeholder while art loads and, more importantly, as a de-dup key for image
|
||||
// downloads -- without it Finamp warns that the server isn't calculating blurhashes.
|
||||
ImageBlurHashes map[string]map[string]string `json:"ImageBlurHashes,omitempty"`
|
||||
BackdropImageTags []string `json:"BackdropImageTags"`
|
||||
UserData *UserItemDataDto `json:"UserData,omitempty"`
|
||||
MediaSources []MediaSourceInfo `json:"MediaSources,omitempty"`
|
||||
Container string `json:"Container,omitempty"`
|
||||
CanDownload bool `json:"CanDownload"`
|
||||
}
|
||||
|
||||
// PlaylistUserPermissions is the response shape for GET /Playlists/{id}/Users(/{userId}), which
|
||||
|
|
|
|||
|
|
@ -117,6 +117,7 @@ func SongToBaseItem(mf model.MediaFile) BaseItemDto {
|
|||
// Finamp resolves song art via AlbumId + a non-empty AlbumPrimaryImageTag.
|
||||
if mf.AlbumID != "" {
|
||||
item.AlbumPrimaryImageTag = mf.AlbumID
|
||||
item.ImageBlurHashes = map[string]map[string]string{"Primary": {mf.AlbumID: blurHash(mf.AlbumID)}}
|
||||
}
|
||||
return item
|
||||
}
|
||||
|
|
@ -134,6 +135,7 @@ func AlbumToBaseItem(al model.Album) BaseItemDto {
|
|||
SongCount: new(al.SongCount),
|
||||
RunTimeTicks: TicksFromSeconds(al.Duration),
|
||||
ImageTags: map[string]string{"Primary": al.ID},
|
||||
ImageBlurHashes: map[string]map[string]string{"Primary": {al.ID: blurHash(al.ID)}},
|
||||
BackdropImageTags: []string{},
|
||||
UserData: UserData(al.Annotations, al.ID),
|
||||
}
|
||||
|
|
@ -161,6 +163,7 @@ func ArtistToBaseItem(ar model.Artist) BaseItemDto {
|
|||
AlbumCount: new(ar.AlbumCount),
|
||||
SongCount: new(ar.SongCount),
|
||||
ImageTags: map[string]string{"Primary": ar.ID},
|
||||
ImageBlurHashes: map[string]map[string]string{"Primary": {ar.ID: blurHash(ar.ID)}},
|
||||
BackdropImageTags: []string{},
|
||||
UserData: UserData(ar.Annotations, ar.ID),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,15 @@ var _ = Describe("mappers", func() {
|
|||
Expect(item.UserData.ItemId).To(Equal(EncodeID("song-1")))
|
||||
Expect(item.MediaSources).To(HaveLen(1))
|
||||
Expect(item.MediaSources[0].Size).To(Equal(int64(2_500_000)))
|
||||
Expect(item.ImageBlurHashes["Primary"]).To(HaveKey(item.AlbumPrimaryImageTag))
|
||||
Expect(item.ImageBlurHashes["Primary"][item.AlbumPrimaryImageTag]).To(HaveLen(6))
|
||||
})
|
||||
|
||||
It("omits ImageBlurHashes when a song has no album", func() {
|
||||
mf := model.MediaFile{ID: "song-noalbum", Title: "Song", Duration: 60}
|
||||
item := SongToBaseItem(mf)
|
||||
Expect(item.AlbumPrimaryImageTag).To(BeEmpty())
|
||||
Expect(item.ImageBlurHashes).To(BeNil())
|
||||
})
|
||||
|
||||
It("builds a MediaSourceInfo from a media file", func() {
|
||||
|
|
@ -125,6 +134,8 @@ var _ = Describe("mappers", func() {
|
|||
Expect(item.ArtistItems).To(Equal(item.AlbumArtists))
|
||||
Expect(*item.ProductionYear).To(Equal(1999))
|
||||
Expect(*item.ChildCount).To(Equal(10))
|
||||
Expect(item.ImageBlurHashes["Primary"]).To(HaveKey(item.ImageTags["Primary"]))
|
||||
Expect(item.ImageBlurHashes["Primary"][item.ImageTags["Primary"]]).To(HaveLen(6))
|
||||
})
|
||||
|
||||
It("maps an artist to a MusicArtist folder item", func() {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue