mirror of
https://github.com/navidrome/navidrome.git
synced 2025-04-15 11:40:36 +03:00
180 lines
4.4 KiB
Go
180 lines
4.4 KiB
Go
package core
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"image"
|
|
_ "image/gif"
|
|
"image/jpeg"
|
|
"image/png"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/dhowden/tag"
|
|
"github.com/disintegration/imaging"
|
|
"github.com/navidrome/navidrome/conf"
|
|
"github.com/navidrome/navidrome/consts"
|
|
"github.com/navidrome/navidrome/log"
|
|
"github.com/navidrome/navidrome/model"
|
|
"github.com/navidrome/navidrome/resources"
|
|
_ "golang.org/x/image/webp"
|
|
)
|
|
|
|
type Artwork interface {
|
|
Get(ctx context.Context, id string, size int) (io.ReadCloser, error)
|
|
}
|
|
|
|
func NewArtwork(ds model.DataStore) Artwork {
|
|
return &artwork{ds: ds}
|
|
}
|
|
|
|
type artwork struct {
|
|
ds model.DataStore
|
|
}
|
|
|
|
func (a *artwork) Get(ctx context.Context, id string, size int) (io.ReadCloser, error) {
|
|
r, _, err := a.get(ctx, id, size)
|
|
return r, err
|
|
}
|
|
|
|
func (a *artwork) get(ctx context.Context, id string, size int) (reader io.ReadCloser, path string, err error) {
|
|
artId, err := model.ParseArtworkID(id)
|
|
if err != nil {
|
|
return nil, "", errors.New("invalid ID")
|
|
}
|
|
|
|
// If requested a resized
|
|
if size > 0 {
|
|
return a.resizedFromOriginal(ctx, id, size)
|
|
}
|
|
|
|
id = artId.ID
|
|
al, err := a.ds.Album(ctx).Get(id)
|
|
if errors.Is(err, model.ErrNotFound) {
|
|
r, path := fromPlaceholder()()
|
|
return r, path, nil
|
|
}
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
r, path := extractImage(ctx, artId,
|
|
fromExternalFile(al.ImageFiles, "cover.png", "cover.jpg", "cover.jpeg", "cover.webp"),
|
|
fromExternalFile(al.ImageFiles, "folder.png", "folder.jpg", "folder.jpeg", "folder.webp"),
|
|
fromExternalFile(al.ImageFiles, "album.png", "album.jpg", "album.jpeg", "album.webp"),
|
|
fromExternalFile(al.ImageFiles, "albumart.png", "albumart.jpg", "albumart.jpeg", "albumart.webp"),
|
|
fromExternalFile(al.ImageFiles, "front.png", "front.jpg", "front.jpeg", "front.webp"),
|
|
fromTag(al.EmbedArtPath),
|
|
fromPlaceholder(),
|
|
)
|
|
return r, path, nil
|
|
}
|
|
|
|
func (a *artwork) resizedFromOriginal(ctx context.Context, id string, size int) (io.ReadCloser, string, error) {
|
|
r, path, err := a.get(ctx, id, 0)
|
|
if err != nil || r == nil {
|
|
return nil, "", err
|
|
}
|
|
defer r.Close()
|
|
usePng := strings.ToLower(filepath.Ext(path)) == ".png"
|
|
r, err = resizeImage(r, size, usePng)
|
|
if err != nil {
|
|
r, path := fromPlaceholder()()
|
|
return r, path, err
|
|
}
|
|
return r, fmt.Sprintf("%s@%d", path, size), nil
|
|
}
|
|
|
|
func extractImage(ctx context.Context, artId model.ArtworkID, extractFuncs ...func() (io.ReadCloser, string)) (io.ReadCloser, string) {
|
|
for _, f := range extractFuncs {
|
|
r, path := f()
|
|
if r != nil {
|
|
log.Trace(ctx, "Found artwork", "artId", artId, "path", path)
|
|
return r, path
|
|
}
|
|
}
|
|
log.Error(ctx, "extractImage should never reach this point!", "artId", artId, "path")
|
|
return nil, ""
|
|
}
|
|
|
|
// This seems unoptimized, but we need to make sure the priority order of validNames
|
|
// is preserved (i.e. png is better than jpg)
|
|
func fromExternalFile(files string, validNames ...string) func() (io.ReadCloser, string) {
|
|
return func() (io.ReadCloser, string) {
|
|
fileList := filepath.SplitList(files)
|
|
for _, validName := range validNames {
|
|
for _, file := range fileList {
|
|
_, name := filepath.Split(file)
|
|
if !strings.EqualFold(validName, name) {
|
|
continue
|
|
}
|
|
f, err := os.Open(file)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
return f, file
|
|
}
|
|
}
|
|
return nil, ""
|
|
}
|
|
}
|
|
|
|
func fromTag(path string) func() (io.ReadCloser, string) {
|
|
return func() (io.ReadCloser, string) {
|
|
if path == "" {
|
|
return nil, ""
|
|
}
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, ""
|
|
}
|
|
defer f.Close()
|
|
|
|
m, err := tag.ReadFrom(f)
|
|
if err != nil {
|
|
return nil, ""
|
|
}
|
|
|
|
picture := m.Picture()
|
|
if picture == nil {
|
|
return nil, ""
|
|
}
|
|
return io.NopCloser(bytes.NewReader(picture.Data)), path
|
|
}
|
|
}
|
|
|
|
func fromPlaceholder() func() (io.ReadCloser, string) {
|
|
return func() (io.ReadCloser, string) {
|
|
r, _ := resources.FS().Open(consts.PlaceholderAlbumArt)
|
|
return r, consts.PlaceholderAlbumArt
|
|
}
|
|
}
|
|
|
|
func resizeImage(reader io.Reader, size int, usePng bool) (io.ReadCloser, error) {
|
|
img, _, err := image.Decode(reader)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Preserve the aspect ratio of the image.
|
|
var m *image.NRGBA
|
|
bounds := img.Bounds()
|
|
if bounds.Max.X > bounds.Max.Y {
|
|
m = imaging.Resize(img, size, 0, imaging.Lanczos)
|
|
} else {
|
|
m = imaging.Resize(img, 0, size, imaging.Lanczos)
|
|
}
|
|
|
|
buf := new(bytes.Buffer)
|
|
if usePng {
|
|
err = png.Encode(buf, m)
|
|
} else {
|
|
err = jpeg.Encode(buf, m, &jpeg.Options{Quality: conf.Server.CoverJpegQuality})
|
|
}
|
|
return io.NopCloser(buf), err
|
|
}
|