fix(persistence): include parent folders in the album artwork version

Multi-disc albums keep their cover in the album-root folder, which the artwork
reader reaches via albumRootParent but which is not in album.folder_ids (only the
disc folders hold media files). A root cover swap therefore advanced the served
cache key without moving the selected artwork version, recreating the stale-hash
deadlock for hash-keyed clients. The version subquery now also considers the
folders' parents. This slightly over-covers (an artist-folder image change can
advance a single-folder album's version), which errs on the side of one spurious
refetch instead of permanent staleness. Plan verified on a 96K-track production
copy: still PK point lookups, outer scan unchanged.
This commit is contained in:
Deluan 2026-07-17 22:47:42 -04:00
parent 9a36047096
commit 08d83a7785
2 changed files with 23 additions and 3 deletions

View File

@ -226,8 +226,11 @@ func (r *albumRepository) UpdateExternalInfo(al *model.Album) error {
func (r *albumRepository) selectAlbum(options ...model.QueryOptions) SelectBuilder {
sql := r.newSelect(options...).Columns("album.*", "library.path as library_path", "library.name as library_name",
// Folds folder image mtimes into the artwork version: an in-place cover swap moves them without
// touching the album row. Bare column (not max()) keeps the decltype so the driver scans time.Time.
"(select f.images_updated_at from folder f, json_each(album.folder_ids) je where f.id = je.value"+
// touching the album row. Parents are included for disc-subfolder layouts, where the reader can
// serve the album-root cover. Bare column (not max()) keeps the decltype so time.Time scans.
"(select f.images_updated_at from folder f where f.id in"+
" (select je.value from json_each(album.folder_ids) je"+
" union select p.parent_id from folder p, json_each(album.folder_ids) je2 where p.id = je2.value)"+
" order by f.images_updated_at desc limit 1) as folder_images_updated_at").
LeftJoin("library on album.library_id = library.id")
sql = r.withAnnotation(sql, "album.id")

View File

@ -910,7 +910,7 @@ var _ = Describe("AlbumRepository folder images version", func() {
Expect(GetDBXBuilder().NewQuery("select folder_ids from album where id = '103'").
Row(&origFolderIDs)).To(Succeed())
DeferCleanup(func() {
_, err := GetDBXBuilder().NewQuery("delete from folder where id = 'fold-blur-1'").Execute()
_, err := GetDBXBuilder().NewQuery("delete from folder where id in ('fold-blur-1', 'fold-blur-root')").Execute()
Expect(err).ToNot(HaveOccurred())
_, err = GetDBXBuilder().NewQuery("update album set folder_ids = {:f} where id = '103'").
Bind(map[string]any{"f": origFolderIDs}).Execute()
@ -935,6 +935,23 @@ var _ = Describe("AlbumRepository folder images version", func() {
Expect(al.ArtworkUpdatedAt().Equal(imagesAt)).To(BeTrue(), "folder image changes must advance the artwork version")
})
It("includes the parent folder's images (album-root cover with disc subfolders)", func() {
discAt := time.Date(2030, 6, 1, 12, 0, 0, 0, time.UTC)
rootAt := discAt.Add(time.Hour) // the root cover is the newest image
_, err := GetDBXBuilder().NewQuery(
"insert into folder (id, library_id, path, name, parent_id, images_updated_at) values" +
" ('fold-blur-root', 1, '.', 'Album', '', {:root}), ('fold-blur-1', 1, './Album', 'CD1', 'fold-blur-root', {:disc})").
Bind(map[string]any{"root": rootAt, "disc": discAt}).Execute()
Expect(err).ToNot(HaveOccurred())
_, err = GetDBXBuilder().NewQuery(`update album set folder_ids = '["fold-blur-1"]' where id = '103'`).Execute()
Expect(err).ToNot(HaveOccurred())
al, err := repo.Get("103")
Expect(err).ToNot(HaveOccurred())
Expect(al.FolderImagesUpdatedAt).ToNot(BeNil())
Expect(al.FolderImagesUpdatedAt.Equal(rootAt)).To(BeTrue(), "the parent folder's newer cover must win")
})
It("leaves FolderImagesUpdatedAt nil when the album has no folders", func() {
al, err := repo.Get("101")
Expect(err).ToNot(HaveOccurred())