mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-14 13:27:09 +02:00
feat(artwork): add read-only queue and absent counters
Both are needed by the artwork status CLI: a queue breakdown by kind and priority, and the absent totals split against the recheck cutoff.
This commit is contained in:
parent
482ed50d7d
commit
7f97107d45
4 changed files with 124 additions and 0 deletions
|
|
@ -149,6 +149,22 @@ type ArtworkQueueRepository interface {
|
|||
// DeleteIfUnchanged deletes only while retry_at still matches, sparing a concurrent re-enqueue.
|
||||
DeleteIfUnchanged(kind, id, imageType string, retryAt time.Time) error
|
||||
Count() (int64, error)
|
||||
// CountByKindAndPriority reports the pending queue rows grouped by kind and priority.
|
||||
CountByKindAndPriority() ([]ArtworkQueueStat, error)
|
||||
// CountAbsent reports the absent states of a kind, and how many of those EnqueueStaleAbsent
|
||||
// would pick up at the given cutoff.
|
||||
CountAbsent(kind Kind, attemptedBefore time.Time) (ArtworkAbsentStat, error)
|
||||
// PurgeDangling removes queue rows whose entity no longer exists.
|
||||
PurgeDangling() (int64, error)
|
||||
}
|
||||
|
||||
type ArtworkQueueStat struct {
|
||||
ItemKind string
|
||||
Priority int
|
||||
Count int64
|
||||
}
|
||||
|
||||
type ArtworkAbsentStat struct {
|
||||
Total int64
|
||||
Stale int64
|
||||
}
|
||||
|
|
|
|||
|
|
@ -194,4 +194,20 @@ func (r *artworkQueueRepository) Count() (int64, error) {
|
|||
return res.Count, err
|
||||
}
|
||||
|
||||
func (r *artworkQueueRepository) CountByKindAndPriority() ([]model.ArtworkQueueStat, error) {
|
||||
var res []model.ArtworkQueueStat
|
||||
err := r.queryAll(Select("item_kind", "priority", "count(*) as count").From(r.tableName).
|
||||
GroupBy("item_kind", "priority").OrderBy("item_kind", "priority desc"), &res)
|
||||
return res, err
|
||||
}
|
||||
|
||||
// CountAbsent matches EnqueueStaleAbsent on hash, so the stale count is what a recheck would queue.
|
||||
func (r *artworkQueueRepository) CountAbsent(kind model.Kind, attemptedBefore time.Time) (model.ArtworkAbsentStat, error) {
|
||||
var res model.ArtworkAbsentStat
|
||||
err := r.queryOne(Select("count(*) as total").
|
||||
Column(Expr("coalesce(sum(attempted_at < ?), 0) as stale", attemptedBefore)).
|
||||
From(itemArtworkTable).Where(Eq{"item_kind": kind.Prefix(), "hash": ""}), &res)
|
||||
return res, err
|
||||
}
|
||||
|
||||
var _ model.ArtworkQueueRepository = (*artworkQueueRepository)(nil)
|
||||
|
|
|
|||
|
|
@ -380,4 +380,53 @@ var _ = Describe("ArtworkQueueRepository", func() {
|
|||
}
|
||||
Expect(count).To(Equal(1), "the already-queued row must not be duplicated")
|
||||
})
|
||||
|
||||
Describe("status counters", func() {
|
||||
It("groups queue rows by kind and priority", func() {
|
||||
Expect(repo.Enqueue(item("ar", "a1", model.ArtworkPriorityBackfill))).To(Succeed())
|
||||
Expect(repo.Enqueue(item("ar", "a2", model.ArtworkPriorityBackfill))).To(Succeed())
|
||||
Expect(repo.Enqueue(item("ar", "a3", model.ArtworkPriorityBump))).To(Succeed())
|
||||
Expect(repo.Enqueue(item("al", "b1", model.ArtworkPriorityScan))).To(Succeed())
|
||||
|
||||
Expect(repo.CountByKindAndPriority()).To(ConsistOf(
|
||||
model.ArtworkQueueStat{ItemKind: "ar", Priority: model.ArtworkPriorityBackfill, Count: 2},
|
||||
model.ArtworkQueueStat{ItemKind: "ar", Priority: model.ArtworkPriorityBump, Count: 1},
|
||||
model.ArtworkQueueStat{ItemKind: "al", Priority: model.ArtworkPriorityScan, Count: 1},
|
||||
))
|
||||
})
|
||||
|
||||
It("reports an empty queue as no rows", func() {
|
||||
Expect(repo.CountByKindAndPriority()).To(BeEmpty())
|
||||
})
|
||||
|
||||
It("counts absent states and how many are due for recheck", func() {
|
||||
awRepo := NewArtworkRepository(context.Background(), GetDBXBuilder())
|
||||
old := time.Now().Add(-48 * time.Hour)
|
||||
for _, ia := range []model.ItemArtwork{
|
||||
{ItemKind: "ar", ItemID: "stale1", ImageType: model.ImageTypePrimary, Hash: "", AttemptedAt: old},
|
||||
{ItemKind: "ar", ItemID: "fresh1", ImageType: model.ImageTypePrimary, Hash: "", AttemptedAt: time.Now()},
|
||||
{ItemKind: "ar", ItemID: "found1", ImageType: model.ImageTypePrimary, Hash: "hX", AttemptedAt: old},
|
||||
{ItemKind: "al", ItemID: "stale2", ImageType: model.ImageTypePrimary, Hash: "", AttemptedAt: old},
|
||||
} {
|
||||
Expect(awRepo.PutItemArtwork(&ia)).To(Succeed())
|
||||
}
|
||||
|
||||
Expect(repo.CountAbsent(model.KindArtistArtwork, time.Now().Add(-24*time.Hour))).
|
||||
To(Equal(model.ArtworkAbsentStat{Total: 2, Stale: 1}))
|
||||
})
|
||||
|
||||
It("reports a kind with no absent state as zero, not as an error", func() {
|
||||
Expect(repo.CountAbsent(model.KindRadioArtwork, time.Now())).To(Equal(model.ArtworkAbsentStat{}))
|
||||
})
|
||||
|
||||
It("counts without enqueueing", func() {
|
||||
awRepo := NewArtworkRepository(context.Background(), GetDBXBuilder())
|
||||
Expect(awRepo.PutItemArtwork(&model.ItemArtwork{ItemKind: "ar", ItemID: "ar9",
|
||||
ImageType: model.ImageTypePrimary, Hash: "", AttemptedAt: time.Now()})).To(Succeed())
|
||||
|
||||
_, err := repo.CountAbsent(model.KindArtistArtwork, time.Now())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(repo.Count()).To(BeZero())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -174,6 +174,49 @@ func (m *MockArtworkQueueRepo) Count() (int64, error) {
|
|||
return int64(len(m.Data)), nil
|
||||
}
|
||||
|
||||
func (m *MockArtworkQueueRepo) CountByKindAndPriority() ([]model.ArtworkQueueStat, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if m.Err != nil {
|
||||
return nil, m.Err
|
||||
}
|
||||
var res []model.ArtworkQueueStat
|
||||
for _, it := range m.Data {
|
||||
i := slices.IndexFunc(res, func(s model.ArtworkQueueStat) bool {
|
||||
return s.ItemKind == it.ItemKind && s.Priority == it.Priority
|
||||
})
|
||||
if i < 0 {
|
||||
res = append(res, model.ArtworkQueueStat{ItemKind: it.ItemKind, Priority: it.Priority, Count: 1})
|
||||
continue
|
||||
}
|
||||
res[i].Count++
|
||||
}
|
||||
slices.SortFunc(res, func(a, b model.ArtworkQueueStat) int {
|
||||
return cmp.Or(cmp.Compare(a.ItemKind, b.ItemKind), cmp.Compare(b.Priority, a.Priority))
|
||||
})
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// CountAbsent mirrors the SQL predicate: an absent state is one with no hash.
|
||||
func (m *MockArtworkQueueRepo) CountAbsent(kind model.Kind, attemptedBefore time.Time) (model.ArtworkAbsentStat, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
var res model.ArtworkAbsentStat
|
||||
if m.Err != nil || m.ItemArtworkSource == nil {
|
||||
return res, m.Err
|
||||
}
|
||||
for _, ia := range m.ItemArtworkSource.ItemData {
|
||||
if ia.ItemKind != kind.Prefix() || ia.Hash != "" {
|
||||
continue
|
||||
}
|
||||
res.Total++
|
||||
if ia.AttemptedAt.Before(attemptedBefore) {
|
||||
res.Stale++
|
||||
}
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (m *MockArtworkQueueRepo) EnqueuePreservingBackoff(items ...model.ArtworkQueueItem) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue