feat(jellyfin): version album and artist image tags by content hash

This commit is contained in:
Deluan 2026-07-23 20:14:40 -04:00
parent 49e8464db4
commit 6f8e33d28a
2 changed files with 77 additions and 7 deletions

View File

@ -200,7 +200,25 @@ func SongToBaseItem(mf model.MediaFile, fields Fields) BaseItemDto {
return item
}
// primaryImageTag returns the image cache tag plus the real-blurhash map; absent art yields ("", nil),
// and unresolved art falls back to the id. A blurhash is never synthesized: Finamp keys its cover cache
// on the value, so a fake one pins a stale cover forever (#5798).
func primaryImageTag(img model.ItemImage, fallback string) (string, map[string]map[string]string) {
if img.ImageAbsent {
return "", nil
}
tag := img.ImageHash
if tag == "" {
tag = fallback
}
if img.BlurHash == "" {
return tag, nil
}
return tag, map[string]map[string]string{"Primary": {tag: img.BlurHash}}
}
func AlbumToBaseItem(al model.Album, fields Fields) BaseItemDto {
tag, blurs := primaryImageTag(al.ItemImage, al.ID)
item := BaseItemDto{
Name: al.Name,
Id: EncodeID(al.ID),
@ -213,11 +231,13 @@ func AlbumToBaseItem(al model.Album, fields Fields) BaseItemDto {
SongCount: new(al.SongCount),
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: blurs,
BackdropImageTags: []string{},
UserData: UserData(al.Annotations, al.ID),
}
if tag != "" {
item.ImageTags = map[string]string{"Primary": tag}
}
if al.AlbumArtistID != "" {
item.AlbumArtists = []NameGuidPair{{Name: al.AlbumArtist, Id: EncodeID(al.AlbumArtistID)}}
item.ArtistItems = item.AlbumArtists
@ -247,7 +267,8 @@ func AlbumToBaseItem(al model.Album, fields Fields) BaseItemDto {
}
func ArtistToBaseItem(ar model.Artist) BaseItemDto {
return BaseItemDto{
tag, blurs := primaryImageTag(ar.ItemImage, ar.ID)
item := BaseItemDto{
Name: ar.Name,
Id: EncodeID(ar.ID),
Type: "MusicArtist",
@ -255,11 +276,14 @@ func ArtistToBaseItem(ar model.Artist) BaseItemDto {
AlbumCount: new(ar.AlbumCount),
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: blurs,
BackdropImageTags: []string{},
UserData: UserData(ar.Annotations, ar.ID),
}
if tag != "" {
item.ImageTags = map[string]string{"Primary": tag}
}
return item
}
func GenreToBaseItem(g model.Genre) BaseItemDto {

View File

@ -254,8 +254,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))
Expect(item.ImageTags).To(HaveKeyWithValue("Primary", "alb-1"))
Expect(item.ImageBlurHashes).To(BeNil())
Expect(item.Genres).To(Equal([]string{"genre 1", "genre 2"}))
Expect(item.GenreItems).To(Equal([]NameGuidPair{{Id: EncodeID("1"), Name: "genre 1"}, {Id: EncodeID("2"), Name: "genre 2"}}))
})
@ -384,6 +384,52 @@ var _ = Describe("mappers", func() {
p := model.Playlist{ID: "pl-1", UpdatedAt: time.Date(2026, 7, 1, 0, 0, 0, 0, time.UTC)}
Expect(PlaylistToBaseItem(p).ImageTags).To(Equal(PlaylistToBaseItem(p).ImageTags))
})
Describe("primary image tags", func() {
It("uses the content hash as the tag and emits the real blurhash", func() {
al := model.Album{ID: "alb-1", Name: "Album"}
al.ImageHash = "0123456789abcdef"
al.BlurHash = "LEHV6nWB2yk8"
item := AlbumToBaseItem(al, nil)
Expect(item.ImageTags).To(HaveKeyWithValue("Primary", "0123456789abcdef"))
Expect(item.ImageBlurHashes["Primary"]).To(HaveKeyWithValue("0123456789abcdef", "LEHV6nWB2yk8"))
})
It("omits the blurhash entirely when none was computed", func() {
al := model.Album{ID: "alb-2", Name: "Album"}
al.ImageHash = "0123456789abcdef"
item := AlbumToBaseItem(al, nil)
Expect(item.ImageTags).To(HaveKeyWithValue("Primary", "0123456789abcdef"))
Expect(item.ImageBlurHashes).To(BeNil(), "a synthesized blurhash pins stale covers in Finamp")
})
It("omits tags for known-absent artwork", func() {
al := model.Album{ID: "alb-3", Name: "Album"}
al.ImageAbsent = true
item := AlbumToBaseItem(al, nil)
Expect(item.ImageTags).To(BeEmpty())
Expect(item.ImageBlurHashes).To(BeNil())
})
It("falls back to the entity id while artwork is still unresolved", func() {
item := AlbumToBaseItem(model.Album{ID: "alb-4", Name: "Album"}, nil)
Expect(item.ImageTags).To(HaveKeyWithValue("Primary", "alb-4"))
Expect(item.ImageBlurHashes).To(BeNil())
})
It("versions an artist's tag by content hash", func() {
ar := model.Artist{ID: "art-1", Name: "Artist"}
ar.ImageHash = "fedcba9876543210"
ar.BlurHash = "L6PZfSi_.AyE"
item := ArtistToBaseItem(ar)
Expect(item.ImageTags).To(HaveKeyWithValue("Primary", "fedcba9876543210"))
Expect(item.ImageBlurHashes["Primary"]).To(HaveKeyWithValue("fedcba9876543210", "L6PZfSi_.AyE"))
})
})
})
var _ = Describe("LyricDtoFromLyrics", func() {