mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-31 07:30:32 +00:00
fix(ui): bust song artwork URLs when the album cover changes
Song art URLs (mf- ids: player bar, queue) were cache-busted only by the song's own updatedAt, and cover responses carry a 10-year max-age — so after an album cover upload/delete the browser kept serving the stale cached image (including the placeholder, for previously artless albums) until the file itself was rescanned. Expose the album's cover_art_updated_at on song payloads via a scalar subquery (a join would make unqualified references ambiguous — media_file and album share column names), in both the media_file select and the playlist tracks query, and pass it through the player queue builder so getCoverArtUrl picks it up.
This commit is contained in:
parent
35d08a25a3
commit
e8946b8810
@ -30,11 +30,14 @@ type MediaFile struct {
|
||||
LibraryID int `structs:"library_id" json:"libraryId" hash:"ignore"`
|
||||
LibraryPath string `structs:"-" json:"libraryPath" hash:"ignore"`
|
||||
LibraryName string `structs:"-" json:"libraryName" hash:"ignore"`
|
||||
FolderID string `structs:"folder_id" json:"folderId" hash:"ignore"`
|
||||
Path string `structs:"path" json:"path" hash:"ignore"`
|
||||
Title string `structs:"title" json:"title"`
|
||||
Album string `structs:"album" json:"album"`
|
||||
ArtistID string `structs:"artist_id" json:"artistId"` // Deprecated: Use Participants instead
|
||||
// CoverArtUpdatedAt mirrors the album's manual-cover timestamp so clients can
|
||||
// bust song artwork URLs that resolve to the album cover.
|
||||
CoverArtUpdatedAt *time.Time `structs:"-" json:"coverArtUpdatedAt,omitempty" hash:"ignore"`
|
||||
FolderID string `structs:"folder_id" json:"folderId" hash:"ignore"`
|
||||
Path string `structs:"path" json:"path" hash:"ignore"`
|
||||
Title string `structs:"title" json:"title"`
|
||||
Album string `structs:"album" json:"album"`
|
||||
ArtistID string `structs:"artist_id" json:"artistId"` // Deprecated: Use Participants instead
|
||||
// Artist is the display name used for the artist.
|
||||
Artist string `structs:"artist" json:"artist"`
|
||||
AlbumArtistID string `structs:"album_artist_id" json:"albumArtistId"` // Deprecated: Use Participants instead
|
||||
|
||||
@ -184,7 +184,10 @@ func (r *mediaFileRepository) UpdateProbeData(id string, data string) error {
|
||||
}
|
||||
|
||||
func (r *mediaFileRepository) selectMediaFile(options ...model.QueryOptions) SelectBuilder {
|
||||
sql := r.newSelect(options...).Columns("media_file.*", "library.path as library_path", "library.name as library_name").
|
||||
// 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").
|
||||
LeftJoin("library on media_file.library_id = library.id")
|
||||
sql = r.withAnnotation(sql, "media_file.id")
|
||||
sql = r.withBookmark(sql, "media_file.id")
|
||||
|
||||
@ -29,6 +29,27 @@ var _ = Describe("MediaRepository", func() {
|
||||
mr = NewMediaFileRepository(ctx, GetDBXBuilder())
|
||||
})
|
||||
|
||||
Describe("CoverArtUpdatedAt", func() {
|
||||
It("exposes the album's cover timestamp on its songs", func() {
|
||||
albumRepo := NewAlbumRepository(request.WithUser(GinkgoT().Context(), model.User{ID: "userid"}), GetDBXBuilder())
|
||||
Expect(albumRepo.Put(&model.Album{ID: "cover-al", Name: "cover", LibraryID: 1})).To(Succeed())
|
||||
Expect(mr.Put(&model.MediaFile{ID: "cover-mf", LibraryID: 1, AlbumID: "cover-al", Path: "test/cover-mf.mp3"})).To(Succeed())
|
||||
DeferCleanup(func() {
|
||||
_, _ = mr.(*mediaFileRepository).executeSQL(squirrel.Delete("media_file").Where(squirrel.Eq{"id": "cover-mf"}))
|
||||
_, _ = mr.(*mediaFileRepository).executeSQL(squirrel.Delete("album").Where(squirrel.Eq{"id": "cover-al"}))
|
||||
})
|
||||
|
||||
got, err := mr.Get("cover-mf")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(got.CoverArtUpdatedAt).To(BeNil())
|
||||
|
||||
Expect(albumRepo.UpdateImage("cover-al", "cover-al_x.jpg")).To(Succeed())
|
||||
got, err = mr.Get("cover-mf")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(got.CoverArtUpdatedAt).ToNot(BeNil())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("GetCursor", func() {
|
||||
It("yields the same media files as GetAll", func() {
|
||||
opts := model.QueryOptions{Sort: "title"}
|
||||
|
||||
@ -314,6 +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",
|
||||
).
|
||||
LeftJoin("annotation on (" +
|
||||
"annotation.item_id = media_file_id" +
|
||||
|
||||
@ -91,6 +91,7 @@ const mapToAudioLists = (item) => {
|
||||
{
|
||||
id: trackId,
|
||||
updatedAt: item.updatedAt,
|
||||
coverArtUpdatedAt: item.coverArtUpdatedAt,
|
||||
album: item.album,
|
||||
},
|
||||
300,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user