diff --git a/core/artwork/artwork.go b/core/artwork/artwork.go index a1ca7d2dd..0f4577d53 100644 --- a/core/artwork/artwork.go +++ b/core/artwork/artwork.go @@ -19,7 +19,7 @@ import ( ) type Artwork interface { - Get(ctx context.Context, id string, size int) (io.ReadCloser, error) + Get(ctx context.Context, id string, size int) (io.ReadCloser, time.Time, error) } func NewArtwork(ds model.DataStore, cache cache.FileCache, ffmpeg ffmpeg.FFmpeg) Artwork { @@ -38,7 +38,7 @@ type artworkReader interface { Reader(ctx context.Context) (io.ReadCloser, string, error) } -func (a *artwork) Get(ctx context.Context, id string, size int) (reader io.ReadCloser, err error) { +func (a *artwork) Get(ctx context.Context, id string, size int) (reader io.ReadCloser, lastUpdate time.Time, err error) { ctx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() @@ -46,31 +46,38 @@ func (a *artwork) Get(ctx context.Context, id string, size int) (reader io.ReadC if id != "" { artID, err = model.ParseArtworkID(id) if err != nil { - return nil, errors.New("invalid ID") + return nil, time.Time{}, errors.New("invalid ID") } } - var artReader artworkReader - switch artID.Kind { - case model.KindAlbumArtwork: - artReader, err = newAlbumArtworkReader(ctx, a, artID) - case model.KindMediaFileArtwork: - artReader, err = newMediafileArtworkReader(ctx, a, artID) - default: - artReader, err = newEmptyIDReader(ctx, artID) - } + artReader, err := a.getArtworkReader(ctx, artID, size) if err != nil { - return nil, err - } - if size > 0 { - artReader = resizedFromOriginal(artReader, artID, size) + return nil, time.Time{}, err } r, err := a.cache.Get(ctx, artReader) if err != nil && !errors.Is(err, context.Canceled) { log.Error(ctx, "Error accessing image cache", "id", id, "size", size, err) } - return r, err + return r, artReader.LastUpdated(), err +} + +func (a *artwork) getArtworkReader(ctx context.Context, artID model.ArtworkID, size int) (artworkReader, error) { + var artReader artworkReader + var err error + if size > 0 { + artReader, err = resizedFromOriginal(ctx, a, artID, size) + } else { + switch artID.Kind { + case model.KindAlbumArtwork: + artReader, err = newAlbumArtworkReader(ctx, a, artID) + case model.KindMediaFileArtwork: + artReader, err = newMediafileArtworkReader(ctx, a, artID) + default: + artReader, err = newEmptyIDReader(ctx, artID) + } + } + return artReader, err } type cacheItem struct { @@ -80,7 +87,14 @@ type cacheItem struct { } func (i *cacheItem) Key() string { - return fmt.Sprintf("%s.%d.%d.%d", i.artID.ID, i.lastUpdate.UnixMilli(), i.size, conf.Server.CoverJpegQuality) + return fmt.Sprintf( + "%s.%d.%d.%d.%t", + i.artID.ID, + i.lastUpdate.UnixMilli(), + i.size, + conf.Server.CoverJpegQuality, + conf.Server.DevFastAccessCoverArt, + ) } type imageCache struct { diff --git a/core/artwork/artwork_cache_warmer.go b/core/artwork/artwork_cache_warmer.go index f1e12449a..a3ef8d8af 100644 --- a/core/artwork/artwork_cache_warmer.go +++ b/core/artwork/artwork_cache_warmer.go @@ -46,7 +46,7 @@ func (a *cacheWarmer) run(ctx context.Context) { } func (a *cacheWarmer) doCacheImage(ctx context.Context, id string) error { - r, err := a.artwork.Get(ctx, id, 0) + r, _, err := a.artwork.Get(ctx, id, 0) if err != nil { return fmt.Errorf("error cacheing id='%s': %w", id, err) } diff --git a/core/artwork/reader_mediafile.go b/core/artwork/reader_mediafile.go index e2a977c11..50f608534 100644 --- a/core/artwork/reader_mediafile.go +++ b/core/artwork/reader_mediafile.go @@ -56,7 +56,7 @@ func (a *mediafileArtworkReader) Reader(ctx context.Context) (io.ReadCloser, str func fromAlbum(ctx context.Context, a *artwork, id model.ArtworkID) sourceFunc { return func() (io.ReadCloser, string, error) { - r, err := a.Get(ctx, id.String(), 0) + r, _, err := a.Get(ctx, id.String(), 0) if err != nil { return nil, "", err } diff --git a/core/artwork/reader_resized.go b/core/artwork/reader_resized.go index 9e2afb474..b0fa3da5a 100644 --- a/core/artwork/reader_resized.go +++ b/core/artwork/reader_resized.go @@ -21,15 +21,21 @@ import ( type resizedArtworkReader struct { cacheItem - original artworkReader + a *artwork } -func resizedFromOriginal(original artworkReader, artID model.ArtworkID, size int) *resizedArtworkReader { - r := &resizedArtworkReader{original: original} +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 + + // Get lastUpdated from original artwork + original, err := a.getArtworkReader(ctx, artID, 0) + if err != nil { + return nil, err + } r.cacheItem.lastUpdate = original.LastUpdated() - return r + return r, nil } func (a *resizedArtworkReader) LastUpdated() time.Time { @@ -37,13 +43,16 @@ func (a *resizedArtworkReader) LastUpdated() time.Time { } func (a *resizedArtworkReader) Reader(ctx context.Context) (io.ReadCloser, string, error) { - orig, path, err := a.original.Reader(ctx) + // Get artwork in original size, possibly from cache + orig, _, err := a.a.Get(ctx, a.artID.String(), 0) if err != nil { return nil, "", err } + // 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(orig, buf) + defer orig.Close() resized, origSize, err := resizeImage(r, a.size) log.Trace(ctx, "Resizing artwork", "artID", a.artID, "original", origSize, "resized", a.size) @@ -53,7 +62,7 @@ func (a *resizedArtworkReader) Reader(ctx context.Context) (io.ReadCloser, strin _, _ = io.Copy(io.Discard, r) return io.NopCloser(buf), "", nil } - return io.NopCloser(resized), fmt.Sprintf("%s@%d", path, a.size), nil + return io.NopCloser(resized), fmt.Sprintf("%s@%d", a.artID, a.size), nil } func asImageReader(r io.Reader) (io.Reader, string, error) { diff --git a/server/subsonic/media_retrieval.go b/server/subsonic/media_retrieval.go index 4e5a37d59..51a2ef59f 100644 --- a/server/subsonic/media_retrieval.go +++ b/server/subsonic/media_retrieval.go @@ -6,6 +6,7 @@ import ( "io" "net/http" "regexp" + "time" "github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/consts" @@ -55,9 +56,10 @@ func (api *Router) GetCoverArt(w http.ResponseWriter, r *http.Request) (*respons id := utils.ParamString(r, "id") size := utils.ParamInt(r, "size", 0) + imgReader, lastUpdate, err := api.artwork.Get(r.Context(), id, size) w.Header().Set("cache-control", "public, max-age=315360000") + w.Header().Set("last-modified", lastUpdate.Format(time.RFC1123)) - imgReader, err := api.artwork.Get(r.Context(), id, size) switch { case errors.Is(err, context.Canceled): return nil, nil