refactor(artwork): merge item artwork state into ArtworkRepository

This commit is contained in:
Deluan 2026-07-21 23:14:01 -04:00
parent b16ef725c9
commit 4f835437a9
12 changed files with 232 additions and 304 deletions

View File

@ -19,11 +19,11 @@ func Prune(ctx context.Context, ds model.DataStore, store *ImageStore) error {
return err
}
if len(orphans) > 0 {
arts, err := repo.GetBatch(orphans)
arts, err := repo.GetImages(orphans)
if err != nil {
return err
}
if err := repo.Delete(orphans...); err != nil {
if err := repo.DeleteImages(orphans...); err != nil {
return err
}
for _, a := range arts {
@ -35,7 +35,7 @@ func Prune(ctx context.Context, ds model.DataStore, store *ImageStore) error {
}
removed, err := store.Sweep(func(hash string) bool {
_, err := repo.Get(hash)
_, err := repo.GetImage(hash)
return !errors.Is(err, model.ErrNotFound)
})
if err != nil {

View File

@ -17,7 +17,7 @@ type flakyGetArtworkRepo struct {
*tests.MockArtworkRepo
}
func (f *flakyGetArtworkRepo) Get(string) (*model.Artwork, error) {
func (f *flakyGetArtworkRepo) GetImage(string) (*model.Artwork, error) {
return nil, errors.New("db locked")
}
@ -36,18 +36,18 @@ var _ = Describe("Prune", func() {
data := []byte("orphan-bytes")
h, _ := HashImage(bytes.NewReader(data))
Expect(store.Write(h, "image/jpeg", bytes.NewReader(data))).To(Succeed())
Expect(awRepo.Put(&model.Artwork{Hash: h, Mime: "image/jpeg",
Expect(awRepo.PutImage(&model.Artwork{Hash: h, Mime: "image/jpeg",
CreatedAt: time.Now().Add(-2 * time.Hour)})).To(Succeed())
awRepo.OrphanHashes = []string{h}
kept := []byte("kept-bytes")
hk, _ := HashImage(bytes.NewReader(kept))
Expect(store.Write(hk, "image/jpeg", bytes.NewReader(kept))).To(Succeed())
Expect(awRepo.Put(&model.Artwork{Hash: hk, Mime: "image/jpeg"})).To(Succeed())
Expect(awRepo.PutImage(&model.Artwork{Hash: hk, Mime: "image/jpeg"})).To(Succeed())
Expect(Prune(context.Background(), ds, store)).To(Succeed())
_, err := awRepo.Get(h)
_, err := awRepo.GetImage(h)
Expect(err).To(MatchError(model.ErrNotFound))
_, err = store.Open(h, "image/jpeg")
Expect(os.IsNotExist(err)).To(BeTrue())

View File

@ -55,21 +55,17 @@ const (
)
type ArtworkRepository interface {
Get(hash string) (*Artwork, error)
Put(a *Artwork) error
GetBatch(hashes []string) (map[string]Artwork, error)
// GetOrphanHashes returns hashes referenced by no item_artwork row and older than cutoff.
// Image identity (artwork table)
GetImage(hash string) (*Artwork, error)
PutImage(a *Artwork) error
GetImages(hashes []string) (map[string]Artwork, error)
GetOrphanHashes(createdBefore time.Time) ([]string, error)
Delete(hashes ...string) error
}
type ItemArtworkRepository interface {
Get(kind, id, imageType string) (*ItemArtwork, error)
Put(ia *ItemArtwork) error
DeleteImages(hashes ...string) error
// Per-item state (item_artwork table)
GetItemArtwork(kind, id, imageType string) (*ItemArtwork, error)
PutItemArtwork(ia *ItemArtwork) error
DeleteForItem(kind, id string) error
// GetInfoForItems hydrates a page: one batched query, item_artwork joined to artwork.
GetInfoForItems(kind string, ids []string) (map[string]ItemArtworkInfo, error)
// EnqueueStaleAbsent inserts queue rows (priority Recheck) for absent states older than cutoff.
EnqueueStaleAbsent(kind string, attemptedBefore time.Time) (int64, error)
}

View File

@ -41,7 +41,6 @@ type DataStore interface {
Scrobble(ctx context.Context) ScrobbleRepository
Plugin(ctx context.Context) PluginRepository
Artwork(ctx context.Context) ArtworkRepository
ItemArtwork(ctx context.Context) ItemArtworkRepository
ArtworkQueue(ctx context.Context) ArtworkQueueRepository
Resource(ctx context.Context, model any) ResourceRepository

View File

@ -21,7 +21,7 @@ func NewArtworkRepository(ctx context.Context, db dbx.Builder) model.ArtworkRepo
return r
}
func (r *artworkRepository) Get(hash string) (*model.Artwork, error) {
func (r *artworkRepository) GetImage(hash string) (*model.Artwork, error) {
sel := Select("*").From(r.tableName).Where(Eq{"hash": hash})
var res model.Artwork
if err := r.queryOne(sel, &res); err != nil {
@ -30,7 +30,7 @@ func (r *artworkRepository) Get(hash string) (*model.Artwork, error) {
return &res, nil
}
func (r *artworkRepository) Put(a *model.Artwork) error {
func (r *artworkRepository) PutImage(a *model.Artwork) error {
if a.CreatedAt.IsZero() {
a.CreatedAt = time.Now()
}
@ -45,7 +45,7 @@ func (r *artworkRepository) Put(a *model.Artwork) error {
return err
}
func (r *artworkRepository) GetBatch(hashes []string) (map[string]model.Artwork, error) {
func (r *artworkRepository) GetImages(hashes []string) (map[string]model.Artwork, error) {
res := map[string]model.Artwork{}
if len(hashes) == 0 {
return res, nil
@ -72,11 +72,82 @@ func (r *artworkRepository) GetOrphanHashes(createdBefore time.Time) ([]string,
return hashes, err
}
func (r *artworkRepository) Delete(hashes ...string) error {
func (r *artworkRepository) DeleteImages(hashes ...string) error {
if len(hashes) == 0 {
return nil
}
return r.delete(Eq{"hash": hashes})
}
func (r *artworkRepository) GetItemArtwork(kind, id, imageType string) (*model.ItemArtwork, error) {
sel := Select("*").From("item_artwork").
Where(Eq{"item_kind": kind, "item_id": id, "image_type": imageType})
var res model.ItemArtwork
if err := r.queryOne(sel, &res); err != nil {
return nil, err
}
return &res, nil
}
func (r *artworkRepository) PutItemArtwork(ia *model.ItemArtwork) error {
if ia.ImageType == "" {
ia.ImageType = model.ImageTypePrimary
}
ia.UpdatedAt = time.Now()
ins := Insert("item_artwork").SetMap(map[string]any{
"item_kind": ia.ItemKind, "item_id": ia.ItemID, "image_type": ia.ImageType,
"hash": ia.Hash, "source": ia.Source,
"attempted_at": ia.AttemptedAt, "updated_at": ia.UpdatedAt,
}).Suffix(`ON CONFLICT (item_kind, item_id, image_type) DO UPDATE SET
hash=excluded.hash, source=excluded.source,
attempted_at=excluded.attempted_at, updated_at=excluded.updated_at`)
_, err := r.executeSQL(ins)
return err
}
func (r *artworkRepository) DeleteForItem(kind, id string) error {
del := Delete("item_artwork").Where(Eq{"item_kind": kind, "item_id": id})
_, err := r.executeSQL(del)
return err
}
func (r *artworkRepository) GetInfoForItems(kind string, ids []string) (map[string]model.ItemArtworkInfo, error) {
res := map[string]model.ItemArtworkInfo{}
if len(ids) == 0 {
return res, nil
}
sel := Select("ia.item_id", "ia.hash", "COALESCE(a.blur_hash, '') as blur_hash").
From("item_artwork ia").
LeftJoin("artwork a ON a.hash = ia.hash").
Where(And{
Eq{"ia.item_kind": kind},
Eq{"ia.image_type": model.ImageTypePrimary},
Eq{"ia.item_id": ids},
})
var rows []struct {
ItemID string
Hash string
BlurHash string
}
if err := r.queryAll(sel, &rows); err != nil {
return nil, err
}
for _, row := range rows {
res[row.ItemID] = model.ItemArtworkInfo{
ItemID: row.ItemID, Hash: row.Hash, BlurHash: row.BlurHash, Absent: row.Hash == "",
}
}
return res, nil
}
func (r *artworkRepository) EnqueueStaleAbsent(kind string, attemptedBefore time.Time) (int64, error) {
now := time.Now()
ins := Expr(`INSERT INTO artwork_queue (item_kind, item_id, image_type, priority, attempts, retry_at, enqueued_at)
SELECT item_kind, item_id, image_type, ?, 0, ?, ?
FROM item_artwork WHERE item_kind = ? AND hash = '' AND attempted_at < ?
ON CONFLICT (item_kind, item_id, image_type) DO NOTHING`,
model.ArtworkPriorityRecheck, now, now, kind, attemptedBefore)
return r.executeSQL(ins)
}
var _ model.ArtworkRepository = (*artworkRepository)(nil)

View File

@ -28,9 +28,9 @@ var _ = Describe("ArtworkRepository", func() {
It("stores and retrieves an artwork by hash", func() {
a := &model.Artwork{Hash: "abc123", Mime: "image/jpeg", Width: 500, Height: 500, SizeBytes: 1234, BlurHash: "LKO2?U%2Tw=w"}
Expect(repo.Put(a)).To(Succeed())
Expect(repo.PutImage(a)).To(Succeed())
got, err := repo.Get("abc123")
got, err := repo.GetImage("abc123")
Expect(err).ToNot(HaveOccurred())
Expect(got.Mime).To(Equal("image/jpeg"))
Expect(got.BlurHash).To(Equal("LKO2?U%2Tw=w"))
@ -39,32 +39,31 @@ var _ = Describe("ArtworkRepository", func() {
It("is idempotent on Put (upsert by hash)", func() {
a := &model.Artwork{Hash: "dup1", Mime: "image/png"}
Expect(repo.Put(a)).To(Succeed())
Expect(repo.PutImage(a)).To(Succeed())
a.BlurHash = "XYZ"
Expect(repo.Put(a)).To(Succeed())
got, _ := repo.Get("dup1")
Expect(repo.PutImage(a)).To(Succeed())
got, _ := repo.GetImage("dup1")
Expect(got.BlurHash).To(Equal("XYZ"))
})
It("returns ErrNotFound for a missing hash", func() {
_, err := repo.Get("nope")
_, err := repo.GetImage("nope")
Expect(err).To(MatchError(model.ErrNotFound))
})
It("fetches a batch", func() {
Expect(repo.Put(&model.Artwork{Hash: "b1", Mime: "image/jpeg"})).To(Succeed())
Expect(repo.Put(&model.Artwork{Hash: "b2", Mime: "image/png"})).To(Succeed())
got, err := repo.GetBatch([]string{"b1", "b2", "missing"})
Expect(repo.PutImage(&model.Artwork{Hash: "b1", Mime: "image/jpeg"})).To(Succeed())
Expect(repo.PutImage(&model.Artwork{Hash: "b2", Mime: "image/png"})).To(Succeed())
got, err := repo.GetImages([]string{"b1", "b2", "missing"})
Expect(err).ToNot(HaveOccurred())
Expect(got).To(HaveLen(2))
Expect(got["b2"].Mime).To(Equal("image/png"))
})
It("finds orphans older than cutoff, honoring item_artwork references", func() {
Expect(repo.Put(&model.Artwork{Hash: "orph1", Mime: "image/jpeg"})).To(Succeed())
Expect(repo.Put(&model.Artwork{Hash: "ref1", Mime: "image/jpeg"})).To(Succeed())
iaRepo := NewItemArtworkRepository(context.Background(), GetDBXBuilder())
Expect(iaRepo.Put(&model.ItemArtwork{ItemKind: "al", ItemID: "a1", ImageType: model.ImageTypePrimary, Hash: "ref1", Source: "folder"})).To(Succeed())
Expect(repo.PutImage(&model.Artwork{Hash: "orph1", Mime: "image/jpeg"})).To(Succeed())
Expect(repo.PutImage(&model.Artwork{Hash: "ref1", Mime: "image/jpeg"})).To(Succeed())
Expect(repo.PutItemArtwork(&model.ItemArtwork{ItemKind: "al", ItemID: "a1", ImageType: model.ImageTypePrimary, Hash: "ref1", Source: "folder"})).To(Succeed())
orphans, err := repo.GetOrphanHashes(time.Now().Add(time.Minute))
Expect(err).ToNot(HaveOccurred())
@ -77,9 +76,80 @@ var _ = Describe("ArtworkRepository", func() {
})
It("deletes by hashes", func() {
Expect(repo.Put(&model.Artwork{Hash: "d1", Mime: "image/jpeg"})).To(Succeed())
Expect(repo.Delete("d1")).To(Succeed())
_, err := repo.Get("d1")
Expect(repo.PutImage(&model.Artwork{Hash: "d1", Mime: "image/jpeg"})).To(Succeed())
Expect(repo.DeleteImages("d1")).To(Succeed())
_, err := repo.GetImage("d1")
Expect(err).To(MatchError(model.ErrNotFound))
})
})
var _ = Describe("ArtworkRepository item state", func() {
var repo model.ArtworkRepository
BeforeEach(func() {
clearArtworkTables()
repo = NewArtworkRepository(context.Background(), GetDBXBuilder())
})
It("upserts and reads state", func() {
ia := &model.ItemArtwork{ItemKind: "al", ItemID: "al1", ImageType: model.ImageTypePrimary,
Hash: "h1", Source: "folder", AttemptedAt: time.Now()}
Expect(repo.PutItemArtwork(ia)).To(Succeed())
ia.Source = "embedded"
Expect(repo.PutItemArtwork(ia)).To(Succeed())
got, err := repo.GetItemArtwork("al", "al1", model.ImageTypePrimary)
Expect(err).ToNot(HaveOccurred())
Expect(got.Source).To(Equal("embedded"))
Expect(got.UpdatedAt).ToNot(BeZero())
})
It("represents known-absent as empty hash", func() {
Expect(repo.PutItemArtwork(&model.ItemArtwork{ItemKind: "ar", ItemID: "ar1",
ImageType: model.ImageTypePrimary, Hash: "", AttemptedAt: time.Now()})).To(Succeed())
got, err := repo.GetItemArtwork("ar", "ar1", model.ImageTypePrimary)
Expect(err).ToNot(HaveOccurred())
Expect(got.Hash).To(BeEmpty())
})
It("hydrates a page in one batch, including blurhash and absence", func() {
Expect(repo.PutImage(&model.Artwork{Hash: "h9", Mime: "image/jpeg", BlurHash: "BH9"})).To(Succeed())
Expect(repo.PutItemArtwork(&model.ItemArtwork{ItemKind: "al", ItemID: "x1", ImageType: model.ImageTypePrimary, Hash: "h9", Source: "folder"})).To(Succeed())
Expect(repo.PutItemArtwork(&model.ItemArtwork{ItemKind: "al", ItemID: "x2", ImageType: model.ImageTypePrimary, Hash: "", Source: ""})).To(Succeed())
info, err := repo.GetInfoForItems("al", []string{"x1", "x2", "x3"})
Expect(err).ToNot(HaveOccurred())
Expect(info).To(HaveLen(2))
Expect(info["x1"].Hash).To(Equal("h9"))
Expect(info["x1"].BlurHash).To(Equal("BH9"))
Expect(info["x1"].Absent).To(BeFalse())
Expect(info["x2"].Absent).To(BeTrue())
_, unresolved := info["x3"]
Expect(unresolved).To(BeFalse())
})
It("deletes all rows for an item", func() {
Expect(repo.PutItemArtwork(&model.ItemArtwork{ItemKind: "pl", ItemID: "p1", ImageType: model.ImageTypePrimary, Hash: "h1"})).To(Succeed())
Expect(repo.DeleteForItem("pl", "p1")).To(Succeed())
_, err := repo.GetItemArtwork("pl", "p1", model.ImageTypePrimary)
Expect(err).To(MatchError(model.ErrNotFound))
})
It("enqueues stale absent states for recheck", func() {
old := time.Now().Add(-48 * time.Hour)
Expect(repo.PutItemArtwork(&model.ItemArtwork{ItemKind: "ar", ItemID: "stale1", ImageType: model.ImageTypePrimary, Hash: "", AttemptedAt: old})).To(Succeed())
Expect(repo.PutItemArtwork(&model.ItemArtwork{ItemKind: "ar", ItemID: "fresh1", ImageType: model.ImageTypePrimary, Hash: "", AttemptedAt: time.Now()})).To(Succeed())
Expect(repo.PutItemArtwork(&model.ItemArtwork{ItemKind: "ar", ItemID: "found1", ImageType: model.ImageTypePrimary, Hash: "hX", AttemptedAt: old})).To(Succeed())
n, err := repo.EnqueueStaleAbsent("ar", time.Now().Add(-24*time.Hour))
Expect(err).ToNot(HaveOccurred())
Expect(n).To(Equal(int64(1)))
qRepo := NewArtworkQueueRepository(context.Background(), GetDBXBuilder())
items, err := qRepo.DequeueBatch(10)
Expect(err).ToNot(HaveOccurred())
Expect(items).To(HaveLen(1))
Expect(items[0].ItemID).To(Equal("stale1"))
Expect(items[0].Priority).To(Equal(model.ArtworkPriorityRecheck))
})
})

View File

@ -1,93 +0,0 @@
package persistence
import (
"context"
"time"
. "github.com/Masterminds/squirrel"
"github.com/navidrome/navidrome/model"
"github.com/pocketbase/dbx"
)
type itemArtworkRepository struct {
sqlRepository
}
func NewItemArtworkRepository(ctx context.Context, db dbx.Builder) model.ItemArtworkRepository {
r := &itemArtworkRepository{}
r.ctx = ctx
r.db = db
r.tableName = "item_artwork"
return r
}
func (r *itemArtworkRepository) Get(kind, id, imageType string) (*model.ItemArtwork, error) {
sel := Select("*").From(r.tableName).
Where(Eq{"item_kind": kind, "item_id": id, "image_type": imageType})
var res model.ItemArtwork
if err := r.queryOne(sel, &res); err != nil {
return nil, err
}
return &res, nil
}
func (r *itemArtworkRepository) Put(ia *model.ItemArtwork) error {
if ia.ImageType == "" {
ia.ImageType = model.ImageTypePrimary
}
ia.UpdatedAt = time.Now()
ins := Insert(r.tableName).SetMap(map[string]any{
"item_kind": ia.ItemKind, "item_id": ia.ItemID, "image_type": ia.ImageType,
"hash": ia.Hash, "source": ia.Source,
"attempted_at": ia.AttemptedAt, "updated_at": ia.UpdatedAt,
}).Suffix(`ON CONFLICT (item_kind, item_id, image_type) DO UPDATE SET
hash=excluded.hash, source=excluded.source,
attempted_at=excluded.attempted_at, updated_at=excluded.updated_at`)
_, err := r.executeSQL(ins)
return err
}
func (r *itemArtworkRepository) DeleteForItem(kind, id string) error {
return r.delete(Eq{"item_kind": kind, "item_id": id})
}
func (r *itemArtworkRepository) GetInfoForItems(kind string, ids []string) (map[string]model.ItemArtworkInfo, error) {
res := map[string]model.ItemArtworkInfo{}
if len(ids) == 0 {
return res, nil
}
sel := Select("ia.item_id", "ia.hash", "COALESCE(a.blur_hash, '') as blur_hash").
From(r.tableName + " ia").
LeftJoin("artwork a ON a.hash = ia.hash").
Where(And{
Eq{"ia.item_kind": kind},
Eq{"ia.image_type": model.ImageTypePrimary},
Eq{"ia.item_id": ids},
})
var rows []struct {
ItemID string
Hash string
BlurHash string
}
if err := r.queryAll(sel, &rows); err != nil {
return nil, err
}
for _, row := range rows {
res[row.ItemID] = model.ItemArtworkInfo{
ItemID: row.ItemID, Hash: row.Hash, BlurHash: row.BlurHash, Absent: row.Hash == "",
}
}
return res, nil
}
func (r *itemArtworkRepository) EnqueueStaleAbsent(kind string, attemptedBefore time.Time) (int64, error) {
now := time.Now()
ins := Expr(`INSERT INTO artwork_queue (item_kind, item_id, image_type, priority, attempts, retry_at, enqueued_at)
SELECT item_kind, item_id, image_type, ?, 0, ?, ?
FROM item_artwork WHERE item_kind = ? AND hash = '' AND attempted_at < ?
ON CONFLICT (item_kind, item_id, image_type) DO NOTHING`,
model.ArtworkPriorityRecheck, now, now, kind, attemptedBefore)
return r.executeSQL(ins)
}
var _ model.ItemArtworkRepository = (*itemArtworkRepository)(nil)

View File

@ -1,83 +0,0 @@
package persistence
import (
"context"
"time"
"github.com/navidrome/navidrome/model"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("ItemArtworkRepository", func() {
var repo model.ItemArtworkRepository
var awRepo model.ArtworkRepository
BeforeEach(func() {
clearArtworkTables()
repo = NewItemArtworkRepository(context.Background(), GetDBXBuilder())
awRepo = NewArtworkRepository(context.Background(), GetDBXBuilder())
})
It("upserts and reads state", func() {
ia := &model.ItemArtwork{ItemKind: "al", ItemID: "al1", ImageType: model.ImageTypePrimary,
Hash: "h1", Source: "folder", AttemptedAt: time.Now()}
Expect(repo.Put(ia)).To(Succeed())
ia.Source = "embedded"
Expect(repo.Put(ia)).To(Succeed())
got, err := repo.Get("al", "al1", model.ImageTypePrimary)
Expect(err).ToNot(HaveOccurred())
Expect(got.Source).To(Equal("embedded"))
Expect(got.UpdatedAt).ToNot(BeZero())
})
It("represents known-absent as empty hash", func() {
Expect(repo.Put(&model.ItemArtwork{ItemKind: "ar", ItemID: "ar1",
ImageType: model.ImageTypePrimary, Hash: "", AttemptedAt: time.Now()})).To(Succeed())
got, err := repo.Get("ar", "ar1", model.ImageTypePrimary)
Expect(err).ToNot(HaveOccurred())
Expect(got.Hash).To(BeEmpty())
})
It("hydrates a page in one batch, including blurhash and absence", func() {
Expect(awRepo.Put(&model.Artwork{Hash: "h9", Mime: "image/jpeg", BlurHash: "BH9"})).To(Succeed())
Expect(repo.Put(&model.ItemArtwork{ItemKind: "al", ItemID: "x1", ImageType: model.ImageTypePrimary, Hash: "h9", Source: "folder"})).To(Succeed())
Expect(repo.Put(&model.ItemArtwork{ItemKind: "al", ItemID: "x2", ImageType: model.ImageTypePrimary, Hash: "", Source: ""})).To(Succeed())
info, err := repo.GetInfoForItems("al", []string{"x1", "x2", "x3"})
Expect(err).ToNot(HaveOccurred())
Expect(info).To(HaveLen(2))
Expect(info["x1"].Hash).To(Equal("h9"))
Expect(info["x1"].BlurHash).To(Equal("BH9"))
Expect(info["x1"].Absent).To(BeFalse())
Expect(info["x2"].Absent).To(BeTrue())
_, unresolved := info["x3"]
Expect(unresolved).To(BeFalse())
})
It("deletes all rows for an item", func() {
Expect(repo.Put(&model.ItemArtwork{ItemKind: "pl", ItemID: "p1", ImageType: model.ImageTypePrimary, Hash: "h1"})).To(Succeed())
Expect(repo.DeleteForItem("pl", "p1")).To(Succeed())
_, err := repo.Get("pl", "p1", model.ImageTypePrimary)
Expect(err).To(MatchError(model.ErrNotFound))
})
It("enqueues stale absent states for recheck", func() {
old := time.Now().Add(-48 * time.Hour)
Expect(repo.Put(&model.ItemArtwork{ItemKind: "ar", ItemID: "stale1", ImageType: model.ImageTypePrimary, Hash: "", AttemptedAt: old})).To(Succeed())
Expect(repo.Put(&model.ItemArtwork{ItemKind: "ar", ItemID: "fresh1", ImageType: model.ImageTypePrimary, Hash: "", AttemptedAt: time.Now()})).To(Succeed())
Expect(repo.Put(&model.ItemArtwork{ItemKind: "ar", ItemID: "found1", ImageType: model.ImageTypePrimary, Hash: "hX", AttemptedAt: old})).To(Succeed())
n, err := repo.EnqueueStaleAbsent("ar", time.Now().Add(-24*time.Hour))
Expect(err).ToNot(HaveOccurred())
Expect(n).To(Equal(int64(1)))
qRepo := NewArtworkQueueRepository(context.Background(), GetDBXBuilder())
items, err := qRepo.DequeueBatch(10)
Expect(err).ToNot(HaveOccurred())
Expect(items).To(HaveLen(1))
Expect(items[0].ItemID).To(Equal("stale1"))
Expect(items[0].Priority).To(Equal(model.ArtworkPriorityRecheck))
})
})

View File

@ -101,10 +101,6 @@ func (s *SQLStore) Artwork(ctx context.Context) model.ArtworkRepository {
return NewArtworkRepository(ctx, s.getDBXBuilder())
}
func (s *SQLStore) ItemArtwork(ctx context.Context) model.ItemArtworkRepository {
return NewItemArtworkRepository(ctx, s.getDBXBuilder())
}
func (s *SQLStore) ArtworkQueue(ctx context.Context) model.ArtworkQueueRepository {
return NewArtworkQueueRepository(ctx, s.getDBXBuilder())
}

View File

@ -9,15 +9,18 @@ import (
type MockArtworkRepo struct {
model.ArtworkRepository
Data map[string]model.Artwork
ItemData map[string]model.ItemArtwork // key: kind + "|" + id + "|" + imageType
OrphanHashes []string
Err error
}
func CreateMockArtworkRepo() *MockArtworkRepo {
return &MockArtworkRepo{Data: map[string]model.Artwork{}}
return &MockArtworkRepo{Data: map[string]model.Artwork{}, ItemData: map[string]model.ItemArtwork{}}
}
func (m *MockArtworkRepo) Get(hash string) (*model.Artwork, error) {
func iaKey(kind, id, imageType string) string { return kind + "|" + id + "|" + imageType }
func (m *MockArtworkRepo) GetImage(hash string) (*model.Artwork, error) {
if m.Err != nil {
return nil, m.Err
}
@ -27,7 +30,7 @@ func (m *MockArtworkRepo) Get(hash string) (*model.Artwork, error) {
return nil, model.ErrNotFound
}
func (m *MockArtworkRepo) Put(a *model.Artwork) error {
func (m *MockArtworkRepo) PutImage(a *model.Artwork) error {
if m.Err != nil {
return m.Err
}
@ -38,7 +41,7 @@ func (m *MockArtworkRepo) Put(a *model.Artwork) error {
return nil
}
func (m *MockArtworkRepo) GetBatch(hashes []string) (map[string]model.Artwork, error) {
func (m *MockArtworkRepo) GetImages(hashes []string) (map[string]model.Artwork, error) {
if m.Err != nil {
return nil, m.Err
}
@ -55,7 +58,7 @@ func (m *MockArtworkRepo) GetOrphanHashes(createdBefore time.Time) ([]string, er
return m.OrphanHashes, m.Err
}
func (m *MockArtworkRepo) Delete(hashes ...string) error {
func (m *MockArtworkRepo) DeleteImages(hashes ...string) error {
if m.Err != nil {
return m.Err
}
@ -64,3 +67,51 @@ func (m *MockArtworkRepo) Delete(hashes ...string) error {
}
return nil
}
func (m *MockArtworkRepo) GetItemArtwork(kind, id, imageType string) (*model.ItemArtwork, error) {
if m.Err != nil {
return nil, m.Err
}
if ia, ok := m.ItemData[iaKey(kind, id, imageType)]; ok {
return &ia, nil
}
return nil, model.ErrNotFound
}
func (m *MockArtworkRepo) PutItemArtwork(ia *model.ItemArtwork) error {
if m.Err != nil {
return m.Err
}
ia.UpdatedAt = time.Now()
m.ItemData[iaKey(ia.ItemKind, ia.ItemID, ia.ImageType)] = *ia
return nil
}
func (m *MockArtworkRepo) DeleteForItem(kind, id string) error {
if m.Err != nil {
return m.Err
}
for k, ia := range m.ItemData {
if ia.ItemKind == kind && ia.ItemID == id {
delete(m.ItemData, k)
}
}
return nil
}
func (m *MockArtworkRepo) GetInfoForItems(kind string, ids []string) (map[string]model.ItemArtworkInfo, error) {
if m.Err != nil {
return nil, m.Err
}
res := map[string]model.ItemArtworkInfo{}
for _, id := range ids {
if ia, ok := m.ItemData[iaKey(kind, id, model.ImageTypePrimary)]; ok {
res[id] = model.ItemArtworkInfo{ItemID: id, Hash: ia.Hash, Absent: ia.Hash == ""}
}
}
return res, nil
}
func (m *MockArtworkRepo) EnqueueStaleAbsent(kind string, attemptedBefore time.Time) (int64, error) {
return 0, m.Err
}

View File

@ -29,7 +29,6 @@ type MockDataStore struct {
MockedRadio model.RadioRepository
MockedPlugin model.PluginRepository
MockedArtwork model.ArtworkRepository
MockedItemArtwork model.ItemArtworkRepository
MockedArtworkQueue model.ArtworkQueueRepository
scrobbleBufferMu sync.Mutex
repoMu sync.Mutex
@ -261,17 +260,6 @@ func (db *MockDataStore) Artwork(ctx context.Context) model.ArtworkRepository {
return db.MockedArtwork
}
func (db *MockDataStore) ItemArtwork(ctx context.Context) model.ItemArtworkRepository {
if db.MockedItemArtwork != nil {
return db.MockedItemArtwork
}
if db.RealDS != nil {
return db.RealDS.ItemArtwork(ctx)
}
db.MockedItemArtwork = CreateMockItemArtworkRepo()
return db.MockedItemArtwork
}
func (db *MockDataStore) ArtworkQueue(ctx context.Context) model.ArtworkQueueRepository {
if db.MockedArtworkQueue != nil {
return db.MockedArtworkQueue

View File

@ -1,67 +0,0 @@
package tests
import (
"time"
"github.com/navidrome/navidrome/model"
)
type MockItemArtworkRepo struct {
model.ItemArtworkRepository
Data map[string]model.ItemArtwork // key: kind + "|" + id + "|" + imageType
Err error
}
func CreateMockItemArtworkRepo() *MockItemArtworkRepo {
return &MockItemArtworkRepo{Data: map[string]model.ItemArtwork{}}
}
func iaKey(kind, id, imageType string) string { return kind + "|" + id + "|" + imageType }
func (m *MockItemArtworkRepo) Get(kind, id, imageType string) (*model.ItemArtwork, error) {
if m.Err != nil {
return nil, m.Err
}
if ia, ok := m.Data[iaKey(kind, id, imageType)]; ok {
return &ia, nil
}
return nil, model.ErrNotFound
}
func (m *MockItemArtworkRepo) Put(ia *model.ItemArtwork) error {
if m.Err != nil {
return m.Err
}
ia.UpdatedAt = time.Now()
m.Data[iaKey(ia.ItemKind, ia.ItemID, ia.ImageType)] = *ia
return nil
}
func (m *MockItemArtworkRepo) DeleteForItem(kind, id string) error {
if m.Err != nil {
return m.Err
}
for k, ia := range m.Data {
if ia.ItemKind == kind && ia.ItemID == id {
delete(m.Data, k)
}
}
return nil
}
func (m *MockItemArtworkRepo) GetInfoForItems(kind string, ids []string) (map[string]model.ItemArtworkInfo, error) {
if m.Err != nil {
return nil, m.Err
}
res := map[string]model.ItemArtworkInfo{}
for _, id := range ids {
if ia, ok := m.Data[iaKey(kind, id, model.ImageTypePrimary)]; ok {
res[id] = model.ItemArtworkInfo{ItemID: id, Hash: ia.Hash, Absent: ia.Hash == ""}
}
}
return res, nil
}
func (m *MockItemArtworkRepo) EnqueueStaleAbsent(kind string, attemptedBefore time.Time) (int64, error) {
return 0, m.Err
}