mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-31 07:30:32 +00:00
feat(artwork): implement item_artwork repository with batched hydration
This commit is contained in:
parent
f926539c04
commit
14dd57052e
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
. "github.com/Masterminds/squirrel"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/pocketbase/dbx"
|
||||
)
|
||||
@ -21,23 +22,72 @@ func NewItemArtworkRepository(ctx context.Context, db dbx.Builder) model.ItemArt
|
||||
}
|
||||
|
||||
func (r *itemArtworkRepository) Get(kind, id, imageType string) (*model.ItemArtwork, error) {
|
||||
return nil, model.ErrNotFound
|
||||
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 {
|
||||
return nil
|
||||
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 nil
|
||||
return r.delete(Eq{"item_kind": kind, "item_id": id})
|
||||
}
|
||||
|
||||
func (r *itemArtworkRepository) GetInfoForItems(kind string, ids []string) (map[string]model.ItemArtworkInfo, error) {
|
||||
return nil, nil
|
||||
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) {
|
||||
return 0, nil
|
||||
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)
|
||||
|
||||
83
persistence/item_artwork_repository_test.go
Normal file
83
persistence/item_artwork_repository_test.go
Normal file
@ -0,0 +1,83 @@
|
||||
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))
|
||||
})
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user