fix(persistence): gate the parent fold by the reader's album-root rule

The parent folder's images_updated_at now counts only when the reader could
actually serve the album-root cover: multiple album folders (disc layout), or a
single folder with no images of its own. This mirrors albumRootParent's first
qualification gate, so an unrelated artist-level image change no longer advances
(and temporarily suppresses) the version of every sibling album with its own
cover. The remaining gates (other-audio, library root) stay unmirrored: with
omission plus the write-side clamp, residual over-approximation only causes a
short suppression that closes on the next serve. Plan verified on a 96K-track
production copy: unchanged, all PK point lookups.
This commit is contained in:
Deluan 2026-07-17 23:41:08 -04:00
parent 9e3fc91cec
commit cddc30b586
2 changed files with 24 additions and 3 deletions

View File

@ -226,11 +226,13 @@ 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. 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.
// touching the album row. Parents count only when the reader could serve the album-root cover
// (disc subfolders, or a single folder with no images of its own), mirroring albumRootParent's
// first gate. 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)"+
" union select p.parent_id from folder p, json_each(album.folder_ids) je2 where p.id = je2.value"+
" and (json_array_length(album.folder_ids) > 1 or json_array_length(p.image_files) = 0))"+
" 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

@ -952,6 +952,25 @@ var _ = Describe("AlbumRepository folder images version", func() {
Expect(al.FolderImagesUpdatedAt.Equal(rootAt)).To(BeTrue(), "the parent folder's newer cover must win")
})
It("ignores the parent when the album's single folder has images of its own", func() {
// Mirrors albumRootParent's first gate: the reader would serve the folder's own cover, so an
// unrelated artist-level image must not advance (and suppress) this album's version.
ownAt := time.Date(2030, 6, 1, 12, 0, 0, 0, time.UTC)
parentAt := ownAt.Add(time.Hour)
_, err := GetDBXBuilder().NewQuery(
"insert into folder (id, library_id, path, name, parent_id, images_updated_at, image_files) values" +
" ('fold-blur-root', 1, '.', 'Artist', '', {:parent}, '[\"artist.jpg\"]')," +
" ('fold-blur-1', 1, './Artist', 'Album', 'fold-blur-root', {:own}, '[\"cover.jpg\"]')").
Bind(map[string]any{"parent": parentAt, "own": ownAt}).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).To(HaveValue(Equal(ownAt)))
})
It("leaves FolderImagesUpdatedAt nil when the album has no folders", func() {
al, err := repo.Get("101")
Expect(err).ToNot(HaveOccurred())