From 8f3387a894a67b130a26c4d4871fad8f9897a3fe Mon Sep 17 00:00:00 2001
From: Deluan <deluan@navidrome.org>
Date: Tue, 27 Dec 2022 16:36:13 -0500
Subject: [PATCH] Fix tests and clean up code a bit

---
 core/artwork/artwork.go                       | 37 ---------
 core/artwork/artwork_internal_test.go         | 81 ++++++++++---------
 core/artwork/artwork_suite_test.go            | 17 ++++
 core/artwork/artwork_test.go                  | 47 +++++++++++
 ...rtwork_cache_warmer.go => cache_warmer.go} |  0
 core/artwork/image_cache.go                   | 47 +++++++++++
 core/artwork/reader_album.go                  |  6 +-
 core/artwork/reader_mediafile.go              |  6 +-
 core/artwork/reader_resized.go                |  8 +-
 core/wire_providers.go                        |  8 +-
 model/artwork_id.go                           |  3 +
 11 files changed, 169 insertions(+), 91 deletions(-)
 create mode 100644 core/artwork/artwork_suite_test.go
 create mode 100644 core/artwork/artwork_test.go
 rename core/artwork/{artwork_cache_warmer.go => cache_warmer.go} (100%)
 create mode 100644 core/artwork/image_cache.go

diff --git a/core/artwork/artwork.go b/core/artwork/artwork.go
index 48d3d68ac..f6cc2a516 100644
--- a/core/artwork/artwork.go
+++ b/core/artwork/artwork.go
@@ -3,18 +3,14 @@ package artwork
 import (
 	"context"
 	"errors"
-	"fmt"
 	_ "image/gif"
 	"io"
 	"time"
 
-	"github.com/navidrome/navidrome/conf"
-	"github.com/navidrome/navidrome/consts"
 	"github.com/navidrome/navidrome/core/ffmpeg"
 	"github.com/navidrome/navidrome/log"
 	"github.com/navidrome/navidrome/model"
 	"github.com/navidrome/navidrome/utils/cache"
-	"github.com/navidrome/navidrome/utils/singleton"
 	_ "golang.org/x/image/webp"
 )
 
@@ -79,36 +75,3 @@ func (a *artwork) getArtworkReader(ctx context.Context, artID model.ArtworkID, s
 	}
 	return artReader, err
 }
