mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-01 07:21:17 +00:00
feat(jellyfin): emit stored blurhashes with version-seeded fake fallback
This commit is contained in:
parent
9f9019df07
commit
585ac0aab3
@ -1,6 +1,10 @@
|
||||
package dto
|
||||
|
||||
import "hash/fnv"
|
||||
import (
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// base83Alphabet is the blurhash spec's base83 encoding alphabet; order is part of the spec.
|
||||
const base83Alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~"
|
||||
@ -34,3 +38,13 @@ func blurHash(seed string) string {
|
||||
dc := (r << 16) | (g << 8) | b
|
||||
return "00" + base83(dc, 4)
|
||||
}
|
||||
|
||||
// primaryBlurHash returns the stored blurhash when it was computed from the entity's current
|
||||
// artwork version; otherwise a fake seeded by id+version, so the value still rotates on any
|
||||
// artwork change (Finamp keys its cover caches by this value; tags never reach its image URLs).
|
||||
func primaryBlurHash(stored string, storedAt *time.Time, id string, version time.Time) string {
|
||||
if stored != "" && storedAt != nil && storedAt.Equal(version) {
|
||||
return stored
|
||||
}
|
||||
return blurHash(fmt.Sprintf("%s-%x", id, version.UnixMilli()))
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ package dto
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
@ -25,3 +26,29 @@ var _ = Describe("blurHash", func() {
|
||||
Expect(blurHash("cover-tag-1")).ToNot(Equal(blurHash("cover-tag-2")))
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("primaryBlurHash", func() {
|
||||
version := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
It("returns the stored hash when it matches the current artwork version", func() {
|
||||
Expect(primaryBlurHash("LEHV6nWB2yk8", &version, "id-1", version)).To(Equal("LEHV6nWB2yk8"))
|
||||
})
|
||||
|
||||
It("falls back to a fake when there is no stored hash", func() {
|
||||
h := primaryBlurHash("", nil, "id-1", version)
|
||||
Expect(h).To(HaveLen(6))
|
||||
})
|
||||
|
||||
It("falls back to a fake when the stored hash is stale", func() {
|
||||
stale := version.Add(-time.Hour)
|
||||
h := primaryBlurHash("LEHV6nWB2yk8", &stale, "id-1", version)
|
||||
Expect(h).To(HaveLen(6))
|
||||
Expect(h).ToNot(Equal("LEHV6nWB2yk8"))
|
||||
})
|
||||
|
||||
It("rotates the fake when the artwork version moves", func() {
|
||||
h1 := primaryBlurHash("", nil, "id-1", version)
|
||||
h2 := primaryBlurHash("", nil, "id-1", version.Add(time.Hour))
|
||||
Expect(h1).ToNot(Equal(h2))
|
||||
})
|
||||
})
|
||||
|
||||
@ -201,7 +201,7 @@ func AlbumToBaseItem(al model.Album) BaseItemDto {
|
||||
RunTimeTicks: TicksFromSeconds(al.Duration),
|
||||
DateCreated: jellyfinDate(&al.CreatedAt),
|
||||
ImageTags: map[string]string{"Primary": al.ID},
|
||||
ImageBlurHashes: map[string]map[string]string{"Primary": {al.ID: blurHash(al.ID)}},
|
||||
ImageBlurHashes: map[string]map[string]string{"Primary": {al.ID: primaryBlurHash(al.BlurHash, al.BlurHashUpdatedAt, al.ID, al.ArtworkUpdatedAt())}},
|
||||
BackdropImageTags: []string{},
|
||||
UserData: UserData(al.Annotations, al.ID),
|
||||
}
|
||||
@ -231,7 +231,7 @@ func ArtistToBaseItem(ar model.Artist) BaseItemDto {
|
||||
SongCount: new(ar.SongCount),
|
||||
DateCreated: jellyfinDate(ar.CreatedAt),
|
||||
ImageTags: map[string]string{"Primary": ar.ID},
|
||||
ImageBlurHashes: map[string]map[string]string{"Primary": {ar.ID: blurHash(ar.ID)}},
|
||||
ImageBlurHashes: map[string]map[string]string{"Primary": {ar.ID: primaryBlurHash(ar.BlurHash, ar.BlurHashUpdatedAt, ar.ID, ar.ArtworkUpdatedAt())}},
|
||||
BackdropImageTags: []string{},
|
||||
UserData: UserData(ar.Annotations, ar.ID),
|
||||
}
|
||||
@ -264,7 +264,7 @@ func PlaylistToBaseItem(p model.Playlist) BaseItemDto {
|
||||
ChildCount: new(p.SongCount),
|
||||
RunTimeTicks: TicksFromSeconds(p.Duration),
|
||||
ImageTags: map[string]string{"Primary": tag},
|
||||
ImageBlurHashes: map[string]map[string]string{"Primary": {tag: blurHash(tag)}},
|
||||
ImageBlurHashes: map[string]map[string]string{"Primary": {tag: primaryBlurHash(p.BlurHash, p.BlurHashUpdatedAt, p.ID, p.ArtworkUpdatedAt())}},
|
||||
BackdropImageTags: []string{},
|
||||
UserData: UserData(p.Annotations, p.ID),
|
||||
}
|
||||
|
||||
@ -390,3 +390,34 @@ var _ = Describe("LyricDtoFromLyrics", func() {
|
||||
Expect(c.Start).To(Equal(int64(10_000_000)))
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("stored blurhashes", func() {
|
||||
version := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
It("emits the stored album blurhash when fresh, and a rotating fake when stale", func() {
|
||||
al := model.Album{ID: "al-1", Name: "A", UpdatedAt: version, ImportedAt: version,
|
||||
BlurHash: "LEHV6nWB2yk8", BlurHashUpdatedAt: &version}
|
||||
Expect(AlbumToBaseItem(al).ImageBlurHashes["Primary"]["al-1"]).To(Equal("LEHV6nWB2yk8"))
|
||||
|
||||
al.UpdatedAt = version.Add(time.Hour) // artwork version moved; stored hash is now stale
|
||||
fake := AlbumToBaseItem(al).ImageBlurHashes["Primary"]["al-1"]
|
||||
Expect(fake).To(HaveLen(6))
|
||||
|
||||
al.UpdatedAt = version.Add(2 * time.Hour)
|
||||
Expect(AlbumToBaseItem(al).ImageBlurHashes["Primary"]["al-1"]).ToNot(Equal(fake))
|
||||
})
|
||||
|
||||
It("emits the stored artist blurhash when fresh", func() {
|
||||
ar := model.Artist{ID: "ar-1", Name: "B", UpdatedAt: &version,
|
||||
BlurHash: "LEHV6nWB2yk8", BlurHashUpdatedAt: &version}
|
||||
Expect(ArtistToBaseItem(ar).ImageBlurHashes["Primary"]["ar-1"]).To(Equal("LEHV6nWB2yk8"))
|
||||
})
|
||||
|
||||
It("emits the stored playlist blurhash when fresh, keyed by the versioned tag", func() {
|
||||
p := model.Playlist{ID: "pl-1", Name: "P", UpdatedAt: version,
|
||||
BlurHash: "LEHV6nWB2yk8", BlurHashUpdatedAt: &version}
|
||||
item := PlaylistToBaseItem(p)
|
||||
tag := item.ImageTags["Primary"]
|
||||
Expect(item.ImageBlurHashes["Primary"][tag]).To(Equal("LEHV6nWB2yk8"))
|
||||
})
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user