package tests
import (
"context"
"maps"
"sync"
"time"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/utils/slice"
)
type MockArtworkRepo struct {
model.ArtworkRepository
mu sync.Mutex
Data map[string]model.Artwork
ItemData map[string]model.ItemArtwork
Err error
ExistingIDs map[string]map[string]bool
}
func CreateMockArtworkRepo() *MockArtworkRepo {
return &MockArtworkRepo{Data: map[string]model.Artwork{}, ItemData: map[string]model.ItemArtwork{}}
}
func iaKey(kind, id, imageType string) string { return kind + "|" + id + "|" + imageType }
func (m *MockArtworkRepo) GetImage(_ context.Context, hash string) (*model.Artwork, error) {
m.mu.Lock()
defer m.mu.Unlock()
if m.Err != nil {
return nil, m.Err
}
if a, ok := m.Data[hash]; ok {
return &a, nil
}
return nil, model.ErrNotFound
}
func (m *MockArtworkRepo) PutImage(_ context.Context, a *model.Artwork) error {
m.mu.Lock()
defer m.mu.Unlock()
if m.Err != nil {
return m.Err
}
a.CreatedAt = time.Now()
m.Data[a.Hash] = *a
return nil
}
func (m *MockArtworkRepo) GetMimeByHash(context.Context) (map[string]string, error) {
m.mu.Lock()
defer m.mu.Unlock()
if m.Err != nil {
return nil, m.Err
}
mimes := make(map[string]string, len(m.Data))
for h, a := range m.Data {
mimes[h] = a.Mime
}
return mimes, nil
}
func (m *MockArtworkRepo) PurgeDanglingItems(context.Context) (int64, error) {
m.mu.Lock()
defer m.mu.Unlock()
if m.Err != nil {
return 0, m.Err
}
var purged int64
for k, ia := range m.ItemData {
existing := m.ExistingIDs[ia.ItemKind]
if existing == nil {
continue
}
if !existing[ia.ItemID] {
delete(m.ItemData, k)
purged++
}
}
return purged, nil
}
func (m *MockArtworkRepo) PurgeOrphans(_ context.Context, createdBefore time.Time) (int64, error) {
m.mu.Lock()
defer m.mu.Unlock()
if m.Err != nil {
return 0, m.Err
}
var deleted int64
for h, a := range m.Data {
if m.referenced(h) || !a.CreatedAt.Before(createdBefore) {
continue
}
delete(m.Data, h)
deleted++
}
return deleted, nil
}
func (m *MockArtworkRepo) referenced(hash string) bool {
for _, ia := range m.ItemData {
if ia.Hash == hash {
return true
}
}
return false
}
func (m *MockArtworkRepo) GetItemArtwork(_ context.Context, kind model.Kind, id, imageType string) (*model.ItemArtwork, error) {
m.mu.Lock()
defer m.mu.Unlock()
if m.Err != nil {
return nil, m.Err
}
if ia, ok := m.ItemData[iaKey(kind.Prefix(), id, imageType)]; ok {
return &ia, nil
}
return nil, model.ErrNotFound
}
func (m *MockArtworkRepo) PutLastFailure(_ context.Context, kind model.Kind, id, imageType, trace string) error {
m.mu.Lock()
defer m.mu.Unlock()
if m.Err != nil {
return m.Err
}
key := iaKey(kind.Prefix(), id, imageType)
if ia, ok := m.ItemData[key]; ok {
ia.LastFailure = trace
m.ItemData[key] = ia
}
return nil
}
func (m *MockArtworkRepo) PutItemArtwork(_ context.Context, ia *model.ItemArtwork) error {
m.mu.Lock()
defer m.mu.Unlock()
if m.Err != nil {
return m.Err
}
if ia.ImageType == "" {
ia.ImageType = model.ImageTypePrimary
}
ia.UpdatedAt = time.Now()
if ia.AttemptedAt.IsZero() {
ia.AttemptedAt = ia.UpdatedAt
}
m.ItemData[iaKey(ia.ItemKind, ia.ItemID, ia.ImageType)] = *ia
return nil
}
func (m *MockArtworkRepo) DeleteForItems(_ context.Context, kind model.Kind, ids []string) error {
m.mu.Lock()
defer m.mu.Unlock()
if m.Err != nil {
return m.Err
}
idSet := slice.ToSet(ids)
maps.DeleteFunc(m.ItemData, func(_ string, ia model.ItemArtwork) bool {
_, ok := idSet[ia.ItemID]
return ok && ia.ItemKind == kind.Prefix()
})
return nil
}
func (m *MockArtworkRepo) GetInfoForItems(_ context.Context, kind model.Kind, ids []string) (map[string]model.ItemArtworkInfo, error) {
m.mu.Lock()
defer m.mu.Unlock()
if m.Err != nil {
return nil, m.Err
}
res := map[string]model.ItemArtworkInfo{}
for _, id := range ids {
if ia, ok := m.ItemData[iaKey(kind.Prefix(), id, model.ImageTypePrimary)]; ok {
info := model.ItemArtworkInfo{ItemID: id, Hash: ia.Hash}
if a, ok := m.Data[ia.Hash]; ok {
info.BlurHash, info.ThumbHash = a.BlurHash, a.ThumbHash
info.DominantColor = a.DominantColor
info.Width, info.Height = a.Width, a.Height
}
res[id] = info
}
}
return res, nil
}