fix(subsonic): expose cover cache-buster on bookmarked songs

GetBookmarks selected media_file.* without the album cover-stamp projection,
so bookmarked songs kept emitting the pre-edit coverArt id after a cover
upload/delete. With this fourth copy of the projection appearing, extract it
into a shared coverArtUpdatedAtCol helper used by all media_file read paths
(list, playlist tracks, single playlist track, bookmarks).
This commit is contained in:
Deluan 2026-07-18 02:18:52 -04:00
parent a5d3d615d8
commit d509d8b2f8
6 changed files with 32 additions and 6 deletions

View File

@ -90,3 +90,9 @@ func mapSortOrder(tableName, order string) string {
repl := fmt.Sprintf("(coalesce(nullif(%[1]s.sort_$1,''),%[1]s.order_$1) collate nocase)", tableName)
return sortOrderRegex.ReplaceAllString(order, repl)
}
// coverArtUpdatedAtCol projects the album's manual-cover timestamp onto rows of the given
// table/alias as a scalar subquery — a join would clash with media_file's column names.
func coverArtUpdatedAtCol(alias string) string {
return "(select cover_art_updated_at from album where album.id = " + alias + ".album_id) as cover_art_updated_at"
}

View File

@ -184,10 +184,8 @@ func (r *mediaFileRepository) UpdateProbeData(id string, data string) error {
}
func (r *mediaFileRepository) selectMediaFile(options ...model.QueryOptions) SelectBuilder {
// Scalar subquery (not a join): album shares column names with media_file, and a join
// would make unqualified filter/sort references ambiguous.
sql := r.newSelect(options...).Columns("media_file.*", "library.path as library_path", "library.name as library_name",
"(select cover_art_updated_at from album where album.id = media_file.album_id) as cover_art_updated_at").
coverArtUpdatedAtCol("media_file")).
LeftJoin("library on media_file.library_id = library.id")
sql = r.withAnnotation(sql, "media_file.id")
sql = r.withBookmark(sql, "media_file.id")

View File

@ -314,7 +314,7 @@ func (r *playlistRepository) tracksQuery(query SelectBuilder, id string) SelectB
"playlist_tracks.*",
"library.path as library_path",
"library.name as library_name",
"(select cover_art_updated_at from album where album.id = f.album_id) as cover_art_updated_at",
coverArtUpdatedAtCol("f"),
).
LeftJoin("annotation on (" +
"annotation.item_id = media_file_id" +

View File

@ -108,7 +108,7 @@ func (r *playlistTrackRepository) Read(id string) (any, error) {
"rated_at",
"f.*",
"playlist_tracks.*",
"(select cover_art_updated_at from album where album.id = f.album_id) as cover_art_updated_at",
coverArtUpdatedAtCol("f"),
).
Join("media_file f on f.id = media_file_id").
Where(And{Eq{"playlist_id": r.playlistId}, Eq{"playlist_tracks.id": id}})

View File

@ -100,7 +100,9 @@ func (r sqlRepository) GetBookmarks() (model.Bookmarks, error) {
user, _ := request.UserFrom(r.ctx)
idField := r.tableName + ".id"
sq := r.newSelect().Columns(r.tableName + ".*")
// Only media files are bookmarkable (see dbMediaFiles scan below), so the cover
// stamp projection is safe to add here
sq := r.newSelect().Columns(r.tableName+".*", coverArtUpdatedAtCol(r.tableName))
sq = r.withAnnotation(sq, idField)
sq = r.withBookmark(sq, idField).Where(NotEq{bookmarkTable + ".item_id": nil})
var mfs dbMediaFiles // TODO Decouple from media_file

View File

@ -3,6 +3,7 @@ package persistence
import (
"context"
"github.com/Masterminds/squirrel"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/request"
@ -70,5 +71,24 @@ var _ = Describe("sqlBookmarks", func() {
Expect(mr.DeleteBookmark(songComeTogether.ID)).To(Succeed())
Expect(mr.GetBookmarks()).To(BeEmpty())
})
It("exposes the album's cover timestamp on bookmarked songs", func() {
albumRepo := NewAlbumRepository(request.WithUser(GinkgoT().Context(), model.User{ID: "userid"}), GetDBXBuilder()).(*albumRepository)
Expect(mr.AddBookmark(songAntenna.ID, "cover test", 1)).To(Succeed())
DeferCleanup(func() {
_ = mr.DeleteBookmark(songAntenna.ID)
// Restore both columns so fixture-equality tests stay untouched
_, _ = albumRepo.executeSQL(squirrel.Update("album").
Set("uploaded_image", "").Set("cover_art_updated_at", nil).
Where(squirrel.Eq{"id": songAntenna.AlbumID}))
})
Expect(albumRepo.UpdateImage(songAntenna.AlbumID, "cover-bm.jpg")).To(Succeed())
bms, err := mr.GetBookmarks()
Expect(err).ToNot(HaveOccurred())
Expect(bms).To(HaveLen(1))
Expect(bms[0].Item.CoverArtUpdatedAt).ToNot(BeNil())
})
})
})