From 73bb0104f09e21f1b7782ae6a81af2a80e7132b7 Mon Sep 17 00:00:00 2001 From: Deluan <deluan@navidrome.org> Date: Thu, 22 Dec 2022 18:06:29 -0500 Subject: [PATCH] Cache original images --- core/artwork.go | 79 +++++++++++++++++++++-------------- core/artwork_internal_test.go | 31 +++++++++++--- utils/cache/file_caches.go | 6 ++- 3 files changed, 77 insertions(+), 39 deletions(-) diff --git a/core/artwork.go b/core/artwork.go index b562ec81c..eb754d5fb 100644 --- a/core/artwork.go +++ b/core/artwork.go @@ -1,6 +1,7 @@ package core import ( + "bufio" "bytes" "context" "errors" @@ -10,6 +11,7 @@ import ( "image/jpeg" "image/png" "io" + "net/http" "os" "path/filepath" "reflect" @@ -53,6 +55,17 @@ func (a *artwork) Get(ctx context.Context, id string, size int) (io.ReadCloser, } } + // If requested a resized image, get the original (possibly from cache) + if size > 0 && id != "" { + r, err := a.Get(ctx, id, 0) + if err != nil { + return nil, err + } + defer r.Close() + resized, err := a.resizedFromOriginal(ctx, artID, r, size) + return io.NopCloser(resized), err + } + key := &artworkKey{a: a, artID: artID, size: size} r, err := a.cache.Get(ctx, key) @@ -62,12 +75,16 @@ func (a *artwork) Get(ctx context.Context, id string, size int) (io.ReadCloser, return r, err } -func (a *artwork) get(ctx context.Context, artID model.ArtworkID, size int) (reader io.ReadCloser, path string, err error) { - // If requested a resized image - if size > 0 { - return a.resizedFromOriginal(ctx, artID, size) - } +type fromFunc func() (io.ReadCloser, string) +func (f fromFunc) String() string { + name := runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name() + name = strings.TrimPrefix(name, "github.com/navidrome/navidrome/core.") + name = strings.TrimSuffix(name, ".func1") + return name +} + +func (a *artwork) get(ctx context.Context, artID model.ArtworkID, size int) (reader io.ReadCloser, path string, err error) { switch artID.Kind { case model.KindAlbumArtwork: reader, path = a.extractAlbumImage(ctx, artID) @@ -116,34 +133,19 @@ func (a *artwork) extractMediaFileImage(ctx context.Context, artID model.Artwork return extractImage(ctx, artID, ff...) } -func (a *artwork) resizedFromOriginal(ctx context.Context, artID model.ArtworkID, size int) (io.ReadCloser, string, error) { - // first get original image - original, path, err := a.get(ctx, artID, 0) - if err != nil || original == nil { - return nil, "", err - } - defer original.Close() - +func (a *artwork) resizedFromOriginal(ctx context.Context, artID model.ArtworkID, original io.Reader, size int) (io.Reader, error) { // Keep a copy of the original data. In case we can't resize it, send it as is buf := new(bytes.Buffer) r := io.TeeReader(original, buf) - usePng := strings.ToLower(filepath.Ext(path)) == ".png" - resized, err := resizeImage(r, size, usePng) + resized, err := resizeImage(r, size) if err != nil { - log.Warn(ctx, "Could not resize image. Sending it as-is", "artID", artID, "size", size, err) - return io.NopCloser(buf), path, nil + log.Warn(ctx, "Could not resize image. Will return image as is", "artID", artID, "size", size, err) + // Force finish reading any remaining data + _, _ = io.Copy(io.Discard, r) + return buf, nil } - return resized, fmt.Sprintf("%s@%d", path, size), nil -} - -type fromFunc func() (io.ReadCloser, string) - -func (f fromFunc) String() string { - name := runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name() - name = strings.TrimPrefix(name, "github.com/navidrome/navidrome/core.") - name = strings.TrimSuffix(name, ".func1") - return name + return resized, nil } func extractImage(ctx context.Context, artID model.ArtworkID, extractFuncs ...fromFunc) (io.ReadCloser, string) { @@ -256,8 +258,22 @@ func fromPlaceholder() fromFunc { } } -func resizeImage(reader io.Reader, size int, usePng bool) (io.ReadCloser, error) { - img, _, err := image.Decode(reader) +func asImageReader(r io.Reader) (io.Reader, string, error) { + br := bufio.NewReader(r) + buf, err := br.Peek(512) + if err != nil { + return nil, "", err + } + return br, http.DetectContentType(buf), nil +} + +func resizeImage(reader io.Reader, size int) (io.Reader, error) { + r, format, err := asImageReader(reader) + if err != nil { + return nil, err + } + + img, _, err := image.Decode(r) if err != nil { return nil, err } @@ -272,12 +288,13 @@ func resizeImage(reader io.Reader, size int, usePng bool) (io.ReadCloser, error) } buf := new(bytes.Buffer) - if usePng { + buf.Reset() + if format == "image/png" { err = png.Encode(buf, m) } else { err = jpeg.Encode(buf, m, &jpeg.Options{Quality: conf.Server.CoverJpegQuality}) } - return io.NopCloser(buf), err + return buf, err } type ArtworkCache struct { diff --git a/core/artwork_internal_test.go b/core/artwork_internal_test.go index 2bc5bf6cb..34325bf44 100644 --- a/core/artwork_internal_test.go +++ b/core/artwork_internal_test.go @@ -164,17 +164,36 @@ var _ = Describe("Artwork", func() { Context("Resize", func() { BeforeEach(func() { ds.Album(ctx).(*tests.MockAlbumRepo).SetData(model.Albums{ - alOnlyExternal, + alMultipleCovers, }) }) - It("returns external cover resized", func() { - r, path, err := aw.get(context.Background(), alOnlyExternal.CoverArtID(), 300) + 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) + Expect(err).ToNot(HaveOccurred()) + + br, format, err := asImageReader(r) + Expect(format).To(Equal("image/png")) + Expect(err).ToNot(HaveOccurred()) + + img, _, err := image.Decode(br) Expect(err).ToNot(HaveOccurred()) - Expect(path).To(Equal("tests/fixtures/front.png@300")) - img, _, err := image.Decode(r) - Expect(err).To(BeNil()) Expect(img.Bounds().Size().X).To(Equal(300)) Expect(img.Bounds().Size().Y).To(Equal(300)) }) + 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().String(), 200) + Expect(err).ToNot(HaveOccurred()) + + br, format, err := asImageReader(r) + Expect(format).To(Equal("image/jpeg")) + Expect(err).ToNot(HaveOccurred()) + + img, _, err := image.Decode(br) + Expect(err).ToNot(HaveOccurred()) + Expect(img.Bounds().Size().X).To(Equal(200)) + Expect(img.Bounds().Size().Y).To(Equal(200)) + }) }) }) diff --git a/utils/cache/file_caches.go b/utils/cache/file_caches.go index 97a3f6f65..6cb3a42cd 100644 --- a/utils/cache/file_caches.go +++ b/utils/cache/file_caches.go @@ -122,10 +122,12 @@ func (fc *fileCache) Get(ctx context.Context, arg Item) (*CachedStream, error) { } go func() { if err := copyAndClose(w, reader); err != nil { - log.Debug(ctx, "Error populating cache", "key", key, err) + log.Debug(ctx, "Error storing file in cache", "cache", fc.name, "key", key, err) if err = fc.invalidate(ctx, key); err != nil { - log.Warn(ctx, "Error removing key from cache", "key", key, err) + log.Warn(ctx, "Error removing key from cache", "cache", fc.name, "key", key, err) } + } else { + log.Trace(ctx, "File stored in cache", "cache", fc.name, "key", key) } }() }