-
-type cacheItem struct {
-	artID      model.ArtworkID
-	size       int
-	lastUpdate time.Time
-}
-
-func (i *cacheItem) Key() string {
-	return fmt.Sprintf(
-		"%s.%d.%d.%d.%t",
-		i.artID.ID,
-		i.lastUpdate.UnixMilli(),
-		i.size,
-		conf.Server.CoverJpegQuality,
-		conf.Server.EnableMediaFileCoverArt,
-	)
-}
-
-type imageCache struct {
-	cache.FileCache
-}
-
-func GetImageCache() cache.FileCache {
-	return singleton.GetInstance(func() *imageCache {
-		return &imageCache{
-			FileCache: cache.NewFileCache("Image", conf.Server.ImageCacheSize, consts.ImageCacheDir, consts.DefaultImageCacheMaxItems,
-				func(ctx context.Context, arg cache.Item) (io.Reader, error) {
-					r, _, err := arg.(artworkReader).Reader(ctx)
-					return r, err
-				}),
-		}
-	})
-}
diff --git a/core/artwork/artwork_internal_test.go b/core/artwork/artwork_internal_test.go
index 6c0a257b0..90e2e5cdf 100644
--- a/core/artwork/artwork_internal_test.go
+++ b/core/artwork/artwork_internal_test.go
@@ -5,7 +5,6 @@ import (
 	"errors"
 	"image"
 	"io"
-	"testing"
 
 	"github.com/navidrome/navidrome/conf"
 	"github.com/navidrome/navidrome/conf/configtest"
@@ -17,13 +16,6 @@ import (
 	. "github.com/onsi/gomega"
 )
 
-func TestArtwork(t *testing.T) {
-	tests.Init(t, false)
-	log.SetLevel(log.LevelFatal)
-	RegisterFailHandler(Fail)
-	RunSpecs(t, "Artwork Suite")
-}
-
 var _ = Describe("Artwork", func() {
 	var aw *artwork
 	var ds model.DataStore
@@ -54,20 +46,11 @@ var _ = Describe("Artwork", func() {
 		aw = NewArtwork(ds, cache, ffmpeg).(*artwork)
 	})
 
-	Context("Empty ID", func() {
-		It("returns placeholder if album is not in the DB", func() {
-			_, path, err := aw.get(context.Background(), model.ArtworkID{}, 0)
-			Expect(err).ToNot(HaveOccurred())
-			Expect(path).To(Equal(consts.PlaceholderAlbumArt))
-		})
-	})
-
-	Context("Albums", func() {
+	Describe("albumArtworkReader", func() {
 		Context("ID not found", func() {
-			It("returns placeholder if album is not in the DB", func() {
-				_, path, err := aw.get(context.Background(), model.MustParseArtworkID("al-NOT_FOUND-0"), 0)
-				Expect(err).ToNot(HaveOccurred())
-				Expect(path).To(Equal(consts.PlaceholderAlbumArt))
+			It("returns ErrNotFound if album is not in the DB", func() {
+				_, err := newAlbumArtworkReader(ctx, aw, model.MustParseArtworkID("al-NOT_FOUND"))
+				Expect(err).To(MatchError(model.ErrNotFound))
 			})
 		})
 		Context("Embed images", func() {
@@ -78,13 +61,17 @@ var _ = Describe("Artwork", func() {
 				})
 			})
 			It("returns embed cover", func() {
-				_, path, err := aw.get(context.Background(), alOnlyEmbed.CoverArtID(), 0)
+				aw, err := newAlbumArtworkReader(ctx, aw, alOnlyEmbed.CoverArtID())
+				Expect(err).ToNot(HaveOccurred())
+				_, path, err := aw.Reader(ctx)
 				Expect(err).ToNot(HaveOccurred())
 				Expect(path).To(Equal("tests/fixtures/test.mp3"))
 			})
 			It("returns placeholder if embed path is not available", func() {
 				ffmpeg.Error = errors.New("not available")
-				_, path, err := aw.get(context.Background(), alEmbedNotFound.CoverArtID(), 0)
+				aw, err := newAlbumArtworkReader(ctx, aw, alEmbedNotFound.CoverArtID())
+				Expect(err).ToNot(HaveOccurred())
+				_, path, err := aw.Reader(ctx)
 				Expect(err).ToNot(HaveOccurred())
 				Expect(path).To(Equal(consts.PlaceholderAlbumArt))
 			})
@@ -93,15 +80,20 @@ var _ = Describe("Artwork", func() {
 			BeforeEach(func() {
 				ds.Album(ctx).(*tests.MockAlbumRepo).SetData(model.Albums{
 					alOnlyExternal,
+					alExternalNotFound,
 				})
 			})
 			It("returns external cover", func() {
-				_, path, err := aw.get(context.Background(), alOnlyExternal.CoverArtID(), 0)
+				aw, err := newAlbumArtworkReader(ctx, aw, alOnlyExternal.CoverArtID())
+				Expect(err).ToNot(HaveOccurred())
+				_, path, err := aw.Reader(ctx)
 				Expect(err).ToNot(HaveOccurred())
 				Expect(path).To(Equal("tests/fixtures/front.png"))
 			})
 			It("returns placeholder if external file is not available", func() {
-				_, path, err := aw.get(context.Background(), alExternalNotFound.CoverArtID(), 0)
+				aw, err := newAlbumArtworkReader(ctx, aw, alExternalNotFound.CoverArtID())
+				Expect(err).ToNot(HaveOccurred())
+				_, path, err := aw.Reader(ctx)
 				Expect(err).ToNot(HaveOccurred())
 				Expect(path).To(Equal(consts.PlaceholderAlbumArt))
 			})
@@ -115,7 +107,9 @@ var _ = Describe("Artwork", func() {
 			DescribeTable("CoverArtPriority",
 				func(priority string, expected string) {
 					conf.Server.CoverArtPriority = priority
-					_, path, err := aw.get(context.Background(), alMultipleCovers.CoverArtID(), 0)
+					aw, err := newAlbumArtworkReader(ctx, aw, alMultipleCovers.CoverArtID())
+					Expect(err).ToNot(HaveOccurred())
+					_, path, err := aw.Reader(ctx)
 					Expect(err).ToNot(HaveOccurred())
 					Expect(path).To(Equal(expected))
 				},
@@ -125,12 +119,11 @@ var _ = Describe("Artwork", func() {
 			)
 		})
 	})
-	Context("MediaFiles", func() {
+	Describe("mediafileArtworkReader", func() {
 		Context("ID not found", func() {
-			It("returns placeholder if album is not in the DB", func() {
-				_, path, err := aw.get(context.Background(), model.MustParseArtworkID("mf-NOT_FOUND-0"), 0)
-				Expect(err).ToNot(HaveOccurred())
-				Expect(path).To(Equal(consts.PlaceholderAlbumArt))
+			It("returns ErrNotFound if mediafile is not in the DB", func() {
+				_, err := newAlbumArtworkReader(ctx, aw, alMultipleCovers.CoverArtID())
+				Expect(err).To(MatchError(model.ErrNotFound))
 			})
 		})
 		Context("Embed images", func() {
@@ -146,30 +139,38 @@ var _ = Describe("Artwork", func() {
 				})
 			})
 			It("returns embed cover", func() {
-				_, path, err := aw.get(context.Background(), mfWithEmbed.CoverArtID(), 0)
+				aw, err := newMediafileArtworkReader(ctx, aw, mfWithEmbed.CoverArtID())
+				Expect(err).ToNot(HaveOccurred())
+				_, path, err := aw.Reader(ctx)
 				Expect(err).ToNot(HaveOccurred())
 				Expect(path).To(Equal("tests/fixtures/test.mp3"))
 			})
 			It("returns embed cover if successfully extracted by ffmpeg", func() {
-				r, path, err := aw.get(context.Background(), mfCorruptedCover.CoverArtID(), 0)
+				aw, err := newMediafileArtworkReader(ctx, aw, mfCorruptedCover.CoverArtID())
+				Expect(err).ToNot(HaveOccurred())
+				r, path, err := aw.Reader(ctx)
 				Expect(err).ToNot(HaveOccurred())
 				Expect(io.ReadAll(r)).To(Equal([]byte("content from ffmpeg")))
 				Expect(path).To(Equal("tests/fixtures/test.ogg"))
 			})
 			It("returns album cover if cannot read embed artwork", func() {
 				ffmpeg.Error = errors.New("not available")
-				_, path, err := aw.get(context.Background(), mfCorruptedCover.CoverArtID(), 0)
+				aw, err := newMediafileArtworkReader(ctx, aw, mfCorruptedCover.CoverArtID())
 				Expect(err).ToNot(HaveOccurred())
-				Expect(path).To(Equal("tests/fixtures/front.png"))
+				_, path, err := aw.Reader(ctx)
+				Expect(err).ToNot(HaveOccurred())
+				Expect(path).To(Equal("al-444"))
 			})
 			It("returns album cover if media file has no cover art", func() {
-				_, path, err := aw.get(context.Background(), mfWithoutEmbed.CoverArtID(), 0)
+				aw, err := newMediafileArtworkReader(ctx, aw, model.MustParseArtworkID("mf-"+mfWithoutEmbed.ID))
 				Expect(err).ToNot(HaveOccurred())
-				Expect(path).To(Equal("tests/fixtures/front.png"))
+				_, path, err := aw.Reader(ctx)
+				Expect(err).ToNot(HaveOccurred())
+				Expect(path).To(Equal("al-444"))
 			})
 		})
 	})
-	Context("Resize", func() {
+	Describe("resizedArtworkReader", func() {
 		BeforeEach(func() {
 			ds.Album(ctx).(*tests.MockAlbumRepo).SetData(model.Albums{
 				alMultipleCovers,
@@ -177,7 +178,7 @@ var _ = Describe("Artwork", func() {
 		})
 		It("returns a PNG if original image is a PNG", func() {
 			conf.Server.CoverArtPriority = "front.png"
-			r, err := aw.Get(context.Background(), alMultipleCovers.CoverArtID().String(), 300)
+			r, _, err := aw.Get(context.Background(), alMultipleCovers.CoverArtID().String(), 300)
 			Expect(err).ToNot(HaveOccurred())
 
 			br, format, err := asImageReader(r)
@@ -191,7 +192,7 @@ var _ = Describe("Artwork", func() {
 		})
 		It("returns a JPEG if original image is not a PNG", func() {
 			conf.Server.CoverArtPriority = "cover.jpg"
-			r, _, err := aw.get(context.Background(), alMultipleCovers.CoverArtID(), 200)
+			r, _, err := aw.Get(context.Background(), alMultipleCovers.CoverArtID().String(), 200)
 			Expect(err).ToNot(HaveOccurred())
 
 			br, format, err := asImageReader(r)
diff --git a/core/artwork/artwork_suite_test.go b/core/artwork/artwork_suite_test.go
new file mode 100644
index 000000000..dfd66e5e5
--- /dev/null
+++ b/core/artwork/artwork_suite_test.go
@@ -0,0 +1,17 @@
+package artwork
+
+import (
+	"testing"
+
+	"github.com/navidrome/navidrome/log"
+	"github.com/navidrome/navidrome/tests"
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+)
+
+func TestArtwork(t *testing.T) {
+	tests.Init(t, false)
+	log.SetLevel(log.LevelFatal)
+	RegisterFailHandler(Fail)
+	RunSpecs(t, "Artwork Suite")
+}
diff --git a/core/artwork/artwork_test.go b/core/artwork/artwork_test.go
new file mode 100644
index 000000000..ec1d5095b
--- /dev/null
+++ b/core/artwork/artwork_test.go
@@ -0,0 +1,47 @@
+package artwork_test
+
+import (
+	"context"
+	"io"
+
+	"github.com/navidrome/navidrome/conf"
+	"github.com/navidrome/navidrome/conf/configtest"
+	"github.com/navidrome/navidrome/consts"
+	"github.com/navidrome/navidrome/core/artwork"
+	"github.com/navidrome/navidrome/model"
+	"github.com/navidrome/navidrome/resources"
+	"github.com/navidrome/navidrome/tests"
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+)
+
+var _ = Describe("Artwork", func() {
+	var aw artwork.Artwork
+	var ds model.DataStore
+	var ffmpeg *tests.MockFFmpeg
+
+	BeforeEach(func() {
+		DeferCleanup(configtest.SetupConfig())
+		conf.Server.ImageCacheSize = "0" // Disable cache
+		cache := artwork.GetImageCache()
+		ffmpeg = tests.NewMockFFmpeg("content from ffmpeg")
+		aw = artwork.NewArtwork(ds, cache, ffmpeg)
+	})
+
+	Context("Empty ID", func() {
+		It("returns placeholder if album is not in the DB", func() {
+			r, _, err := aw.Get(context.Background(), "", 0)
+			Expect(err).ToNot(HaveOccurred())
+
+			ph, err := resources.FS().Open(consts.PlaceholderAlbumArt)
+			Expect(err).ToNot(HaveOccurred())
+			phBytes, err := io.ReadAll(ph)
+			Expect(err).ToNot(HaveOccurred())
+
+			result, err := io.ReadAll(r)
+			Expect(err).ToNot(HaveOccurred())
+
+			Expect(result).To(Equal(phBytes))
+		})
+	})
+})
diff --git a/core/artwork/artwork_cache_warmer.go b/core/artwork/cache_warmer.go
similarity index 100%
rename from core/artwork/artwork_cache_warmer.go
rename to core/artwork/cache_warmer.go
diff --git a/core/artwork/image_cache.go b/core/artwork/image_cache.go
new file mode 100644
index 000000000..13c8b8fdd
--- /dev/null
+++ b/core/artwork/image_cache.go
@@ -0,0 +1,47 @@
+package artwork
+
+import (
+	"context"
+	"fmt"
+	"io"
+	"time"
+
+	"github.com/navidrome/navidrome/conf"
+	"github.com/navidrome/navidrome/consts"
+	"github.com/navidrome/navidrome/model"
+	"github.com/navidrome/navidrome/utils/cache"
+	"github.com/navidrome/navidrome/utils/singleton"
+)
+
+type cacheKey struct {
+	artID      model.ArtworkID
+	size       int
+	lastUpdate time.Time
+}
+
+func (k *cacheKey) Key() string {
+	return fmt.Sprintf(
+		"%s.%d.%d.%d.%t",
+		k.artID.ID,
+		k.lastUpdate.UnixMilli(),
+		k.size,
+		conf.Server.CoverJpegQuality,
+		conf.Server.EnableMediaFileCoverArt,
+	)
+}
+
+type imageCache struct {
+	cache.FileCache
+}
+
+func GetImageCache() cache.FileCache {
+	return singleton.GetInstance(func() *imageCache {
+		return &imageCache{
+			FileCache: cache.NewFileCache("Image", conf.Server.ImageCacheSize, consts.ImageCacheDir, consts.DefaultImageCacheMaxItems,
+				func(ctx context.Context, arg cache.Item) (io.Reader, error) {
+					r, _, err := arg.(artworkReader).Reader(ctx)
+					return r, err
+				}),
+		}
+	})
+}
diff --git a/core/artwork/reader_album.go b/core/artwork/reader_album.go
index e89435af7..dbe804b60 100644
--- a/core/artwork/reader_album.go
+++ b/core/artwork/reader_album.go
@@ -12,7 +12,7 @@ import (
 )
 
 type albumArtworkReader struct {
-	cacheItem
+	cacheKey
 	a     *artwork
 	album model.Album
 }
@@ -26,8 +26,8 @@ func newAlbumArtworkReader(ctx context.Context, artwork *artwork, artID model.Ar
 		a:     artwork,
 		album: *al,
 	}
-	a.cacheItem.artID = artID
-	a.cacheItem.lastUpdate = al.UpdatedAt
+	a.cacheKey.artID = artID
+	a.cacheKey.lastUpdate = al.UpdatedAt
 	return a, nil
 }
 
diff --git a/core/artwork/reader_mediafile.go b/core/artwork/reader_mediafile.go
index 50f608534..13b35fcc0 100644
--- a/core/artwork/reader_mediafile.go
+++ b/core/artwork/reader_mediafile.go
@@ -9,7 +9,7 @@ import (
 )
 
 type mediafileArtworkReader struct {
-	cacheItem
+	cacheKey
 	a         *artwork
 	mediafile model.MediaFile
 	album     model.Album
@@ -29,8 +29,8 @@ func newMediafileArtworkReader(ctx context.Context, artwork *artwork, artID mode
 		mediafile: *mf,
 		album:     *al,
 	}
-	a.cacheItem.artID = artID
-	a.cacheItem.lastUpdate = a.LastUpdated()
+	a.cacheKey.artID = artID
+	a.cacheKey.lastUpdate = a.LastUpdated()
 	return a, nil
 }
 
diff --git a/core/artwork/reader_resized.go b/core/artwork/reader_resized.go
index b0fa3da5a..d7594b12c 100644
--- a/core/artwork/reader_resized.go
+++ b/core/artwork/reader_resized.go
@@ -20,21 +20,21 @@ import (
 )
 
 type resizedArtworkReader struct {
-	cacheItem
+	cacheKey
 	a *artwork
 }
 
 func resizedFromOriginal(ctx context.Context, a *artwork, artID model.ArtworkID, size int) (*resizedArtworkReader, error) {
 	r := &resizedArtworkReader{a: a}
-	r.cacheItem.artID = artID
-	r.cacheItem.size = size
+	r.cacheKey.artID = artID
+	r.cacheKey.size = size
 
 	// Get lastUpdated from original artwork
 	original, err := a.getArtworkReader(ctx, artID, 0)
 	if err != nil {
 		return nil, err
 	}
-	r.cacheItem.lastUpdate = original.LastUpdated()
+	r.cacheKey.lastUpdate = original.LastUpdated()
 	return r, nil
 }
 
diff --git a/core/wire_providers.go b/core/wire_providers.go
index 0a4c365f1..699db3bfc 100644
--- a/core/wire_providers.go
+++ b/core/wire_providers.go
@@ -9,17 +9,17 @@ import (
 )
 
 var Set = wire.NewSet(
-	artwork.NewArtwork,
 	NewMediaStreamer,
 	GetTranscodingCache,
-	artwork.GetImageCache,
 	NewArchiver,
 	NewExternalMetadata,
 	NewPlayers,
+	NewShare,
+	NewPlaylists,
 	agents.New,
 	ffmpeg.New,
 	scrobbler.GetPlayTracker,
-	NewShare,
-	NewPlaylists,
+	artwork.NewArtwork,
+	artwork.GetImageCache,
 	artwork.NewCacheWarmer,
 )
diff --git a/model/artwork_id.go b/model/artwork_id.go
index 8928929c3..28ea8ef2b 100644
--- a/model/artwork_id.go
+++ b/model/artwork_id.go
@@ -19,6 +19,9 @@ type ArtworkID struct {
 }
 
 func (id ArtworkID) String() string {
+	if id.ID == "" {
+		return ""
+	}
 	return fmt.Sprintf("%s-%s", id.Kind.prefix, id.ID)
 }