mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-31 07:30:32 +00:00
refactor(artwork): thread model.Kind through the artwork API
Entity-level artwork queries now take a typed model.Kind instead of a bare prefix string. GetItemArtwork, DeleteForItem(s), GetInfoForItems, EnqueueStaleAbsent, hydrateItemImages, enqueueBackfillKind and artwork.Refresh convert to the prefix string only at the two real boundaries: the SQL item_kind column (kind.Prefix() inside each repo) and external string inputs (a new model.ParseKind for the nativeapi URL param, which also validates it). The Backfill/stale-absent kind slices, the resolve.go dispatch switch, and the kind→resource / kind→table lookup maps now use the Kind vars directly. The queue lifecycle methods (MarkFailed/Delete*) keep string kinds — they operate on a dequeued item's raw ItemKind column, which stays a string field, always populated via kind.Prefix(). Removes every bare "al"/"ar"/… prefix literal from non-test code (27 -> 0); behavior is unchanged.
This commit is contained in:
parent
53babc7a2a
commit
61cbd2f5f9
@ -42,13 +42,13 @@ var _ = Describe("Acquisition → serve loop", func() {
|
||||
)
|
||||
|
||||
// itemFound reports whether the worker has persisted a resolved (hash-bearing) state row.
|
||||
itemFound := func(kind, id string) func() bool {
|
||||
itemFound := func(kind model.Kind, id string) func() bool {
|
||||
return func() bool {
|
||||
ia, err := artRepo.GetItemArtwork(kind, id, model.ImageTypePrimary)
|
||||
return err == nil && ia.Hash != ""
|
||||
}
|
||||
}
|
||||
itemAbsent := func(kind, id string) func() bool {
|
||||
itemAbsent := func(kind model.Kind, id string) func() bool {
|
||||
return func() bool {
|
||||
ia, err := artRepo.GetItemArtwork(kind, id, model.ImageTypePrimary)
|
||||
return err == nil && ia.Hash == ""
|
||||
@ -115,9 +115,9 @@ var _ = Describe("Acquisition → serve loop", func() {
|
||||
It("acquires album folder art and serves the exact bytes under its hash", func() {
|
||||
seedFolderAlbum("al1")
|
||||
worker.Bump("al", "al1")
|
||||
runWorkerUntil(ctx, worker, itemFound("al", "al1"))
|
||||
runWorkerUntil(ctx, worker, itemFound(model.KindAlbumArtwork, "al1"))
|
||||
|
||||
ia, err := artRepo.GetItemArtwork("al", "al1", model.ImageTypePrimary)
|
||||
ia, err := artRepo.GetItemArtwork(model.KindAlbumArtwork, "al1", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(ia.Source).To(Equal("folder"))
|
||||
|
||||
@ -132,9 +132,9 @@ var _ = Describe("Acquisition → serve loop", func() {
|
||||
name := writeUpload(consts.EntityArtist, "artist-e2e.png", artistPngFixture)
|
||||
artistRepo.SetData(model.Artists{{ID: "ar1", Name: "Artist", UploadedImage: name}})
|
||||
worker.Bump("ar", "ar1")
|
||||
runWorkerUntil(ctx, worker, itemFound("ar", "ar1"))
|
||||
runWorkerUntil(ctx, worker, itemFound(model.KindArtistArtwork, "ar1"))
|
||||
|
||||
ia, err := artRepo.GetItemArtwork("ar", "ar1", model.ImageTypePrimary)
|
||||
ia, err := artRepo.GetItemArtwork(model.KindArtistArtwork, "ar1", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(ia.Source).To(Equal("upload"))
|
||||
|
||||
@ -149,9 +149,9 @@ var _ = Describe("Acquisition → serve loop", func() {
|
||||
plRepo.SetData(model.Playlists{{ID: "pl1", Name: "Playlist"}})
|
||||
plRepo.TracksRepo = &tests.MockPlaylistTrackRepo{AlbumIDs: []string{"al1"}}
|
||||
worker.Bump("pl", "pl1")
|
||||
runWorkerUntil(ctx, worker, itemFound("pl", "pl1"))
|
||||
runWorkerUntil(ctx, worker, itemFound(model.KindPlaylistArtwork, "pl1"))
|
||||
|
||||
ia, err := artRepo.GetItemArtwork("pl", "pl1", model.ImageTypePrimary)
|
||||
ia, err := artRepo.GetItemArtwork(model.KindPlaylistArtwork, "pl1", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(ia.Source).To(Equal("generated"))
|
||||
|
||||
@ -169,9 +169,9 @@ var _ = Describe("Acquisition → serve loop", func() {
|
||||
name := writeUpload(consts.EntityRadio, "radio-e2e.jpg", coverFixture)
|
||||
radioRepo.Data["ra1"] = &model.Radio{ID: "ra1", Name: "Station", UploadedImage: name}
|
||||
worker.Bump("ra", "ra1")
|
||||
runWorkerUntil(ctx, worker, itemFound("ra", "ra1"))
|
||||
runWorkerUntil(ctx, worker, itemFound(model.KindRadioArtwork, "ra1"))
|
||||
|
||||
ia, err := artRepo.GetItemArtwork("ra", "ra1", model.ImageTypePrimary)
|
||||
ia, err := artRepo.GetItemArtwork(model.KindRadioArtwork, "ra1", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(ia.Source).To(Equal("upload"))
|
||||
|
||||
@ -194,12 +194,12 @@ var _ = Describe("Acquisition → serve loop", func() {
|
||||
provisionalBytes := readAll(provisional)
|
||||
Expect(len(provisionalBytes)).To(BeNumerically(">", 0))
|
||||
|
||||
_, err = artRepo.GetItemArtwork("mf", "mf1", model.ImageTypePrimary)
|
||||
_, err = artRepo.GetItemArtwork(model.KindMediaFileArtwork, "mf1", model.ImageTypePrimary)
|
||||
Expect(err).To(MatchError(model.ErrNotFound), "provisional serving must not write a state row")
|
||||
|
||||
// The provisional read enqueued a Bump; drain it and confirm the persisted hash matches.
|
||||
runWorkerUntil(ctx, worker, itemFound("mf", "mf1"))
|
||||
ia, err := artRepo.GetItemArtwork("mf", "mf1", model.ImageTypePrimary)
|
||||
runWorkerUntil(ctx, worker, itemFound(model.KindMediaFileArtwork, "mf1"))
|
||||
ia, err := artRepo.GetItemArtwork(model.KindMediaFileArtwork, "mf1", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(ia.Source).To(Equal("embedded"))
|
||||
Expect(ia.Hash).To(Equal(provisional.Hash))
|
||||
@ -214,7 +214,7 @@ var _ = Describe("Acquisition → serve loop", func() {
|
||||
It("records an absent state for an entity with no art and reports it unavailable", func() {
|
||||
albumRepo.SetData(model.Albums{{ID: "alx", Name: "Artless", LibraryID: 0}})
|
||||
worker.Bump("al", "alx")
|
||||
runWorkerUntil(ctx, worker, itemAbsent("al", "alx"))
|
||||
runWorkerUntil(ctx, worker, itemAbsent(model.KindAlbumArtwork, "alx"))
|
||||
|
||||
_, err := svc.Get(ctx, model.MustParseArtworkID("al-alx"), 0, false)
|
||||
Expect(err).To(MatchError(artwork.ErrUnavailable))
|
||||
|
||||
@ -22,7 +22,9 @@ const FingerprintPropertyKey = "artwork.fingerprint"
|
||||
const staleAbsentAge = 24 * time.Hour
|
||||
|
||||
// staleAbsentKinds are the item kinds eligible for the periodic stale-absent recheck.
|
||||
var staleAbsentKinds = []string{"ar", "al", "pl", "ra"}
|
||||
var staleAbsentKinds = []model.Kind{
|
||||
model.KindArtistArtwork, model.KindAlbumArtwork, model.KindPlaylistArtwork, model.KindRadioArtwork,
|
||||
}
|
||||
|
||||
// Fingerprint summarizes the config knobs that affect artwork resolution outcomes; a
|
||||
// change means previously resolved (or absent) state may no longer be correct.
|
||||
@ -50,13 +52,13 @@ func Backfill(ctx context.Context, ds model.DataStore) (bool, error) {
|
||||
|
||||
// Artists first: few entities, most external-dependent, so they get queue headstart.
|
||||
kinds := []struct {
|
||||
kind string
|
||||
kind model.Kind
|
||||
fetch func() ([]string, error)
|
||||
}{
|
||||
{"ar", func() ([]string, error) { return ds.Artist(ctx).GetAllIDs() }},
|
||||
{"al", func() ([]string, error) { return ds.Album(ctx).GetAllIDs() }},
|
||||
{"pl", func() ([]string, error) { return ds.Playlist(ctx).GetAllIDs() }},
|
||||
{"ra", func() ([]string, error) { return ds.Radio(ctx).GetAllIDs() }},
|
||||
{model.KindArtistArtwork, func() ([]string, error) { return ds.Artist(ctx).GetAllIDs() }},
|
||||
{model.KindAlbumArtwork, func() ([]string, error) { return ds.Album(ctx).GetAllIDs() }},
|
||||
{model.KindPlaylistArtwork, func() ([]string, error) { return ds.Playlist(ctx).GetAllIDs() }},
|
||||
{model.KindRadioArtwork, func() ([]string, error) { return ds.Radio(ctx).GetAllIDs() }},
|
||||
}
|
||||
for _, k := range kinds {
|
||||
ids, err := k.fetch()
|
||||
@ -75,14 +77,14 @@ func Backfill(ctx context.Context, ds model.DataStore) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func enqueueBackfillKind(ctx context.Context, ds model.DataStore, kind string, ids []string) error {
|
||||
func enqueueBackfillKind(ctx context.Context, ds model.DataStore, kind model.Kind, ids []string) error {
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
items := make([]model.ArtworkQueueItem, len(ids))
|
||||
for i, id := range ids {
|
||||
items[i] = model.ArtworkQueueItem{
|
||||
ItemKind: kind, ItemID: id, ImageType: model.ImageTypePrimary, Priority: model.ArtworkPriorityBackfill,
|
||||
ItemKind: kind.Prefix(), ItemID: id, ImageType: model.ImageTypePrimary, Priority: model.ArtworkPriorityBackfill,
|
||||
}
|
||||
}
|
||||
return ds.ArtworkQueue(ctx).Enqueue(items...)
|
||||
|
||||
@ -86,7 +86,7 @@ var _ = Describe("processItem", func() {
|
||||
out := processItem(ctx, deps, model.ArtworkQueueItem{ItemKind: "al", ItemID: "al1"})
|
||||
Expect(out).To(Equal(outcomeFound))
|
||||
|
||||
ia, err := artRepo.GetItemArtwork("al", "al1", model.ImageTypePrimary)
|
||||
ia, err := artRepo.GetItemArtwork(model.KindAlbumArtwork, "al1", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(ia.Hash).ToNot(BeEmpty())
|
||||
Expect(ia.Source).To(Equal("folder"))
|
||||
@ -108,7 +108,7 @@ var _ = Describe("processItem", func() {
|
||||
out := processItem(ctx, deps, model.ArtworkQueueItem{ItemKind: "al", ItemID: "al2"})
|
||||
Expect(out).To(Equal(outcomeFound))
|
||||
|
||||
ia, err := artRepo.GetItemArtwork("al", "al2", model.ImageTypePrimary)
|
||||
ia, err := artRepo.GetItemArtwork(model.KindAlbumArtwork, "al2", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(ia.Source).To(Equal("embedded"))
|
||||
Expect(filepath.ToSlash(ia.SourcePath)).To(HaveSuffix("tests/fixtures/artist/an-album/test.mp3"))
|
||||
@ -131,7 +131,7 @@ var _ = Describe("processItem", func() {
|
||||
out := processItem(ctx, deps, model.ArtworkQueueItem{ItemKind: "al", ItemID: "al3"})
|
||||
Expect(out).To(Equal(outcomeAbsent))
|
||||
|
||||
ia, err := artRepo.GetItemArtwork("al", "al3", model.ImageTypePrimary)
|
||||
ia, err := artRepo.GetItemArtwork(model.KindAlbumArtwork, "al3", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(ia.Hash).To(BeEmpty())
|
||||
Expect(ia.Source).To(BeEmpty())
|
||||
@ -148,7 +148,7 @@ var _ = Describe("processItem", func() {
|
||||
out := processItem(ctx, deps, model.ArtworkQueueItem{ItemKind: "al", ItemID: "al4"})
|
||||
Expect(out).To(Equal(outcomeFailed))
|
||||
|
||||
_, err := artRepo.GetItemArtwork("al", "al4", model.ImageTypePrimary)
|
||||
_, err := artRepo.GetItemArtwork(model.KindAlbumArtwork, "al4", model.ImageTypePrimary)
|
||||
Expect(err).To(MatchError(model.ErrNotFound))
|
||||
})
|
||||
|
||||
@ -166,7 +166,7 @@ var _ = Describe("processItem", func() {
|
||||
out := processItem(ctx, deps, model.ArtworkQueueItem{ItemKind: "al", ItemID: "alstale"})
|
||||
Expect(out).To(Equal(outcomeFoundStale))
|
||||
|
||||
ia, err := artRepo.GetItemArtwork("al", "alstale", model.ImageTypePrimary)
|
||||
ia, err := artRepo.GetItemArtwork(model.KindAlbumArtwork, "alstale", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(ia.Hash).ToNot(BeEmpty())
|
||||
Expect(ia.Source).To(Equal("folder"))
|
||||
@ -187,7 +187,7 @@ var _ = Describe("processItem", func() {
|
||||
out := processItem(ctx, deps, model.ArtworkQueueItem{ItemKind: "al", ItemID: "alext"})
|
||||
Expect(out).To(Equal(outcomeFound))
|
||||
|
||||
ia, err := artRepo.GetItemArtwork("al", "alext", model.ImageTypePrimary)
|
||||
ia, err := artRepo.GetItemArtwork(model.KindAlbumArtwork, "alext", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(ia.Source).To(Equal("external:deezerFake"))
|
||||
Expect(ia.Hash).ToNot(BeEmpty())
|
||||
@ -212,7 +212,7 @@ var _ = Describe("processItem", func() {
|
||||
|
||||
out1 := processItem(ctx, deps, model.ArtworkQueueItem{ItemKind: "al", ItemID: "al5"})
|
||||
Expect(out1).To(Equal(outcomeFound))
|
||||
ia1, err := artRepo.GetItemArtwork("al", "al5", model.ImageTypePrimary)
|
||||
ia1, err := artRepo.GetItemArtwork(model.KindAlbumArtwork, "al5", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Poison the stored blurhash: if the second item re-decodes instead of
|
||||
@ -223,7 +223,7 @@ var _ = Describe("processItem", func() {
|
||||
|
||||
out2 := processItem(ctx, deps, model.ArtworkQueueItem{ItemKind: "al", ItemID: "al6"})
|
||||
Expect(out2).To(Equal(outcomeFound))
|
||||
ia2, err := artRepo.GetItemArtwork("al", "al6", model.ImageTypePrimary)
|
||||
ia2, err := artRepo.GetItemArtwork(model.KindAlbumArtwork, "al6", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(ia2.Hash).To(Equal(ia1.Hash))
|
||||
|
||||
@ -253,7 +253,7 @@ var _ = Describe("processItem", func() {
|
||||
|
||||
folderRepo.result = []model.Folder{{Path: "album-a", ImageFiles: []string{"cover.jpg"}}}
|
||||
Expect(processItem(ctx, deps, model.ArtworkQueueItem{ItemKind: "al", ItemID: "alA"})).To(Equal(outcomeFound))
|
||||
iaA, err := artRepo.GetItemArtwork("al", "alA", model.ImageTypePrimary)
|
||||
iaA, err := artRepo.GetItemArtwork(model.KindAlbumArtwork, "alA", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(iaA.Source).To(Equal("folder"))
|
||||
Expect(filepath.ToSlash(iaA.SourcePath)).To(HaveSuffix("album-a/cover.jpg"))
|
||||
@ -266,14 +266,14 @@ var _ = Describe("processItem", func() {
|
||||
|
||||
folderRepo.result = []model.Folder{{Path: "album-b", ImageFiles: []string{"cover.jpg"}}}
|
||||
Expect(processItem(ctx, deps, model.ArtworkQueueItem{ItemKind: "al", ItemID: "alB"})).To(Equal(outcomeFound))
|
||||
iaB, err := artRepo.GetItemArtwork("al", "alB", model.ImageTypePrimary)
|
||||
iaB, err := artRepo.GetItemArtwork(model.KindAlbumArtwork, "alB", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(iaB.Hash).To(Equal(iaA.Hash))
|
||||
Expect(filepath.ToSlash(iaB.SourcePath)).To(HaveSuffix("album-b/cover.jpg"))
|
||||
Expect(iaB.RefMtime).To(Equal(time.Unix(2000, 0).UnixNano()))
|
||||
|
||||
// The first item's provenance survives the second item processing identical bytes.
|
||||
iaAafter, err := artRepo.GetItemArtwork("al", "alA", model.ImageTypePrimary)
|
||||
iaAafter, err := artRepo.GetItemArtwork(model.KindAlbumArtwork, "alA", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(filepath.ToSlash(iaAafter.SourcePath)).To(HaveSuffix("album-a/cover.jpg"))
|
||||
Expect(iaAafter.RefMtime).To(Equal(time.Unix(1000, 0).UnixNano()))
|
||||
@ -299,7 +299,7 @@ var _ = Describe("processItem", func() {
|
||||
out := processItem(ctx, deps, model.ArtworkQueueItem{ItemKind: "ra", ItemID: "ra1"})
|
||||
Expect(out).To(Equal(outcomeFailed))
|
||||
|
||||
_, err := artRepo.GetItemArtwork("ra", "ra1", model.ImageTypePrimary)
|
||||
_, err := artRepo.GetItemArtwork(model.KindRadioArtwork, "ra1", model.ImageTypePrimary)
|
||||
Expect(err).To(MatchError(model.ErrNotFound))
|
||||
})
|
||||
|
||||
@ -320,7 +320,7 @@ var _ = Describe("processItem", func() {
|
||||
out := processItem(ctx, deps, model.ArtworkQueueItem{ItemKind: "ra", ItemID: "big"})
|
||||
Expect(out).To(Equal(outcomeFailed))
|
||||
|
||||
_, err = artRepo.GetItemArtwork("ra", "big", model.ImageTypePrimary)
|
||||
_, err = artRepo.GetItemArtwork(model.KindRadioArtwork, "big", model.ImageTypePrimary)
|
||||
Expect(err).To(MatchError(model.ErrNotFound))
|
||||
})
|
||||
|
||||
@ -345,7 +345,7 @@ var _ = Describe("processItem", func() {
|
||||
out := processItem(ctx, deps, model.ArtworkQueueItem{ItemKind: "al", ItemID: "al7"})
|
||||
Expect(out).To(Equal(outcomeFailed))
|
||||
|
||||
_, err := artRepo.GetItemArtwork("al", "al7", model.ImageTypePrimary)
|
||||
_, err := artRepo.GetItemArtwork(model.KindAlbumArtwork, "al7", model.ImageTypePrimary)
|
||||
Expect(err).To(MatchError(model.ErrNotFound))
|
||||
})
|
||||
})
|
||||
|
||||
@ -51,11 +51,11 @@ var _ = Describe("Prune", func() {
|
||||
|
||||
Expect(Prune(context.Background(), ds, store)).To(Succeed())
|
||||
|
||||
_, err := awRepo.GetItemArtwork("al", "gone-album", model.ImageTypePrimary)
|
||||
_, err := awRepo.GetItemArtwork(model.KindAlbumArtwork, "gone-album", model.ImageTypePrimary)
|
||||
Expect(err).To(MatchError(model.ErrNotFound))
|
||||
_, err = awRepo.GetItemArtwork("ar", "gone-artist", model.ImageTypePrimary)
|
||||
_, err = awRepo.GetItemArtwork(model.KindArtistArtwork, "gone-artist", model.ImageTypePrimary)
|
||||
Expect(err).To(MatchError(model.ErrNotFound))
|
||||
_, err = awRepo.GetItemArtwork("ar", "live-artist", model.ImageTypePrimary)
|
||||
_, err = awRepo.GetItemArtwork(model.KindArtistArtwork, "live-artist", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
|
||||
|
||||
@ -9,11 +9,11 @@ import (
|
||||
|
||||
// Refresh clears an item's resolved artwork state and re-queues it at Bump priority, so a
|
||||
// deliberate refresh (image upload or manual re-resolve) drops the current pick and re-resolves it.
|
||||
func Refresh(ctx context.Context, ds model.DataStore, kind, id string) error {
|
||||
func Refresh(ctx context.Context, ds model.DataStore, kind model.Kind, id string) error {
|
||||
if err := ds.Artwork(ctx).DeleteForItem(kind, id); err != nil {
|
||||
return fmt.Errorf("clearing artwork state: %w", err)
|
||||
}
|
||||
item := model.ArtworkQueueItem{ItemKind: kind, ItemID: id, ImageType: model.ImageTypePrimary, Priority: model.ArtworkPriorityBump}
|
||||
item := model.ArtworkQueueItem{ItemKind: kind.Prefix(), ItemID: id, ImageType: model.ImageTypePrimary, Priority: model.ArtworkPriorityBump}
|
||||
if err := ds.ArtworkQueue(ctx).Enqueue(item); err != nil {
|
||||
return fmt.Errorf("enqueuing artwork refresh: %w", err)
|
||||
}
|
||||
|
||||
@ -50,16 +50,17 @@ func resolveItemMode(ctx context.Context, ds model.DataStore, ag *agents.Agents,
|
||||
if gate == nil {
|
||||
gate = passthroughGate
|
||||
}
|
||||
switch item.ItemKind {
|
||||
case "al":
|
||||
kind, _ := model.ParseKind(item.ItemKind)
|
||||
switch kind {
|
||||
case model.KindAlbumArtwork:
|
||||
return resolveAlbum(ctx, ds, ag, ffmpeg, item.ItemID, gate, localOnly)
|
||||
case "ar":
|
||||
case model.KindArtistArtwork:
|
||||
return resolveArtist(ctx, ds, ag, ffmpeg, item.ItemID, gate, localOnly)
|
||||
case "pl":
|
||||
case model.KindPlaylistArtwork:
|
||||
return resolvePlaylist(ctx, ds, ag, ffmpeg, item.ItemID, gate, localOnly)
|
||||
case "ra":
|
||||
case model.KindRadioArtwork:
|
||||
return resolveRadio(ctx, ds, item.ItemID)
|
||||
case "mf":
|
||||
case model.KindMediaFileArtwork:
|
||||
return resolveMediaFile(ctx, ds, ffmpeg, item.ItemID)
|
||||
default:
|
||||
return resolution{}, fmt.Errorf("resolveItem: kind %q is not resolvable by the worker", item.ItemKind)
|
||||
|
||||
@ -97,7 +97,7 @@ const requestRecheckAge = time.Hour
|
||||
// found row serves its hash, absent row is unavailable (promoting a stale recheck on view),
|
||||
// missing row reads through provisionally.
|
||||
func (s *service) serveEntity(ctx context.Context, artID model.ArtworkID, size int, square bool) (*Image, error) {
|
||||
ia, err := s.ds.Artwork(ctx).GetItemArtwork(artID.Kind.Prefix(), artID.ID, model.ImageTypePrimary)
|
||||
ia, err := s.ds.Artwork(ctx).GetItemArtwork(artID.Kind, artID.ID, model.ImageTypePrimary)
|
||||
switch {
|
||||
case errors.Is(err, model.ErrNotFound):
|
||||
return s.provisional(ctx, artID, size, square)
|
||||
@ -255,7 +255,7 @@ func (s *service) serveMediaFile(ctx context.Context, artID model.ArtworkID, siz
|
||||
}
|
||||
return s.Get(ctx, mf.DiscCoverArtID(), size, square)
|
||||
}
|
||||
ia, err := s.ds.Artwork(ctx).GetItemArtwork("mf", artID.ID, model.ImageTypePrimary)
|
||||
ia, err := s.ds.Artwork(ctx).GetItemArtwork(model.KindMediaFileArtwork, artID.ID, model.ImageTypePrimary)
|
||||
switch {
|
||||
case err == nil && ia.Hash != "":
|
||||
return s.serveHash(ctx, artID, ia, size, square)
|
||||
|
||||
@ -163,7 +163,7 @@ var _ = Describe("Service", func() {
|
||||
_, err := svc.Get(ctx, model.MustParseArtworkID("al-al3"), 0, false)
|
||||
Expect(err).To(MatchError(ErrUnavailable))
|
||||
Expect(queueRepo.Data[primaryKey("al", "al3")].Priority).To(Equal(model.ArtworkPriorityScan))
|
||||
ia, err := artRepo.GetItemArtwork("al", "al3", model.ImageTypePrimary)
|
||||
ia, err := artRepo.GetItemArtwork(model.KindAlbumArtwork, "al3", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(ia.Hash).To(Equal("bbbbbbbbbbbbbbbb"))
|
||||
})
|
||||
@ -202,7 +202,7 @@ var _ = Describe("Service", func() {
|
||||
Expect(err).To(MatchError(ErrUnavailable))
|
||||
Expect(queueRepo.Data[primaryKey("al", "al4b")].Priority).To(Equal(model.ArtworkPriorityBump))
|
||||
// The absent state row is left intact; only a recheck is scheduled.
|
||||
ia, err := artRepo.GetItemArtwork("al", "al4b", model.ImageTypePrimary)
|
||||
ia, err := artRepo.GetItemArtwork(model.KindAlbumArtwork, "al4b", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(ia.Hash).To(BeEmpty())
|
||||
})
|
||||
@ -218,7 +218,7 @@ var _ = Describe("Service", func() {
|
||||
Expect(readAll(img)).To(Equal(coverBytes))
|
||||
|
||||
Expect(queueRepo.Data[primaryKey("al", "al5")].Priority).To(Equal(model.ArtworkPriorityBump))
|
||||
_, err = artRepo.GetItemArtwork("al", "al5", model.ImageTypePrimary)
|
||||
_, err = artRepo.GetItemArtwork(model.KindAlbumArtwork, "al5", model.ImageTypePrimary)
|
||||
Expect(err).To(MatchError(model.ErrNotFound))
|
||||
})
|
||||
|
||||
@ -229,7 +229,7 @@ var _ = Describe("Service", func() {
|
||||
_, err := svc.Get(ctx, model.MustParseArtworkID("al-al6"), 0, false)
|
||||
Expect(err).To(MatchError(ErrUnavailable))
|
||||
Expect(queueRepo.Data[primaryKey("al", "al6")].Priority).To(Equal(model.ArtworkPriorityBump))
|
||||
_, err = artRepo.GetItemArtwork("al", "al6", model.ImageTypePrimary)
|
||||
_, err = artRepo.GetItemArtwork(model.KindAlbumArtwork, "al6", model.ImageTypePrimary)
|
||||
Expect(err).To(MatchError(model.ErrNotFound))
|
||||
})
|
||||
})
|
||||
@ -290,7 +290,7 @@ var _ = Describe("Service", func() {
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(len(readAll(img))).To(BeNumerically(">", 0))
|
||||
Expect(queueRepo.Data[primaryKey("mf", "mf4")].Priority).To(Equal(model.ArtworkPriorityBump))
|
||||
_, err = artRepo.GetItemArtwork("mf", "mf4", model.ImageTypePrimary)
|
||||
_, err = artRepo.GetItemArtwork(model.KindMediaFileArtwork, "mf4", model.ImageTypePrimary)
|
||||
Expect(err).To(MatchError(model.ErrNotFound))
|
||||
})
|
||||
|
||||
|
||||
@ -37,10 +37,10 @@ type Uploader interface {
|
||||
|
||||
// uploadEntityKind maps an upload's entity type to its artwork kind prefix, so a
|
||||
// successful upload can clear and re-queue that item's artwork state.
|
||||
var uploadEntityKind = map[string]string{
|
||||
consts.EntityArtist: model.KindArtistArtwork.Prefix(),
|
||||
consts.EntityPlaylist: model.KindPlaylistArtwork.Prefix(),
|
||||
consts.EntityRadio: model.KindRadioArtwork.Prefix(),
|
||||
var uploadEntityKind = map[string]model.Kind{
|
||||
consts.EntityArtist: model.KindArtistArtwork,
|
||||
consts.EntityPlaylist: model.KindPlaylistArtwork,
|
||||
consts.EntityRadio: model.KindRadioArtwork,
|
||||
}
|
||||
|
||||
type uploader struct {
|
||||
|
||||
@ -87,7 +87,7 @@ var _ = Describe("Uploader", func() {
|
||||
|
||||
// SetImage only writes the file; the state row survives and nothing is queued until
|
||||
// the caller has persisted the new filename and called EnqueueArtwork.
|
||||
_, err = artRepo.GetItemArtwork("ar", "ar-1", model.ImageTypePrimary)
|
||||
_, err = artRepo.GetItemArtwork(model.KindArtistArtwork, "ar-1", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(queueRepo.DequeueBatch(1000)).To(BeEmpty())
|
||||
})
|
||||
@ -102,7 +102,7 @@ var _ = Describe("Uploader", func() {
|
||||
|
||||
svc.EnqueueArtwork(ctx, consts.EntityArtist, "ar-1")
|
||||
|
||||
_, err := artRepo.GetItemArtwork("ar", "ar-1", model.ImageTypePrimary)
|
||||
_, err := artRepo.GetItemArtwork(model.KindArtistArtwork, "ar-1", model.ImageTypePrimary)
|
||||
Expect(err).To(MatchError(model.ErrNotFound))
|
||||
|
||||
queued, err := queueRepo.DequeueBatch(1000)
|
||||
|
||||
@ -165,14 +165,14 @@ func (w *Worker) drain(ctx context.Context, concurrency int) (int, error) {
|
||||
return len(items), nil
|
||||
}
|
||||
|
||||
// artworkKindToResource maps a queue item's kind to the UI resource name carried
|
||||
// in the refresh event.
|
||||
var artworkKindToResource = map[string]string{
|
||||
"al": "album",
|
||||
"ar": "artist",
|
||||
"pl": "playlist",
|
||||
"ra": "radio",
|
||||
"mf": "song",
|
||||
// artworkKindToResource maps an artwork kind to the UI resource name carried in the refresh
|
||||
// event; note media_file maps to "song", so this can't derive from Kind.String().
|
||||
var artworkKindToResource = map[model.Kind]string{
|
||||
model.KindAlbumArtwork: "album",
|
||||
model.KindArtistArtwork: "artist",
|
||||
model.KindPlaylistArtwork: "playlist",
|
||||
model.KindRadioArtwork: "radio",
|
||||
model.KindMediaFileArtwork: "song",
|
||||
}
|
||||
|
||||
// broadcastRefresh emits one coalesced RefreshResource for the batch's newly-acquired
|
||||
@ -184,7 +184,8 @@ func (w *Worker) broadcastRefresh(ctx context.Context, found []model.ArtworkQueu
|
||||
event := &events.RefreshResource{}
|
||||
byResource := map[string][]string{}
|
||||
for _, it := range found {
|
||||
if res, ok := artworkKindToResource[it.ItemKind]; ok {
|
||||
kind, _ := model.ParseKind(it.ItemKind)
|
||||
if res, ok := artworkKindToResource[kind]; ok {
|
||||
byResource[res] = append(byResource[res], it.ItemID)
|
||||
}
|
||||
}
|
||||
@ -235,7 +236,8 @@ func (w *Worker) precache(ctx context.Context, item model.ArtworkQueueItem) {
|
||||
imageType = model.ImageTypePrimary
|
||||
}
|
||||
repo := w.deps.ds.Artwork(ctx)
|
||||
ia, err := repo.GetItemArtwork(item.ItemKind, item.ItemID, imageType)
|
||||
kind, _ := model.ParseKind(item.ItemKind)
|
||||
ia, err := repo.GetItemArtwork(kind, item.ItemID, imageType)
|
||||
if err != nil || ia.Hash == "" {
|
||||
return
|
||||
}
|
||||
|
||||
@ -105,7 +105,8 @@ var _ = Describe("Worker soak", func() {
|
||||
// "Serve-adjacent" read-back: exercise the Phase 2 surfaces a caller would
|
||||
// use after acquisition, not the old serving pipeline.
|
||||
if out == outcomeFound {
|
||||
ia, err := artRepo.GetItemArtwork(it.ItemKind, it.ItemID, model.ImageTypePrimary)
|
||||
kind, _ := model.ParseKind(it.ItemKind)
|
||||
ia, err := artRepo.GetItemArtwork(kind, it.ItemID, model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred(), "cycle %d: GetItemArtwork", i)
|
||||
art, err := artRepo.GetImage(ia.Hash)
|
||||
Expect(err).ToNot(HaveOccurred(), "cycle %d: GetImage", i)
|
||||
|
||||
@ -172,7 +172,7 @@ var _ = Describe("Worker", func() {
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(n).To(Equal(1))
|
||||
|
||||
ia, err := artRepo.GetItemArtwork("al", "al1", model.ImageTypePrimary)
|
||||
ia, err := artRepo.GetItemArtwork(model.KindAlbumArtwork, "al1", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(ia.Source).To(Equal("folder"))
|
||||
|
||||
@ -195,7 +195,7 @@ var _ = Describe("Worker", func() {
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(n).To(Equal(1))
|
||||
|
||||
ia, err := artRepo.GetItemArtwork("mf", "mf1", model.ImageTypePrimary)
|
||||
ia, err := artRepo.GetItemArtwork(model.KindMediaFileArtwork, "mf1", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(ia.Source).To(Equal("embedded"))
|
||||
Expect(ia.Hash).ToNot(BeEmpty())
|
||||
@ -229,7 +229,7 @@ var _ = Describe("Worker", func() {
|
||||
Expect(it.Attempts).To(Equal(1))
|
||||
Expect(it.RetryAt).To(BeTemporally(">", time.Now()))
|
||||
|
||||
_, err = artRepo.GetItemArtwork("al", "al4", model.ImageTypePrimary)
|
||||
_, err = artRepo.GetItemArtwork(model.KindAlbumArtwork, "al4", model.ImageTypePrimary)
|
||||
Expect(err).To(MatchError(model.ErrNotFound), "a timeout must never settle on absent")
|
||||
})
|
||||
|
||||
@ -254,7 +254,7 @@ var _ = Describe("Worker", func() {
|
||||
Expect(it.Attempts).To(Equal(1))
|
||||
Expect(it.RetryAt).To(BeTemporally(">", time.Now()))
|
||||
|
||||
ia, err := artRepo.GetItemArtwork("al", "alstale", model.ImageTypePrimary)
|
||||
ia, err := artRepo.GetItemArtwork(model.KindAlbumArtwork, "alstale", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(ia.Source).To(Equal("folder"), "the fallback art is served meanwhile")
|
||||
|
||||
@ -284,7 +284,7 @@ var _ = Describe("Worker", func() {
|
||||
|
||||
// The concurrent re-enqueue changed retry_at, so the found-path delete was a no-op.
|
||||
Expect(findQueued(queueRepo, "al", "al7")).ToNot(BeNil())
|
||||
ia, err := artRepo.GetItemArtwork("al", "al7", model.ImageTypePrimary)
|
||||
ia, err := artRepo.GetItemArtwork(model.KindAlbumArtwork, "al7", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(ia.Source).To(Equal("folder"))
|
||||
})
|
||||
@ -327,7 +327,7 @@ var _ = Describe("Worker", func() {
|
||||
|
||||
// Resolved as absent (no art) and removed — not stuck failing on ErrNotFound forever.
|
||||
Expect(findQueued(queueRepo, "pl", "plPriv")).To(BeNil())
|
||||
ia, err := artRepo.GetItemArtwork("pl", "plPriv", model.ImageTypePrimary)
|
||||
ia, err := artRepo.GetItemArtwork(model.KindPlaylistArtwork, "plPriv", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(ia.Hash).To(BeEmpty())
|
||||
})
|
||||
@ -381,7 +381,7 @@ var _ = Describe("Worker", func() {
|
||||
Expect(evts).To(HaveLen(1), "a removed cover must live-refresh clients so they drop it")
|
||||
Expect(evts[0].(*events.RefreshResource).Data(evts[0])).To(ContainSubstring("al3"))
|
||||
|
||||
ia, err := artRepo.GetItemArtwork("al", "al3", model.ImageTypePrimary)
|
||||
ia, err := artRepo.GetItemArtwork(model.KindAlbumArtwork, "al3", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(ia.Hash).To(BeEmpty(), "the outcome was absent, not found")
|
||||
})
|
||||
|
||||
@ -428,7 +428,7 @@ var _ = Describe("Playlists", func() {
|
||||
|
||||
Expect(ps.RemoveImage(ctx, "pls-1")).To(Succeed())
|
||||
|
||||
_, err := ds.Artwork(ctx).GetItemArtwork("pl", "pls-1", model.ImageTypePrimary)
|
||||
_, err := ds.Artwork(ctx).GetItemArtwork(model.KindPlaylistArtwork, "pls-1", model.ImageTypePrimary)
|
||||
Expect(err).To(MatchError(model.ErrNotFound))
|
||||
queued, _ := ds.ArtworkQueue(ctx).DequeueBatch(100)
|
||||
Expect(queued).To(ContainElement(SatisfyAll(
|
||||
|
||||
@ -78,13 +78,13 @@ type ArtworkRepository interface {
|
||||
// DeleteOrphans deletes the given hashes only if still unreferenced and older than cutoff (atomic re-check).
|
||||
DeleteOrphans(createdBefore time.Time, hashes []string) error
|
||||
// Per-item state (item_artwork table)
|
||||
GetItemArtwork(kind, id, imageType string) (*ItemArtwork, error)
|
||||
GetItemArtwork(kind Kind, id, imageType string) (*ItemArtwork, error)
|
||||
PutItemArtwork(ia *ItemArtwork) error
|
||||
DeleteForItem(kind, id string) error
|
||||
DeleteForItem(kind Kind, id string) error
|
||||
// DeleteForItems removes state rows for the given ids of one kind, in chunks.
|
||||
DeleteForItems(kind string, ids []string) error
|
||||
DeleteForItems(kind Kind, ids []string) error
|
||||
// GetInfoForItems hydrates a page: one batched query, item_artwork joined to artwork.
|
||||
GetInfoForItems(kind string, ids []string) (map[string]ItemArtworkInfo, error)
|
||||
GetInfoForItems(kind Kind, ids []string) (map[string]ItemArtworkInfo, error)
|
||||
// GetAllMimes returns hash -> current mime for every stored artwork, for sweep retention checks.
|
||||
GetAllMimes() (map[string]string, error)
|
||||
// PurgeDanglingItemArtwork removes state rows whose entity no longer exists.
|
||||
@ -111,7 +111,7 @@ type ArtworkQueueRepository interface {
|
||||
DeleteIfUnchanged(kind, id, imageType string, retryAt time.Time) error
|
||||
Count() (int64, error)
|
||||
// EnqueueStaleAbsent inserts queue rows (priority Recheck) for absent states older than cutoff.
|
||||
EnqueueStaleAbsent(kind string, attemptedBefore time.Time) (int64, error)
|
||||
EnqueueStaleAbsent(kind Kind, attemptedBefore time.Time) (int64, error)
|
||||
// PurgeDangling removes queue rows whose entity no longer exists.
|
||||
PurgeDangling() (int64, error)
|
||||
}
|
||||
|
||||
@ -40,6 +40,13 @@ var artworkKindMap = map[string]Kind{
|
||||
KindRadioArtwork.prefix: KindRadioArtwork,
|
||||
}
|
||||
|
||||
// ParseKind resolves an item_kind prefix (e.g. "al") to its Kind, reporting whether it was known.
|
||||
// Use it at string boundaries — URL params, the item_kind column — to enter the typed Kind domain.
|
||||
func ParseKind(prefix string) (Kind, bool) {
|
||||
k, ok := artworkKindMap[prefix]
|
||||
return k, ok
|
||||
}
|
||||
|
||||
type ArtworkID struct {
|
||||
Kind Kind
|
||||
ID string
|
||||
|
||||
@ -262,7 +262,7 @@ func (r *albumRepository) hydrateArtwork(albums model.Albums) {
|
||||
return
|
||||
}
|
||||
ids := slice.Map(albums, func(a model.Album) string { return a.ID })
|
||||
infos := hydrateItemImages(r.ctx, r.db, model.KindAlbumArtwork.Prefix(), ids)
|
||||
infos := hydrateItemImages(r.ctx, r.db, model.KindAlbumArtwork, ids)
|
||||
for i := range albums {
|
||||
applyItemImage(infos, albums[i].ID, &albums[i].ItemImage)
|
||||
}
|
||||
|
||||
@ -284,7 +284,7 @@ func (r *artistRepository) hydrateArtwork(artists model.Artists) {
|
||||
return
|
||||
}
|
||||
ids := slice.Map(artists, func(a model.Artist) string { return a.ID })
|
||||
infos := hydrateItemImages(r.ctx, r.db, model.KindArtistArtwork.Prefix(), ids)
|
||||
infos := hydrateItemImages(r.ctx, r.db, model.KindArtistArtwork, ids)
|
||||
for i := range artists {
|
||||
applyItemImage(infos, artists[i].ID, &artists[i].ItemImage)
|
||||
}
|
||||
|
||||
@ -54,7 +54,7 @@ func chunkOptions(options []model.QueryOptions, idField string) func([]string) m
|
||||
|
||||
// hydrateItemImages returns per-item artwork info for a fetched page via one batched query per kind
|
||||
// (never a join, see spec §6). On error it logs and returns an empty map so the page still renders.
|
||||
func hydrateItemImages(ctx context.Context, db dbx.Builder, kind string, ids []string) map[string]model.ItemArtworkInfo {
|
||||
func hydrateItemImages(ctx context.Context, db dbx.Builder, kind model.Kind, ids []string) map[string]model.ItemArtworkInfo {
|
||||
if len(ids) == 0 {
|
||||
return map[string]model.ItemArtworkInfo{}
|
||||
}
|
||||
|
||||
@ -109,14 +109,14 @@ func (r *artworkQueueRepository) Count() (int64, error) {
|
||||
return res.Count, err
|
||||
}
|
||||
|
||||
func (r *artworkQueueRepository) EnqueueStaleAbsent(kind string, attemptedBefore time.Time) (int64, error) {
|
||||
func (r *artworkQueueRepository) EnqueueStaleAbsent(kind model.Kind, attemptedBefore time.Time) (int64, error) {
|
||||
now := time.Now()
|
||||
// DO NOTHING is deliberate: rechecks must not bump priority/retry_at of already-queued items.
|
||||
ins := Expr(`INSERT INTO `+r.tableName+` (item_kind, item_id, image_type, priority, attempts, retry_at, enqueued_at)
|
||||
SELECT item_kind, item_id, image_type, ?, 0, ?, ?
|
||||
FROM `+itemArtworkTable+` WHERE item_kind = ? AND hash = '' AND attempted_at < ?
|
||||
ON CONFLICT (item_kind, item_id, image_type) DO NOTHING`,
|
||||
model.ArtworkPriorityRecheck, now, now, kind, attemptedBefore)
|
||||
model.ArtworkPriorityRecheck, now, now, kind.Prefix(), attemptedBefore)
|
||||
return r.executeSQL(ins)
|
||||
}
|
||||
|
||||
|
||||
@ -174,7 +174,7 @@ var _ = Describe("ArtworkQueueRepository", func() {
|
||||
Expect(awRepo.PutItemArtwork(&model.ItemArtwork{ItemKind: "ar", ItemID: "fresh1", ImageType: model.ImageTypePrimary, Hash: "", AttemptedAt: time.Now()})).To(Succeed())
|
||||
Expect(awRepo.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))
|
||||
n, err := repo.EnqueueStaleAbsent(model.KindArtistArtwork, time.Now().Add(-24*time.Hour))
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(n).To(Equal(int64(1)))
|
||||
|
||||
|
||||
@ -114,13 +114,13 @@ func (r *artworkRepository) DeleteOrphans(createdBefore time.Time, hashes []stri
|
||||
return nil
|
||||
}
|
||||
|
||||
// danglingItemArtworkKinds maps item_kind prefixes to the table that owns the entity.
|
||||
var danglingItemArtworkKinds = map[string]string{
|
||||
"al": "album",
|
||||
"ar": "artist",
|
||||
"pl": "playlist",
|
||||
"ra": "radio",
|
||||
"mf": "media_file",
|
||||
// danglingItemArtworkKinds maps an artwork kind to the table that owns the entity.
|
||||
var danglingItemArtworkKinds = map[model.Kind]string{
|
||||
model.KindAlbumArtwork: "album",
|
||||
model.KindArtistArtwork: "artist",
|
||||
model.KindPlaylistArtwork: "playlist",
|
||||
model.KindRadioArtwork: "radio",
|
||||
model.KindMediaFileArtwork: "media_file",
|
||||
}
|
||||
|
||||
// purgeDangling deletes rows in table whose owning entity is gone, one statement per kind.
|
||||
@ -128,7 +128,7 @@ func purgeDangling(execute func(Sqlizer) (int64, error), table string) (int64, e
|
||||
var total int64
|
||||
for kind, entityTable := range danglingItemArtworkKinds {
|
||||
del := Delete(table).Where(And{
|
||||
Eq{"item_kind": kind},
|
||||
Eq{"item_kind": kind.Prefix()},
|
||||
Expr("item_id NOT IN (SELECT id FROM " + entityTable + ")"),
|
||||
})
|
||||
c, err := execute(del)
|
||||
@ -144,9 +144,9 @@ func (r *artworkRepository) PurgeDanglingItemArtwork() (int64, error) {
|
||||
return purgeDangling(r.items.executeSQL, itemArtworkTable)
|
||||
}
|
||||
|
||||
func (r *artworkRepository) GetItemArtwork(kind, id, imageType string) (*model.ItemArtwork, error) {
|
||||
func (r *artworkRepository) GetItemArtwork(kind model.Kind, id, imageType string) (*model.ItemArtwork, error) {
|
||||
sel := Select("*").From(itemArtworkTable).
|
||||
Where(Eq{"item_kind": kind, "item_id": id, "image_type": imageType})
|
||||
Where(Eq{"item_kind": kind.Prefix(), "item_id": id, "image_type": imageType})
|
||||
var res model.ItemArtwork
|
||||
if err := r.items.queryOne(sel, &res); err != nil {
|
||||
return nil, err
|
||||
@ -174,27 +174,27 @@ func (r *artworkRepository) PutItemArtwork(ia *model.ItemArtwork) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *artworkRepository) DeleteForItem(kind, id string) error {
|
||||
return r.items.delete(Eq{"item_kind": kind, "item_id": id})
|
||||
func (r *artworkRepository) DeleteForItem(kind model.Kind, id string) error {
|
||||
return r.items.delete(Eq{"item_kind": kind.Prefix(), "item_id": id})
|
||||
}
|
||||
|
||||
func (r *artworkRepository) DeleteForItems(kind string, ids []string) error {
|
||||
func (r *artworkRepository) DeleteForItems(kind model.Kind, ids []string) error {
|
||||
for chunk := range slices.Chunk(ids, artworkBatchSize) {
|
||||
if err := r.items.delete(Eq{"item_kind": kind, "item_id": chunk}); err != nil {
|
||||
if err := r.items.delete(Eq{"item_kind": kind.Prefix(), "item_id": chunk}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *artworkRepository) GetInfoForItems(kind string, ids []string) (map[string]model.ItemArtworkInfo, error) {
|
||||
func (r *artworkRepository) GetInfoForItems(kind model.Kind, ids []string) (map[string]model.ItemArtworkInfo, error) {
|
||||
res := map[string]model.ItemArtworkInfo{}
|
||||
for chunk := range slices.Chunk(ids, artworkBatchSize) {
|
||||
sel := Select("ia.item_id", "ia.hash", "COALESCE(a.blur_hash, '') as blur_hash").
|
||||
From(itemArtworkTable + " ia").
|
||||
LeftJoin("artwork a ON a.hash = ia.hash").
|
||||
Where(And{
|
||||
Eq{"ia.item_kind": kind},
|
||||
Eq{"ia.item_kind": kind.Prefix()},
|
||||
Eq{"ia.image_type": model.ImageTypePrimary},
|
||||
Eq{"ia.item_id": chunk},
|
||||
})
|
||||
|
||||
@ -161,7 +161,8 @@ var _ = Describe("ArtworkRepository", func() {
|
||||
{ItemKind: "ra", ItemID: radioWithHomePage.ID},
|
||||
{ItemKind: "mf", ItemID: songDayInALife.ID},
|
||||
} {
|
||||
_, err := repo.GetItemArtwork(kept.ItemKind, kept.ItemID, model.ImageTypePrimary)
|
||||
k, _ := model.ParseKind(kept.ItemKind)
|
||||
_, err := repo.GetItemArtwork(k, kept.ItemID, model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
}
|
||||
for _, gone := range []model.ItemArtwork{
|
||||
@ -171,7 +172,8 @@ var _ = Describe("ArtworkRepository", func() {
|
||||
{ItemKind: "ra", ItemID: "no-such-radio"},
|
||||
{ItemKind: "mf", ItemID: "no-such-mediafile"},
|
||||
} {
|
||||
_, err := repo.GetItemArtwork(gone.ItemKind, gone.ItemID, model.ImageTypePrimary)
|
||||
k, _ := model.ParseKind(gone.ItemKind)
|
||||
_, err := repo.GetItemArtwork(k, gone.ItemID, model.ImageTypePrimary)
|
||||
Expect(err).To(MatchError(model.ErrNotFound))
|
||||
}
|
||||
})
|
||||
@ -187,7 +189,7 @@ var _ = Describe("ArtworkRepository", func() {
|
||||
ia.RefMtime = 222
|
||||
Expect(repo.PutItemArtwork(ia)).To(Succeed())
|
||||
|
||||
got, err := repo.GetItemArtwork("al", "al1", model.ImageTypePrimary)
|
||||
got, err := repo.GetItemArtwork(model.KindAlbumArtwork, "al1", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(got.Source).To(Equal("embedded"))
|
||||
Expect(got.SourcePath).To(Equal("/music/a/track.mp3"))
|
||||
@ -199,7 +201,7 @@ var _ = Describe("ArtworkRepository", func() {
|
||||
before := time.Now().Add(-time.Second)
|
||||
Expect(repo.PutItemArtwork(&model.ItemArtwork{ItemKind: "ar", ItemID: "noattempt",
|
||||
ImageType: model.ImageTypePrimary, Hash: ""})).To(Succeed())
|
||||
got, err := repo.GetItemArtwork("ar", "noattempt", model.ImageTypePrimary)
|
||||
got, err := repo.GetItemArtwork(model.KindArtistArtwork, "noattempt", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(got.AttemptedAt).To(BeTemporally(">", before))
|
||||
})
|
||||
@ -207,7 +209,7 @@ var _ = Describe("ArtworkRepository", func() {
|
||||
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)
|
||||
got, err := repo.GetItemArtwork(model.KindArtistArtwork, "ar1", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(got.Hash).To(BeEmpty())
|
||||
})
|
||||
@ -217,7 +219,7 @@ var _ = Describe("ArtworkRepository", func() {
|
||||
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"})
|
||||
info, err := repo.GetInfoForItems(model.KindAlbumArtwork, []string{"x1", "x2", "x3"})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(info).To(HaveLen(2))
|
||||
Expect(info["x1"].Hash).To(Equal("h9"))
|
||||
@ -230,8 +232,8 @@ var _ = Describe("ArtworkRepository", func() {
|
||||
|
||||
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(repo.DeleteForItem(model.KindPlaylistArtwork, "p1")).To(Succeed())
|
||||
_, err := repo.GetItemArtwork(model.KindPlaylistArtwork, "p1", model.ImageTypePrimary)
|
||||
Expect(err).To(MatchError(model.ErrNotFound))
|
||||
})
|
||||
|
||||
@ -245,13 +247,13 @@ var _ = Describe("ArtworkRepository", func() {
|
||||
}
|
||||
Expect(repo.PutItemArtwork(&model.ItemArtwork{ItemKind: "mf", ItemID: "keep", ImageType: model.ImageTypePrimary, Hash: "h1"})).To(Succeed())
|
||||
|
||||
Expect(repo.DeleteForItems("mf", ids)).To(Succeed())
|
||||
Expect(repo.DeleteForItems(model.KindMediaFileArtwork, ids)).To(Succeed())
|
||||
|
||||
for _, id := range ids {
|
||||
_, err := repo.GetItemArtwork("mf", id, model.ImageTypePrimary)
|
||||
_, err := repo.GetItemArtwork(model.KindMediaFileArtwork, id, model.ImageTypePrimary)
|
||||
Expect(err).To(MatchError(model.ErrNotFound))
|
||||
}
|
||||
kept, err := repo.GetItemArtwork("mf", "keep", model.ImageTypePrimary)
|
||||
kept, err := repo.GetItemArtwork(model.KindMediaFileArtwork, "keep", model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(kept.ItemID).To(Equal("keep"))
|
||||
})
|
||||
|
||||
@ -237,8 +237,8 @@ func (r *mediaFileRepository) hydrateArtwork(mfs model.MediaFiles) {
|
||||
eligibleIDs = append(eligibleIDs, mfs[i].ID)
|
||||
}
|
||||
}
|
||||
albumInfos := hydrateItemImages(r.ctx, r.db, model.KindAlbumArtwork.Prefix(), albumIDs)
|
||||
mfInfos := hydrateItemImages(r.ctx, r.db, model.KindMediaFileArtwork.Prefix(), eligibleIDs)
|
||||
albumInfos := hydrateItemImages(r.ctx, r.db, model.KindAlbumArtwork, albumIDs)
|
||||
mfInfos := hydrateItemImages(r.ctx, r.db, model.KindMediaFileArtwork, eligibleIDs)
|
||||
for i := range mfs {
|
||||
mf := &mfs[i]
|
||||
applyItemImage(albumInfos, mf.AlbumID, &mf.AlbumImage)
|
||||
|
||||
@ -184,7 +184,7 @@ func (r *playlistRepository) hydrateArtwork(playlists model.Playlists) {
|
||||
return
|
||||
}
|
||||
ids := slice.Map(playlists, func(p model.Playlist) string { return p.ID })
|
||||
infos := hydrateItemImages(r.ctx, r.db, model.KindPlaylistArtwork.Prefix(), ids)
|
||||
infos := hydrateItemImages(r.ctx, r.db, model.KindPlaylistArtwork, ids)
|
||||
for i := range playlists {
|
||||
applyItemImage(infos, playlists[i].ID, &playlists[i].ItemImage)
|
||||
}
|
||||
@ -330,7 +330,7 @@ func (r *playlistRepository) refreshCounters(pls *model.Playlist) error {
|
||||
pls.Size = int64(res.Size)
|
||||
// The generated 2x2 grid depends on the track set, so re-resolve the cover whenever it
|
||||
// changes. No clear: the old cover keeps serving until the worker rebuilds (no flicker).
|
||||
item := model.ArtworkQueueItem{ItemKind: "pl", ItemID: pls.ID, ImageType: model.ImageTypePrimary,
|
||||
item := model.ArtworkQueueItem{ItemKind: model.KindPlaylistArtwork.Prefix(), ItemID: pls.ID, ImageType: model.ImageTypePrimary,
|
||||
Priority: model.ArtworkPriorityScan}
|
||||
if err := NewArtworkQueueRepository(r.ctx, r.db).Enqueue(item); err != nil {
|
||||
log.Warn(r.ctx, "could not enqueue playlist artwork after content change", "id", pls.ID, err)
|
||||
|
||||
@ -75,7 +75,7 @@ func (r *radioRepository) hydrateArtwork(radios model.Radios) {
|
||||
return
|
||||
}
|
||||
ids := slice.Map(radios, func(rd model.Radio) string { return rd.ID })
|
||||
infos := hydrateItemImages(r.ctx, r.db, model.KindRadioArtwork.Prefix(), ids)
|
||||
infos := hydrateItemImages(r.ctx, r.db, model.KindRadioArtwork, ids)
|
||||
for i := range radios {
|
||||
applyItemImage(infos, radios[i].ID, &radios[i].ItemImage)
|
||||
}
|
||||
@ -108,7 +108,7 @@ func (r *radioRepository) Put(radio *model.Radio, colsToUpdate ...string) error
|
||||
}
|
||||
// Enqueue artwork resolution for the created/updated radio at Bump priority so a new
|
||||
// radio's cover resolves proactively. Never fails the save.
|
||||
item := model.ArtworkQueueItem{ItemKind: "ra", ItemID: radio.ID, ImageType: model.ImageTypePrimary,
|
||||
item := model.ArtworkQueueItem{ItemKind: model.KindRadioArtwork.Prefix(), ItemID: radio.ID, ImageType: model.ImageTypePrimary,
|
||||
Priority: model.ArtworkPriorityBump}
|
||||
if err := NewArtworkQueueRepository(r.ctx, r.db).Enqueue(item); err != nil {
|
||||
log.Warn(r.ctx, "could not enqueue radio artwork", "id", radio.ID, err)
|
||||
|
||||
@ -369,7 +369,7 @@ func (p *phaseFolders) persistChanges(entry *folderEntry) (*folderEntry, error)
|
||||
}
|
||||
if entry.artists[i].Name != consts.UnknownArtist && entry.artists[i].Name != consts.VariousArtists {
|
||||
queueItems = append(queueItems, model.ArtworkQueueItem{
|
||||
ItemKind: "ar", ItemID: entry.artists[i].ID, ImageType: model.ImageTypePrimary,
|
||||
ItemKind: model.KindArtistArtwork.Prefix(), ItemID: entry.artists[i].ID, ImageType: model.ImageTypePrimary,
|
||||
Priority: model.ArtworkPriorityScan,
|
||||
})
|
||||
}
|
||||
@ -384,7 +384,7 @@ func (p *phaseFolders) persistChanges(entry *folderEntry) (*folderEntry, error)
|
||||
}
|
||||
if entry.albums[i].Name != consts.UnknownAlbum {
|
||||
queueItems = append(queueItems, model.ArtworkQueueItem{
|
||||
ItemKind: "al", ItemID: entry.albums[i].ID, ImageType: model.ImageTypePrimary,
|
||||
ItemKind: model.KindAlbumArtwork.Prefix(), ItemID: entry.albums[i].ID, ImageType: model.ImageTypePrimary,
|
||||
Priority: model.ArtworkPriorityScan,
|
||||
})
|
||||
}
|
||||
@ -405,7 +405,7 @@ func (p *phaseFolders) persistChanges(entry *folderEntry) (*folderEntry, error)
|
||||
for i := range entry.tracks {
|
||||
trackIDs[i] = entry.tracks[i].ID
|
||||
}
|
||||
if err := tx.Artwork(p.ctx).DeleteForItems("mf", trackIDs); err != nil {
|
||||
if err := tx.Artwork(p.ctx).DeleteForItems(model.KindMediaFileArtwork, trackIDs); err != nil {
|
||||
log.Warn(p.ctx, "Scanner: could not invalidate media_file artwork", "folder", entry.path, err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -145,7 +145,7 @@ func (p *phasePlaylists) processPlaylistsInFolder(folder *model.Folder) (*model.
|
||||
} else {
|
||||
log.Debug("Scanner: Imported playlist", "name", pls.Name, "lastUpdated", pls.UpdatedAt, "path", pls.Path, "numTracks", len(pls.Tracks), "elapsed", time.Since(started))
|
||||
}
|
||||
item := model.ArtworkQueueItem{ItemKind: "pl", ItemID: pls.ID, ImageType: model.ImageTypePrimary,
|
||||
item := model.ArtworkQueueItem{ItemKind: model.KindPlaylistArtwork.Prefix(), ItemID: pls.ID, ImageType: model.ImageTypePrimary,
|
||||
Priority: model.ArtworkPriorityScan}
|
||||
if err := p.ds.ArtworkQueue(p.ctx).Enqueue(item); err != nil {
|
||||
log.Warn(p.ctx, "Scanner: could not enqueue playlist artwork", "id", pls.ID, err)
|
||||
|
||||
@ -226,7 +226,7 @@ var _ = Describe("Scanner", Ordered, func() {
|
||||
fsys.UpdateTags("The Beatles/Help!/01 - Help!.mp3", _t{"comment": "reimport"})
|
||||
Expect(runScanner(ctx, true)).To(Succeed())
|
||||
|
||||
_, err = ds.Artwork(ctx).GetItemArtwork("mf", trackID, model.ImageTypePrimary)
|
||||
_, err = ds.Artwork(ctx).GetItemArtwork(model.KindMediaFileArtwork, trackID, model.ImageTypePrimary)
|
||||
Expect(err).To(MatchError(model.ErrNotFound))
|
||||
})
|
||||
})
|
||||
|
||||
@ -10,12 +10,12 @@ import (
|
||||
)
|
||||
|
||||
// refreshableArtworkKinds are the entity kinds a manual re-resolve accepts.
|
||||
var refreshableArtworkKinds = map[string]bool{
|
||||
model.KindAlbumArtwork.Prefix(): true,
|
||||
model.KindArtistArtwork.Prefix(): true,
|
||||
model.KindPlaylistArtwork.Prefix(): true,
|
||||
model.KindRadioArtwork.Prefix(): true,
|
||||
model.KindMediaFileArtwork.Prefix(): true,
|
||||
var refreshableArtworkKinds = map[model.Kind]bool{
|
||||
model.KindAlbumArtwork: true,
|
||||
model.KindArtistArtwork: true,
|
||||
model.KindPlaylistArtwork: true,
|
||||
model.KindRadioArtwork: true,
|
||||
model.KindMediaFileArtwork: true,
|
||||
}
|
||||
|
||||
func (api *Router) addArtworkRoute(r chi.Router) {
|
||||
@ -27,7 +27,7 @@ func (api *Router) addArtworkRoute(r chi.Router) {
|
||||
func (api *Router) refreshArtwork() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
kind := chi.URLParam(r, "kind")
|
||||
kind, _ := model.ParseKind(chi.URLParam(r, "kind"))
|
||||
id := chi.URLParam(r, "id")
|
||||
if !refreshableArtworkKinds[kind] {
|
||||
http.Error(w, "invalid artwork kind", http.StatusBadRequest)
|
||||
|
||||
@ -56,7 +56,7 @@ var _ = Describe("Artwork API", func() {
|
||||
|
||||
Expect(w.Code).To(Equal(http.StatusNoContent))
|
||||
|
||||
_, err := artRepo.GetItemArtwork("al", "al-1", model.ImageTypePrimary)
|
||||
_, err := artRepo.GetItemArtwork(model.KindAlbumArtwork, "al-1", model.ImageTypePrimary)
|
||||
Expect(err).To(MatchError(model.ErrNotFound))
|
||||
|
||||
queued, err := queueRepo.DequeueBatch(1000)
|
||||
|
||||
@ -151,14 +151,14 @@ var _ = Describe("Artwork Serving", Ordered, func() {
|
||||
worker.Bump("al", artfulID)
|
||||
worker.Bump("al", artlessID)
|
||||
runWorkerUntil(ctx, worker, func() bool {
|
||||
found, err := ds.Artwork(ctx).GetItemArtwork("al", artfulID, model.ImageTypePrimary)
|
||||
found, err := ds.Artwork(ctx).GetItemArtwork(model.KindAlbumArtwork, artfulID, model.ImageTypePrimary)
|
||||
if err != nil || found.Hash == "" {
|
||||
return false
|
||||
}
|
||||
absent, err := ds.Artwork(ctx).GetItemArtwork("al", artlessID, model.ImageTypePrimary)
|
||||
absent, err := ds.Artwork(ctx).GetItemArtwork(model.KindAlbumArtwork, artlessID, model.ImageTypePrimary)
|
||||
return err == nil && absent.Hash == ""
|
||||
})
|
||||
ia, err := ds.Artwork(ctx).GetItemArtwork("al", artfulID, model.ImageTypePrimary)
|
||||
ia, err := ds.Artwork(ctx).GetItemArtwork(model.KindAlbumArtwork, artfulID, model.ImageTypePrimary)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(ia.Source).To(Equal("folder"))
|
||||
artfulHash = ia.Hash
|
||||
|
||||
@ -186,7 +186,7 @@ func (m *MockArtworkQueueRepo) EnqueueBump(items ...model.ArtworkQueueItem) erro
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockArtworkQueueRepo) EnqueueStaleAbsent(kind string, attemptedBefore time.Time) (int64, error) {
|
||||
func (m *MockArtworkQueueRepo) EnqueueStaleAbsent(kind model.Kind, attemptedBefore time.Time) (int64, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if m.Err != nil || m.ItemArtworkSource == nil {
|
||||
@ -195,7 +195,7 @@ func (m *MockArtworkQueueRepo) EnqueueStaleAbsent(kind string, attemptedBefore t
|
||||
now := time.Now()
|
||||
var inserted int64
|
||||
for _, ia := range m.ItemArtworkSource.ItemData {
|
||||
if ia.ItemKind != kind || ia.Hash != "" || !ia.AttemptedAt.Before(attemptedBefore) {
|
||||
if ia.ItemKind != kind.Prefix() || ia.Hash != "" || !ia.AttemptedAt.Before(attemptedBefore) {
|
||||
continue
|
||||
}
|
||||
k := iaKey(ia.ItemKind, ia.ItemID, ia.ImageType)
|
||||
|
||||
@ -132,13 +132,13 @@ func (m *MockArtworkRepo) referenced(hash string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *MockArtworkRepo) GetItemArtwork(kind, id, imageType string) (*model.ItemArtwork, error) {
|
||||
func (m *MockArtworkRepo) GetItemArtwork(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, id, imageType)]; ok {
|
||||
if ia, ok := m.ItemData[iaKey(kind.Prefix(), id, imageType)]; ok {
|
||||
return &ia, nil
|
||||
}
|
||||
return nil, model.ErrNotFound
|
||||
@ -161,21 +161,21 @@ func (m *MockArtworkRepo) PutItemArtwork(ia *model.ItemArtwork) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockArtworkRepo) DeleteForItem(kind, id string) error {
|
||||
func (m *MockArtworkRepo) DeleteForItem(kind model.Kind, id string) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if m.Err != nil {
|
||||
return m.Err
|
||||
}
|
||||
for k, ia := range m.ItemData {
|
||||
if ia.ItemKind == kind && ia.ItemID == id {
|
||||
if ia.ItemKind == kind.Prefix() && ia.ItemID == id {
|
||||
delete(m.ItemData, k)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockArtworkRepo) DeleteForItems(kind string, ids []string) error {
|
||||
func (m *MockArtworkRepo) DeleteForItems(kind model.Kind, ids []string) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if m.Err != nil {
|
||||
@ -186,14 +186,14 @@ func (m *MockArtworkRepo) DeleteForItems(kind string, ids []string) error {
|
||||
idSet[id] = true
|
||||
}
|
||||
for k, ia := range m.ItemData {
|
||||
if ia.ItemKind == kind && idSet[ia.ItemID] {
|
||||
if ia.ItemKind == kind.Prefix() && idSet[ia.ItemID] {
|
||||
delete(m.ItemData, k)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockArtworkRepo) GetInfoForItems(kind string, ids []string) (map[string]model.ItemArtworkInfo, error) {
|
||||
func (m *MockArtworkRepo) GetInfoForItems(kind model.Kind, ids []string) (map[string]model.ItemArtworkInfo, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if m.Err != nil {
|
||||
@ -201,7 +201,7 @@ func (m *MockArtworkRepo) GetInfoForItems(kind string, ids []string) (map[string
|
||||
}
|
||||
res := map[string]model.ItemArtworkInfo{}
|
||||
for _, id := range ids {
|
||||
if ia, ok := m.ItemData[iaKey(kind, id, model.ImageTypePrimary)]; ok {
|
||||
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 = a.BlurHash
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user