feat(persistence): add UpdateBlurHash targeted update to album/artist/playlist repos

This commit is contained in:
Deluan 2026-07-16 00:04:26 -04:00
parent a115726e71
commit f763ebff5b
11 changed files with 101 additions and 3 deletions

View File

@ -1,9 +1,10 @@
-- +goose Up
alter table album add column blur_hash varchar;
-- blur_hash is not null default '' so NULLs never reach the Go string field; '' means "not computed".
alter table album add column blur_hash varchar not null default '';
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 varchar not null default '';
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 varchar not null default '';
alter table playlist add column blur_hash_updated_at datetime;
-- +goose Down

View File

@ -156,6 +156,7 @@ type AlbumRepository interface {
Exists(id string) (bool, error)
Put(*Album) error
UpdateExternalInfo(*Album) error
UpdateBlurHash(id string, blurHash string, artworkUpdatedAt time.Time) error
Get(id string) (*Album, error)
GetAll(...QueryOptions) (Albums, error)
GetCursor(...QueryOptions) (AlbumCursor, error)

View File

@ -103,6 +103,7 @@ type ArtistRepository interface {
Exists(id string) (bool, error)
Put(m *Artist, colsToUpdate ...string) error
UpdateExternalInfo(a *Artist) error
UpdateBlurHash(id string, blurHash string, artworkUpdatedAt time.Time) error
Get(id string) (*Artist, error)
GetAll(options ...QueryOptions) (Artists, error)
GetCursor(options ...QueryOptions) (ArtistCursor, error)

View File

@ -142,6 +142,7 @@ type PlaylistRepository interface {
GetAll(options ...QueryOptions) (Playlists, error)
GetCursor(options ...QueryOptions) (PlaylistCursor, error)
FindByPath(path string) (*Playlist, error)
UpdateBlurHash(id string, blurHash string, artworkUpdatedAt time.Time) error
Delete(id string) error
Tracks(playlistId string, refreshSmartPlaylist bool) PlaylistTrackRepository
GetPlaylists(mediaFileId string) (Playlists, error)

View File

@ -213,6 +213,16 @@ func (r *albumRepository) Put(al *model.Album) error {
return nil
}
// UpdateBlurHash is a targeted update: a full-row put would race with the scanner. Deliberately
// a plain UPDATE with no insert fallback — updating a just-deleted row must be a silent no-op.
func (r *albumRepository) UpdateBlurHash(id, blurHash string, artworkUpdatedAt time.Time) error {
upd := Update(r.tableName).Where(Eq{"id": id}).
Set("blur_hash", blurHash).
Set("blur_hash_updated_at", artworkUpdatedAt)
_, err := r.executeSQL(upd)
return err
}
// TODO Move external metadata to a separated table
func (r *albumRepository) UpdateExternalInfo(al *model.Album) error {
_, err := r.put(al.ID, &dbAlbum{Album: al}, "description", "small_image_url", "medium_image_url", "large_image_url", "external_url", "external_info_updated_at")

View File

@ -899,3 +899,34 @@ func _p(id, name string, sortName ...string) model.Participant {
}
return p
}
var _ = Describe("AlbumRepository.UpdateBlurHash", func() {
var repo model.AlbumRepository
BeforeEach(func() {
ctx := request.WithUser(GinkgoT().Context(), model.User{ID: "userid", UserName: "johndoe"})
repo = NewAlbumRepository(ctx, GetDBXBuilder())
DeferCleanup(func() {
_, err := GetDBXBuilder().NewQuery("update album set blur_hash = '', blur_hash_updated_at = null").Execute()
Expect(err).ToNot(HaveOccurred())
})
})
It("persists the hash and its artwork version snapshot", func() {
al, err := repo.Get("103")
Expect(err).ToNot(HaveOccurred())
Expect(al.BlurHash).To(BeEmpty())
version := time.Date(2024, 5, 1, 10, 30, 0, 0, time.UTC)
Expect(repo.UpdateBlurHash(al.ID, "LKO2?U%2Tw=w]~RBVZRi};RPxuwH", version)).To(Succeed())
updated, err := repo.Get(al.ID)
Expect(err).ToNot(HaveOccurred())
Expect(updated.BlurHash).To(Equal("LKO2?U%2Tw=w]~RBVZRi};RPxuwH"))
Expect(updated.BlurHashUpdatedAt).ToNot(BeNil())
// Round-trip through SQLite must preserve equality — the DTO layer compares with Equal.
Expect(updated.BlurHashUpdatedAt.Equal(version)).To(BeTrue())
// The targeted update must not touch the row's own timestamps.
Expect(updated.UpdatedAt).To(Equal(al.UpdatedAt))
})
})

View File

@ -232,6 +232,16 @@ func (r *artistRepository) Put(a *model.Artist, colsToUpdate ...string) error {
return err
}
// UpdateBlurHash is a targeted update: a full-row put would race with the scanner. Deliberately
// a plain UPDATE with no insert fallback — updating a just-deleted row must be a silent no-op.
func (r *artistRepository) UpdateBlurHash(id, blurHash string, artworkUpdatedAt time.Time) error {
upd := Update(r.tableName).Where(Eq{"id": id}).
Set("blur_hash", blurHash).
Set("blur_hash_updated_at", artworkUpdatedAt)
_, err := r.executeSQL(upd)
return err
}
func (r *artistRepository) UpdateExternalInfo(a *model.Artist) error {
dba := &dbArtist{Artist: a}
_, err := r.put(a.ID, dba,

View File

@ -155,6 +155,16 @@ func (r *playlistRepository) GetWithTracks(id string, refreshSmartPlaylist, incl
return pls, nil
}
// UpdateBlurHash is a targeted update: a full-row put would race with playlist sync. Deliberately
// a plain UPDATE with no insert fallback — updating a just-deleted row must be a silent no-op.
func (r *playlistRepository) UpdateBlurHash(id, blurHash string, artworkUpdatedAt time.Time) error {
upd := Update(r.tableName).Where(Eq{"id": id}).
Set("blur_hash", blurHash).
Set("blur_hash_updated_at", artworkUpdatedAt)
_, err := r.executeSQL(upd)
return err
}
func (r *playlistRepository) FindByPath(path string) (*model.Playlist, error) {
return r.findBy(Eq{"path": path})
}

View File

@ -29,6 +29,17 @@ func (m *MockAlbumRepo) SetError(err bool) {
m.Err = err
}
func (m *MockAlbumRepo) UpdateBlurHash(id, blurHash string, artworkUpdatedAt time.Time) error {
if m.Err {
return errors.New("unexpected error")
}
if al, ok := m.Data[id]; ok {
al.BlurHash = blurHash
al.BlurHashUpdatedAt = &artworkUpdatedAt
}
return nil
}
func (m *MockAlbumRepo) SetData(albums model.Albums) {
m.Data = make(map[string]*model.Album, len(albums))
m.All = albums

View File

@ -32,6 +32,17 @@ func (m *MockArtistRepo) SetData(artists model.Artists) {
}
}
func (m *MockArtistRepo) UpdateBlurHash(id, blurHash string, artworkUpdatedAt time.Time) error {
if m.Err {
return errors.New("unexpected error")
}
if ar, ok := m.Data[id]; ok {
ar.BlurHash = blurHash
ar.BlurHashUpdatedAt = &artworkUpdatedAt
}
return nil
}
func (m *MockArtistRepo) Exists(id string) (bool, error) {
if m.Err {
return false, errors.New("Error!")

View File

@ -42,6 +42,17 @@ func (m *MockPlaylistRepo) SetData(playlists model.Playlists) {
}
}
func (m *MockPlaylistRepo) UpdateBlurHash(id, blurHash string, artworkUpdatedAt time.Time) error {
if m.Err {
return errors.New("unexpected error")
}
if pl, ok := m.Data[id]; ok {
pl.BlurHash = blurHash
pl.BlurHashUpdatedAt = &artworkUpdatedAt
}
return nil
}
func (m *MockPlaylistRepo) GetAll(options ...model.QueryOptions) (model.Playlists, error) {
if len(options) > 0 {
m.Options = options[0]