mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-01 07:21:17 +00:00
Mechanical cleanups, no behavior change: - 15 copies of the same id-extraction loop collapse to slice.Map (5 repo mocks, the scanner's track sweep, 4 wantIDs assertions) and slice.ToMap (6 index-by-id loops in the hydration specs). - disc.go built a map[string]bool purely to dedup folder ids and then walked it back into a slice; slice.Unique says that directly. - folders_artist.go's image filter is slice.Filter over model.IsImageFile. - mock_artwork_repo deleted from a map while ranging it; maps.DeleteFunc states the intent. - sort.Slice -> slices.SortFunc + cmp.Or; math.Min/Max -> builtin min/max; make+copy -> bytes.Clone; strings.Split -> SplitSeq on a per-request path; three-clause pixel loops -> for range. - Reuse utils.BaseName where a stem was recomputed by hand. Not at playlist_cover.go:27: that path is a full OS path and utils.BaseName uses path.Base, which does not split backslashes. - Drop a dead nil-guard in agents.go: getAgent returns a bare nil interface, and a type assertion on nil already yields ok == false. cmp.Or was rejected for the gate fallback (func types are not comparable, does not compile) and for ItemArtwork.AttemptedAt (cmp.Or compares time.Time with ==, which includes loc; IsZero does not).
145 lines
4.4 KiB
Go
145 lines
4.4 KiB
Go
package artwork
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"image"
|
|
"image/draw"
|
|
"image/jpeg"
|
|
"image/png"
|
|
"io"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/gen2brain/webp"
|
|
"github.com/navidrome/navidrome/conf"
|
|
"github.com/navidrome/navidrome/core/ffmpeg"
|
|
"github.com/navidrome/navidrome/log"
|
|
xdraw "golang.org/x/image/draw"
|
|
)
|
|
|
|
func init() {
|
|
conf.AddHook(func() {
|
|
// gen2brain/webp picks native vs WASM in its own init(), with no way to switch at
|
|
// runtime: 32-bit builds need the "nodynamic" tag (see Dockerfile) to force WASM.
|
|
if err := webp.Dynamic(); err != nil {
|
|
log.Debug("Artwork: Using WASM WebP encoder/decoder", "reason", err)
|
|
} else {
|
|
log.Debug("Artwork: Using native libwebp for WebP encoding/decoding")
|
|
}
|
|
})
|
|
}
|
|
|
|
var bufPool = sync.Pool{
|
|
New: func() any {
|
|
return new(bytes.Buffer)
|
|
},
|
|
}
|
|
|
|
// resizeImageData resizes raw image bytes to fit size, preserving animation where
|
|
// possible. A nil reader means the image was already within bounds (no resize needed).
|
|
func resizeImageData(ctx context.Context, ffm ffmpeg.FFmpeg, data []byte, size int, square bool) (io.Reader, int, error) {
|
|
start := time.Now()
|
|
defer func() {
|
|
log.Trace(ctx, "Artwork: Resized image", "bytes", len(data), "size", size, "square", square,
|
|
"elapsed", time.Since(start))
|
|
}()
|
|
if isAnimatedGIF(data) {
|
|
if ffm.IsAvailable() {
|
|
// Animated GIF: convert to animated WebP via ffmpeg (with optional resize)
|
|
r, err := ffm.ConvertAnimatedImage(ctx, bytes.NewReader(data), size, conf.Server.CoverArtQuality)
|
|
if err == nil {
|
|
return r, 0, nil
|
|
}
|
|
log.Warn(ctx, "Artwork: Could not convert animated GIF, falling back to static", err)
|
|
}
|
|
} else if isAnimatedWebP(data) || isAnimatedPNG(data) {
|
|
// Animated WebP/APNG: return original as-is (ffmpeg can't re-encode these)
|
|
return bytes.NewReader(data), 0, nil
|
|
}
|
|
|
|
return resizeStaticImage(data, size, square)
|
|
}
|
|
|
|
// toFastScaleType converts types x/image/draw has no optimized scaler for (e.g. *image.NYCbCrA,
|
|
// *image.Paletted) to *image.RGBA, avoiding CatmullRom.Scale's generic per-pixel fallback.
|
|
func toFastScaleType(img image.Image) image.Image {
|
|
switch img.(type) {
|
|
case *image.RGBA, *image.NRGBA, *image.Gray, *image.YCbCr:
|
|
return img
|
|
default:
|
|
rgba := image.NewRGBA(img.Bounds())
|
|
draw.Draw(rgba, rgba.Bounds(), img, img.Bounds().Min, draw.Src)
|
|
return rgba
|
|
}
|
|
}
|
|
|
|
func resizeStaticImage(data []byte, size int, square bool) (io.Reader, int, error) {
|
|
original, format, err := image.Decode(bytes.NewReader(data))
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
bounds := original.Bounds()
|
|
originalSize := max(bounds.Max.X, bounds.Max.Y)
|
|
|
|
// Clamp size to original dimensions - upscaling wastes resources and adds no information
|
|
if size > originalSize {
|
|
size = originalSize
|
|
}
|
|
|
|
if originalSize <= size && !square {
|
|
return nil, originalSize, nil
|
|
}
|
|
|
|
// Calculate aspect-fit dimensions
|
|
srcW, srcH := bounds.Dx(), bounds.Dy()
|
|
scale := float64(size) / float64(max(srcW, srcH))
|
|
dstW := int(float64(srcW) * scale)
|
|
dstH := int(float64(srcH) * scale)
|
|
|
|
var dst *image.NRGBA
|
|
var dstRect image.Rectangle
|
|
if square {
|
|
// Square canvas with image centered (transparent padding via zero-initialized NRGBA)
|
|
dst = image.NewNRGBA(image.Rect(0, 0, size, size))
|
|
offsetX := (size - dstW) / 2
|
|
offsetY := (size - dstH) / 2
|
|
dstRect = image.Rect(offsetX, offsetY, offsetX+dstW, offsetY+dstH)
|
|
} else {
|
|
// Tight-fit canvas
|
|
dst = image.NewNRGBA(image.Rect(0, 0, dstW, dstH))
|
|
dstRect = dst.Bounds()
|
|
}
|
|
original = toFastScaleType(original)
|
|
xdraw.CatmullRom.Scale(dst, dstRect, original, bounds, draw.Src, nil)
|
|
|
|
buf := bufPool.Get().(*bytes.Buffer)
|
|
buf.Reset()
|
|
if conf.Server.EnableWebPEncoding {
|
|
err = webp.Encode(buf, dst, webp.Options{Quality: conf.Server.CoverArtQuality})
|
|
} else if format == "png" || square {
|
|
err = png.Encode(buf, dst)
|
|
} else {
|
|
err = jpeg.Encode(buf, dst, &jpeg.Options{Quality: conf.Server.CoverArtQuality})
|
|
}
|
|
if err != nil {
|
|
bufPool.Put(buf)
|
|
return nil, originalSize, err
|
|
}
|
|
// Copy bytes before returning buffer to pool (pool may reuse the buffer)
|
|
encoded := bytes.Clone(buf.Bytes())
|
|
bufPool.Put(buf)
|
|
return bytes.NewReader(encoded), originalSize, nil
|
|
}
|
|
|
|
// formatQualityTag folds the encoder config (WebP toggle + quality) into a cache-key
|
|
// fragment, so flipping either setting invalidates previously-encoded sized artwork.
|
|
func formatQualityTag() string {
|
|
if conf.Server.EnableWebPEncoding {
|
|
return fmt.Sprintf("webp%d", conf.Server.CoverArtQuality)
|
|
}
|
|
return fmt.Sprintf("q%d", conf.Server.CoverArtQuality)
|
|
}
|