diff --git a/core/artwork/e2e/acquire_serve_test.go b/core/artwork/e2e/acquire_serve_test.go index 3a20a0245..fe5f7e430 100644 --- a/core/artwork/e2e/acquire_serve_test.go +++ b/core/artwork/e2e/acquire_serve_test.go @@ -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)) diff --git a/core/artwork/housekeeping.go b/core/artwork/housekeeping.go index f45c62ab6..400c48a69 100644 --- a/core/artwork/housekeeping.go +++ b/core/artwork/housekeeping.go @@ -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...) diff --git a/core/artwork/processor_test.go b/core/artwork/processor_test.go index 3d145ff71..de765ae9d 100644 --- a/core/artwork/processor_test.go +++ b/core/artwork/processor_test.go @@ -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)) }) }) diff --git a/core/artwork/prune_test.go b/core/artwork/prune_test.go index 646ad7440..d102f155c 100644 --- a/core/artwork/prune_test.go +++ b/core/artwork/prune_test.go @@ -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()) }) diff --git a/core/artwork/refresh.go b/core/artwork/refresh.go index bbc54c800..e5aa68bb9 100644 --- a/core/artwork/refresh.go +++ b/core/artwork/refresh.go @@ -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) } diff --git a/core/artwork/resolve.go b/core/artwork/resolve.go index ff095bf24..dc9285bc1 100644 --- a/core/artwork/resolve.go +++ b/core/artwork/resolve.go @@ -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) diff --git a/core/artwork/serving.go b/core/artwork/serving.go index a29e5325f..6e6bed4e0 100644 --- a/core/artwork/serving.go +++ b/core/artwork/serving.go @@ -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) diff --git a/core/artwork/serving_test.go b/core/artwork/serving_test.go index dc20a358a..58f9585ba 100644 --- a/core/artwork/serving_test.go +++ b/core/artwork/serving_test.go @@ -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)) }) diff --git a/core/artwork/uploader.go b/core/artwork/uploader.go index fb8c1ab94..47c701d9b 100644 --- a/core/artwork/uploader.go +++ b/core/artwork/uploader.go @@ -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 { diff --git a/core/artwork/uploader_test.go b/core/artwork/uploader_test.go index 128cc5990..44f5ede26 100644 --- a/core/artwork/uploader_test.go +++ b/core/artwork/uploader_test.go @@ -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) diff --git a/core/artwork/worker.go b/core/artwork/worker.go index 5191e2285..5513e0975 100644 --- a/core/artwork/worker.go +++ b/core/artwork/worker.go @@ -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 } diff --git a/core/artwork/worker_soak_test.go b/core/artwork/worker_soak_test.go index 59a042106..b6642d315 100644 --- a/core/artwork/worker_soak_test.go +++ b/core/artwork/worker_soak_test.go @@ -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) diff --git a/core/artwork/worker_test.go b/core/artwork/worker_test.go index d773d6bec..9e1b43c5e 100644 --- a/core/artwork/worker_test.go +++ b/core/artwork/worker_test.go @@ -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") }) diff --git a/core/playlists/playlists_test.go b/core/playlists/playlists_test.go index 3d4e5e1c9..e8134b2ef 100644 --- a/core/playlists/playlists_test.go +++ b/core/playlists/playlists_test.go @@ -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( diff --git a/model/artwork.go b/model/artwork.go index 88e31e6fb..8bbd7a2cb 100644 --- a/model/artwork.go +++ b/model/artwork.go @@ -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) } diff --git a/model/artwork_id.go b/model/artwork_id.go index e308cb676..634a6442f 100644 --- a/model/artwork_id.go +++ b/model/artwork_id.go @@ -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 diff --git a/persistence/album_repository.go b/persistence/album_repository.go index f14e3ab38..fe919dd29 100644 --- a/persistence/album_repository.go +++ b/persistence/album_repository.go @@ -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) } diff --git a/persistence/artist_repository.go b/persistence/artist_repository.go index dcc8cac46..00aa75c81 100644 --- a/persistence/artist_repository.go +++ b/persistence/artist_repository.go @@ -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) } diff --git a/persistence/artwork_hydration.go b/persistence/artwork_hydration.go index a8a6fd5eb..4ea92152a 100644 --- a/persistence/artwork_hydration.go +++ b/persistence/artwork_hydration.go @@ -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{} } diff --git a/persistence/artwork_queue_repository.go b/persistence/artwork_queue_repository.go index 81f674dd9..ba8c42f95 100644 --- a/persistence/artwork_queue_repository.go +++ b/persistence/artwork_queue_repository.go @@ -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) } diff --git a/persistence/artwork_queue_repository_test.go b/persistence/artwork_queue_repository_test.go index f5eafec00..10015a225 100644 --- a/persistence/artwork_queue_repository_test.go +++ b/persistence/artwork_queue_repository_test.go @@ -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))) diff --git a/persistence/artwork_repository.go b/persistence/artwork_repository.go index e21a69c63..f5ba7be13 100644 --- a/persistence/artwork_repository.go +++ b/persistence/artwork_repository.go @@ -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}, }) diff --git a/persistence/artwork_repository_test.go b/persistence/artwork_repository_test.go index c1f1ad741..5a3c1394e 100644 --- a/persistence/artwork_repository_test.go +++ b/persistence/artwork_repository_test.go @@ -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")) }) diff --git a/persistence/mediafile_repository.go b/persistence/mediafile_repository.go index 91110d132..010bc6006 100644 --- a/persistence/mediafile_repository.go +++ b/persistence/mediafile_repository.go @@ -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) diff --git a/persistence/playlist_repository.go b/persistence/playlist_repository.go index 40ab85013..0aa848dec 100644 --- a/persistence/playlist_repository.go +++ b/persistence/playlist_repository.go @@ -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) diff --git a/persistence/radio_repository.go b/persistence/radio_repository.go index ac3950005..2ee5351fe 100644 --- a/persistence/radio_repository.go +++ b/persistence/radio_repository.go @@ -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) diff --git a/scanner/phase_1_folders.go b/scanner/phase_1_folders.go index f55f975dc..eeffe800e 100644 --- a/scanner/phase_1_folders.go +++ b/scanner/phase_1_folders.go @@ -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) } } diff --git a/scanner/phase_4_playlists.go b/scanner/phase_4_playlists.go index 04ad2c276..baa8b749a 100644 --- a/scanner/phase_4_playlists.go +++ b/scanner/phase_4_playlists.go @@ -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) diff --git a/scanner/scanner_test.go b/scanner/scanner_test.go index 811ceb813..66e0d0542 100644 --- a/scanner/scanner_test.go +++ b/scanner/scanner_test.go @@ -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)) }) }) diff --git a/server/nativeapi/artwork.go b/server/nativeapi/artwork.go index f50bdd903..1fd8af479 100644 --- a/server/nativeapi/artwork.go +++ b/server/nativeapi/artwork.go @@ -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) diff --git a/server/nativeapi/artwork_test.go b/server/nativeapi/artwork_test.go index c84e83fdc..b26a94e6a 100644 --- a/server/nativeapi/artwork_test.go +++ b/server/nativeapi/artwork_test.go @@ -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) diff --git a/server/subsonic/e2e/subsonic_artwork_test.go b/server/subsonic/e2e/subsonic_artwork_test.go index b28bc4448..1e7825214 100644 --- a/server/subsonic/e2e/subsonic_artwork_test.go +++ b/server/subsonic/e2e/subsonic_artwork_test.go @@ -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 diff --git a/tests/mock_artwork_queue_repo.go b/tests/mock_artwork_queue_repo.go index cbb2f3f58..43bc6218d 100644 --- a/tests/mock_artwork_queue_repo.go +++ b/tests/mock_artwork_queue_repo.go @@ -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) diff --git a/tests/mock_artwork_repo.go b/tests/mock_artwork_repo.go index aa873e7c6..206f4031f 100644 --- a/tests/mock_artwork_repo.go +++ b/tests/mock_artwork_repo.go @@ -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