navidrome/model/album_test.go
Deluan 07af29fbb4 fix(jellyfin): fold folder image mtimes into the album blurhash version
An in-place cover-file swap followed by a quick scan updates only the folder's
images_updated_at: no tracks are imported, so the album row never moves and
ArtworkUpdatedAt() stayed at the old value. The Jellyfin DTO then kept emitting
the previous stored blurhash, and clients that key their cover caches on it never
refetched the image, so the served-bytes recompute could never run.

The album select now surfaces the newest folder images_updated_at (bare-column
correlated subquery over json_each(folder_ids), so the datetime decltype survives
and scans as time.Time) and ArtworkUpdatedAt() folds it in. Benchmarked on a 96K
track production copy: ~90us/page added, no query-plan change; the subquery is a
PK point lookup per folder with at most two rows to order. Artist images and
playlist sidecars have the same theoretical gap but their timestamps cannot be
derived in SQL; they remain a documented follow-up.
2026-07-17 22:14:13 -04:00

81 lines
2.8 KiB
Go

package model_test
import (
"encoding/json"
"time"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/conf/configtest"
. "github.com/navidrome/navidrome/model"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("Album", func() {
BeforeEach(func() {
DeferCleanup(configtest.SetupConfig())
})
DescribeTable("FullName",
func(enabled bool, tags Tags, expected string) {
conf.Server.Subsonic.AppendAlbumVersion = enabled
a := Album{Name: "Album", Tags: tags}
Expect(a.FullName()).To(Equal(expected))
},
Entry("appends version when enabled and tag is present", true, Tags{TagAlbumVersion: []string{"Remastered"}}, "Album (Remastered)"),
Entry("returns just name when disabled", false, Tags{TagAlbumVersion: []string{"Remastered"}}, "Album"),
Entry("returns just name when tag is absent", true, Tags{}, "Album"),
Entry("returns just name when tag is an empty slice", true, Tags{TagAlbumVersion: []string{}}, "Album"),
)
})
var _ = Describe("Albums", func() {
var albums Albums
Context("JSON Marshalling", func() {
When("we have a valid Albums object", func() {
BeforeEach(func() {
albums = Albums{
{ID: "1", AlbumArtist: "Artist", AlbumArtistID: "11", SortAlbumArtistName: "SortAlbumArtistName", OrderAlbumArtistName: "OrderAlbumArtistName"},
{ID: "2", AlbumArtist: "Artist", AlbumArtistID: "11", SortAlbumArtistName: "SortAlbumArtistName", OrderAlbumArtistName: "OrderAlbumArtistName"},
}
})
It("marshals correctly", func() {
data, err := json.Marshal(albums)
Expect(err).To(BeNil())
var albums2 Albums
err = json.Unmarshal(data, &albums2)
Expect(err).To(BeNil())
Expect(albums2).To(Equal(albums))
})
})
})
})
var _ = Describe("Album.ArtworkUpdatedAt", func() {
base := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
later := base.Add(24 * time.Hour)
latest := base.Add(48 * time.Hour)
It("returns UpdatedAt when it is the newest", func() {
al := Album{UpdatedAt: later, ImportedAt: base}
Expect(al.ArtworkUpdatedAt()).To(Equal(later))
})
It("returns ImportedAt when it is the newest", func() {
al := Album{UpdatedAt: base, ImportedAt: later}
Expect(al.ArtworkUpdatedAt()).To(Equal(later))
})
It("ignores ExternalInfoUpdatedAt (agent TTL refreshes bump it without an image change)", func() {
al := Album{UpdatedAt: base, ImportedAt: later, ExternalInfoUpdatedAt: &latest}
Expect(al.ArtworkUpdatedAt()).To(Equal(later))
})
It("returns FolderImagesUpdatedAt when it is the newest (in-place cover swap)", func() {
al := Album{UpdatedAt: base, ImportedAt: later, FolderImagesUpdatedAt: &latest}
Expect(al.ArtworkUpdatedAt()).To(Equal(latest))
})
It("ignores an older FolderImagesUpdatedAt", func() {
al := Album{UpdatedAt: later, ImportedAt: base, FolderImagesUpdatedAt: &base}
Expect(al.ArtworkUpdatedAt()).To(Equal(later))
})
})