feat(model): add blur_hash columns and ArtworkUpdatedAt version methods

This commit is contained in:
Deluan 2026-07-15 23:11:14 -04:00
parent 470b4bdfa0
commit a115726e71
7 changed files with 100 additions and 0 deletions

View File

@ -0,0 +1,15 @@
-- +goose Up
alter table album add column blur_hash varchar;
alter table album add column blur_hash_updated_at datetime;
alter table artist add column blur_hash varchar;
alter table artist add column blur_hash_updated_at datetime;
alter table playlist add column blur_hash varchar;
alter table playlist add column blur_hash_updated_at datetime;
-- +goose Down
alter table album drop column blur_hash;
alter table album drop column blur_hash_updated_at;
alter table artist drop column blur_hash;
alter table artist drop column blur_hash_updated_at;
alter table playlist drop column blur_hash;
alter table playlist drop column blur_hash_updated_at;

View File

@ -67,12 +67,29 @@ type Album struct {
ImportedAt time.Time `structs:"imported_at" json:"importedAt" hash:"ignore"` // When this album was imported/updated
CreatedAt time.Time `structs:"created_at" json:"createdAt"` // Oldest CreatedAt for all songs in this album
UpdatedAt time.Time `structs:"updated_at" json:"updatedAt"` // Newest UpdatedAt for all songs in this album
// BlurHash of the album cover, computed asynchronously from the served artwork.
BlurHash string `structs:"blur_hash" json:"blurHash,omitempty" hash:"ignore"`
BlurHashUpdatedAt *time.Time `structs:"blur_hash_updated_at" json:"-" hash:"ignore"`
}
func (a Album) CoverArtID() ArtworkID {
return artworkIDFromAlbum(a)
}
// ArtworkUpdatedAt is the album's artwork version: the newest row timestamp that can affect
// which cover image is served (scan updates, imports, agent-fetched external images).
func (a Album) ArtworkUpdatedAt() time.Time {
t := a.UpdatedAt
if a.ImportedAt.After(t) {
t = a.ImportedAt
}
if a.ExternalInfoUpdatedAt != nil && a.ExternalInfoUpdatedAt.After(t) {
t = *a.ExternalInfoUpdatedAt
}
return t
}
func (a Album) FullName() string {
if conf.Server.Subsonic.AppendAlbumVersion && len(a.Tags[TagAlbumVersion]) > 0 {
return fmt.Sprintf("%s (%s)", a.Name, a.Tags[TagAlbumVersion][0])

View File

@ -2,6 +2,7 @@ package model_test
import (
"encoding/json"
"time"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/conf/configtest"
@ -50,3 +51,22 @@ var _ = Describe("Albums", func() {
})
})
})
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("returns ExternalInfoUpdatedAt when it is the newest", func() {
al := Album{UpdatedAt: base, ImportedAt: later, ExternalInfoUpdatedAt: &latest}
Expect(al.ArtworkUpdatedAt()).To(Equal(latest))
})
})

View File

@ -41,6 +41,9 @@ type Artist struct {
CreatedAt *time.Time `structs:"created_at" json:"createdAt,omitempty"`
UpdatedAt *time.Time `structs:"updated_at" json:"updatedAt,omitempty"`
BlurHash string `structs:"blur_hash" json:"blurHash,omitempty"`
BlurHashUpdatedAt *time.Time `structs:"blur_hash_updated_at" json:"-"`
}
type ArtistStats struct {
@ -63,6 +66,19 @@ func (a Artist) CoverArtID() ArtworkID {
return artworkIDFromArtist(a)
}
// ArtworkUpdatedAt is the artist's artwork version; images often arrive via external agents,
// which bump ExternalInfoUpdatedAt rather than UpdatedAt.
func (a Artist) ArtworkUpdatedAt() time.Time {
var t time.Time
if a.UpdatedAt != nil {
t = *a.UpdatedAt
}
if a.ExternalInfoUpdatedAt != nil && a.ExternalInfoUpdatedAt.After(t) {
t = *a.ExternalInfoUpdatedAt
}
return t
}
func (a Artist) UploadedImagePath() string {
return UploadedImagePath(consts.EntityArtist, a.UploadedImage)
}

View File

@ -2,6 +2,7 @@ package model_test
import (
"path/filepath"
"time"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/conf/configtest"
@ -28,3 +29,18 @@ var _ = Describe("Artist", func() {
})
})
})
var _ = Describe("Artist.ArtworkUpdatedAt", func() {
base := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
later := base.Add(24 * time.Hour)
It("handles nil timestamps", func() {
Expect(model.Artist{}.ArtworkUpdatedAt()).To(Equal(time.Time{}))
})
It("returns UpdatedAt when newest", func() {
Expect(model.Artist{UpdatedAt: &later, ExternalInfoUpdatedAt: &base}.ArtworkUpdatedAt()).To(Equal(later))
})
It("returns ExternalInfoUpdatedAt when newest", func() {
Expect(model.Artist{UpdatedAt: &base, ExternalInfoUpdatedAt: &later}.ArtworkUpdatedAt()).To(Equal(later))
})
})

View File

@ -30,6 +30,9 @@ type Playlist struct {
CreatedAt time.Time `structs:"created_at" json:"createdAt"`
UpdatedAt time.Time `structs:"updated_at" json:"updatedAt"`
BlurHash string `structs:"blur_hash" json:"blurHash,omitempty"`
BlurHashUpdatedAt *time.Time `structs:"blur_hash_updated_at" json:"-"`
// SmartPlaylist attributes
Rules *criteria.Criteria `structs:"rules" json:"rules"`
EvaluatedAt *time.Time `structs:"evaluated_at" json:"evaluatedAt"`
@ -39,6 +42,10 @@ func (pls Playlist) IsSmartPlaylist() bool {
return pls.Rules != nil && pls.Rules.Expression != nil
}
func (pls Playlist) ArtworkUpdatedAt() time.Time {
return pls.UpdatedAt
}
func (pls Playlist) MediaFiles() MediaFiles {
if len(pls.Tracks) == 0 {
return nil

View File

@ -1,6 +1,8 @@
package model_test
import (
"time"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/tests"
. "github.com/onsi/ginkgo/v2"
@ -44,3 +46,10 @@ var _ = Describe("Playlist", func() {
})
})
})
var _ = Describe("Playlist.ArtworkUpdatedAt", func() {
It("returns UpdatedAt", func() {
now := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
Expect(model.Playlist{UpdatedAt: now}.ArtworkUpdatedAt()).To(Equal(now))
})
})