Merge branch 'master' into package-lock/integrit

This commit is contained in:
Deluan Quintão 2026-04-05 10:37:34 -04:00 committed by GitHub
commit 66ba2d82af
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 365 additions and 142 deletions

View File

@ -1,6 +1,8 @@
GO_VERSION=$(shell grep "^go " go.mod | cut -f 2 -d ' ') GO_VERSION=$(shell grep "^go " go.mod | cut -f 2 -d ' ')
NODE_VERSION=$(shell cat .nvmrc) NODE_VERSION=$(shell cat .nvmrc)
GO_BUILD_TAGS=netgo,sqlite_fts5
comma:=,
GO_BUILD_TAGS=netgo,sqlite_fts5$(if $(EXTRA_BUILD_TAGS),$(comma)$(EXTRA_BUILD_TAGS))
# Set global environment variables, required for most targets # Set global environment variables, required for most targets
export CGO_CFLAGS_ALLOW=--define-prefix export CGO_CFLAGS_ALLOW=--define-prefix

View File

@ -70,6 +70,7 @@ type configOptions struct {
MPVCmdTemplate string MPVCmdTemplate string
CoverArtPriority string CoverArtPriority string
CoverArtQuality int CoverArtQuality int
EnableWebPEncoding bool
ArtistArtPriority string ArtistArtPriority string
ArtistImageFolder string ArtistImageFolder string
DiscArtPriority string DiscArtPriority string
@ -87,6 +88,7 @@ type configOptions struct {
DefaultLanguage string DefaultLanguage string
DefaultUIVolume int DefaultUIVolume int
UISearchDebounceMs int UISearchDebounceMs int
UICoverArtSize int
EnableReplayGain bool EnableReplayGain bool
EnableCoverAnimation bool EnableCoverAnimation bool
EnableNowPlaying bool EnableNowPlaying bool
@ -141,7 +143,6 @@ type configOptions struct {
DevOptimizeDB bool DevOptimizeDB bool
DevPreserveUnicodeInExternalCalls bool DevPreserveUnicodeInExternalCalls bool
DevEnableMediaFileProbe bool DevEnableMediaFileProbe bool
DevJpegCoverArt bool
} }
type scannerOptions struct { type scannerOptions struct {
@ -424,6 +425,13 @@ func Load(noConfigDump bool) {
// Removed options // Removed options
logRemovedOptions("Spotify.ID", "Spotify.Secret") logRemovedOptions("Spotify.ID", "Spotify.Secret")
// Validate other options
if Server.UICoverArtSize < 200 || Server.UICoverArtSize > 1200 {
newValue := max(200, min(1200, Server.UICoverArtSize))
log.Warn("UICoverArtSize must be between 200 and 1200, clamping", "value", Server.UICoverArtSize, "newValue", newValue)
Server.UICoverArtSize = newValue
}
// Call init hooks // Call init hooks
for _, hook := range hooks { for _, hook := range hooks {
hook() hook()
@ -712,10 +720,13 @@ func setViperDefaults() {
viper.SetDefault("ignoredarticles", "The El La Los Las Le Les Os As O A") viper.SetDefault("ignoredarticles", "The El La Los Las Le Les Os As O A")
viper.SetDefault("indexgroups", "A B C D E F G H I J K L M N O P Q R S T U V W X-Z(XYZ) [Unknown]([)") viper.SetDefault("indexgroups", "A B C D E F G H I J K L M N O P Q R S T U V W X-Z(XYZ) [Unknown]([)")
viper.SetDefault("ffmpegpath", "") viper.SetDefault("ffmpegpath", "")
viper.SetDefault("mpvpath", "")
viper.SetDefault("mpvcmdtemplate", "mpv --audio-device=%d --no-audio-display %f --input-ipc-server=%s") viper.SetDefault("mpvcmdtemplate", "mpv --audio-device=%d --no-audio-display %f --input-ipc-server=%s")
viper.SetDefault("coverartpriority", "cover.*, folder.*, front.*, embedded, external") viper.SetDefault("coverartpriority", "cover.*, folder.*, front.*, embedded, external")
viper.SetDefault("coverartquality", 75) viper.SetDefault("coverartquality", 75)
viper.SetDefault("enablewebpencoding", false)
viper.SetDefault("artistartpriority", "artist.*, album/artist.*, external") viper.SetDefault("artistartpriority", "artist.*, album/artist.*, external")
viper.SetDefault("artistimagefolder", "")
viper.SetDefault("discartpriority", "disc*.*, cd*.*, cover.*, folder.*, front.*, discsubtitle, embedded") viper.SetDefault("discartpriority", "disc*.*, cd*.*, cover.*, folder.*, front.*, discsubtitle, embedded")
viper.SetDefault("lyricspriority", ".lrc,.txt,embedded") viper.SetDefault("lyricspriority", ".lrc,.txt,embedded")
viper.SetDefault("enablegravatar", false) viper.SetDefault("enablegravatar", false)
@ -726,6 +737,7 @@ func setViperDefaults() {
viper.SetDefault("defaultlanguage", "") viper.SetDefault("defaultlanguage", "")
viper.SetDefault("defaultuivolume", consts.DefaultUIVolume) viper.SetDefault("defaultuivolume", consts.DefaultUIVolume)
viper.SetDefault("uisearchdebouncems", consts.DefaultUISearchDebounceMs) viper.SetDefault("uisearchdebouncems", consts.DefaultUISearchDebounceMs)
viper.SetDefault("uicoverartsize", consts.DefaultUICoverArtSize)
viper.SetDefault("enablereplaygain", true) viper.SetDefault("enablereplaygain", true)
viper.SetDefault("enablecoveranimation", true) viper.SetDefault("enablecoveranimation", true)
viper.SetDefault("enablenowplaying", true) viper.SetDefault("enablenowplaying", true)
@ -794,6 +806,7 @@ func setViperDefaults() {
viper.SetDefault("plugins.enabled", true) viper.SetDefault("plugins.enabled", true)
viper.SetDefault("plugins.cachesize", "200MB") viper.SetDefault("plugins.cachesize", "200MB")
viper.SetDefault("plugins.autoreload", false) viper.SetDefault("plugins.autoreload", false)
viper.SetDefault("plugins.loglevel", "")
// DevFlags. These are used to enable/disable debugging and incomplete features // DevFlags. These are used to enable/disable debugging and incomplete features
viper.SetDefault("devlogsourceline", false) viper.SetDefault("devlogsourceline", false)
@ -807,7 +820,7 @@ func setViperDefaults() {
viper.SetDefault("devuishowconfig", true) viper.SetDefault("devuishowconfig", true)
viper.SetDefault("devneweventstream", true) viper.SetDefault("devneweventstream", true)
viper.SetDefault("devoffsetoptimize", 50000) viper.SetDefault("devoffsetoptimize", 50000)
viper.SetDefault("devartworkmaxrequests", max(4, runtime.NumCPU())) viper.SetDefault("devartworkmaxrequests", max(2, runtime.NumCPU()/2))
viper.SetDefault("devartworkthrottlebackloglimit", consts.RequestThrottleBacklogLimit) viper.SetDefault("devartworkthrottlebackloglimit", consts.RequestThrottleBacklogLimit)
viper.SetDefault("devartworkthrottlebacklogtimeout", consts.RequestThrottleBacklogTimeout) viper.SetDefault("devartworkthrottlebacklogtimeout", consts.RequestThrottleBacklogTimeout)
viper.SetDefault("devartistinfotimetolive", consts.ArtistInfoTimeToLive) viper.SetDefault("devartistinfotimetolive", consts.ArtistInfoTimeToLive)
@ -823,7 +836,6 @@ func setViperDefaults() {
viper.SetDefault("devoptimizedb", true) viper.SetDefault("devoptimizedb", true)
viper.SetDefault("devpreserveunicodeinexternalcalls", false) viper.SetDefault("devpreserveunicodeinexternalcalls", false)
viper.SetDefault("devenablemediafileprobe", true) viper.SetDefault("devenablemediafileprobe", true)
viper.SetDefault("devjpegcoverart", false)
} }
func init() { func init() {

View File

@ -85,11 +85,9 @@ const (
) )
const ( const (
UICoverArtSize = 600 DefaultUICoverArtSize = 300
) )
var CacheWarmerImageSizes = []int{UICoverArtSize}
// Prometheus options // Prometheus options
const ( const (
PrometheusDefaultPath = "/metrics" PrometheusDefaultPath = "/metrics"

View File

@ -380,24 +380,24 @@ var _ = Describe("Artwork", func() {
}) })
}) })
When("Square is false", func() { When("Square is false", func() {
It("returns WebP even if original image is a PNG", func() { It("returns PNG if original image is a PNG", func() {
conf.Server.CoverArtPriority = "front.png" conf.Server.CoverArtPriority = "front.png"
r, _, err := aw.Get(context.Background(), alMultipleCovers.CoverArtID(), 15, false) r, _, err := aw.Get(context.Background(), alMultipleCovers.CoverArtID(), 15, false)
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
img, format, err := image.Decode(r) img, format, err := image.Decode(r)
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
Expect(format).To(Equal("webp")) Expect(format).To(Equal("png"))
Expect(img.Bounds().Size().X).To(Equal(15)) Expect(img.Bounds().Size().X).To(Equal(15))
Expect(img.Bounds().Size().Y).To(Equal(15)) Expect(img.Bounds().Size().Y).To(Equal(15))
}) })
It("returns WebP if original image is not a PNG", func() { It("returns JPEG if original image is not a PNG", func() {
conf.Server.CoverArtPriority = "cover.jpg" conf.Server.CoverArtPriority = "cover.jpg"
r, _, err := aw.Get(context.Background(), alMultipleCovers.CoverArtID(), 200, false) r, _, err := aw.Get(context.Background(), alMultipleCovers.CoverArtID(), 200, false)
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
img, format, err := image.Decode(r) img, format, err := image.Decode(r)
Expect(format).To(Equal("webp")) Expect(format).To(Equal("jpeg"))
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
Expect(img.Bounds().Size().X).To(Equal(200)) Expect(img.Bounds().Size().X).To(Equal(200))
Expect(img.Bounds().Size().Y).To(Equal(200)) Expect(img.Bounds().Size().Y).To(Equal(200))
@ -430,24 +430,51 @@ var _ = Describe("Artwork", func() {
Expect(img.Bounds().Size().X).To(Equal(size)) Expect(img.Bounds().Size().X).To(Equal(size))
Expect(img.Bounds().Size().Y).To(Equal(size)) Expect(img.Bounds().Size().Y).To(Equal(size))
}, },
Entry("portrait png image", "png", "webp", false, 200), Entry("portrait png image", "png", "png", false, 200),
Entry("landscape png image", "png", "webp", true, 200), Entry("landscape png image", "png", "png", true, 200),
Entry("portrait jpg image", "jpg", "webp", false, 200), Entry("portrait jpg image", "jpg", "png", false, 200),
Entry("landscape jpg image", "jpg", "webp", true, 200), Entry("landscape jpg image", "jpg", "png", true, 200),
) )
}) })
When("DevJpegCoverArt is true and square is false", func() { When("EnableWebPEncoding is true and square is false", func() {
BeforeEach(func() { BeforeEach(func() {
conf.Server.DevJpegCoverArt = true conf.Server.EnableWebPEncoding = true
}) })
It("returns JPEG even if original image is a PNG", func() { It("returns WebP even if original image is a PNG", func() {
conf.Server.CoverArtPriority = "front.png" conf.Server.CoverArtPriority = "front.png"
r, _, err := aw.Get(context.Background(), alMultipleCovers.CoverArtID(), 15, false) r, _, err := aw.Get(context.Background(), alMultipleCovers.CoverArtID(), 15, false)
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
img, format, err := image.Decode(r) img, format, err := image.Decode(r)
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
Expect(format).To(Equal("jpeg")) Expect(format).To(Equal("webp"))
Expect(img.Bounds().Size().X).To(Equal(15))
Expect(img.Bounds().Size().Y).To(Equal(15))
})
It("returns WebP if original image is not a PNG", func() {
conf.Server.CoverArtPriority = "cover.jpg"
r, _, err := aw.Get(context.Background(), alMultipleCovers.CoverArtID(), 200, false)
Expect(err).ToNot(HaveOccurred())
img, format, err := image.Decode(r)
Expect(format).To(Equal("webp"))
Expect(err).ToNot(HaveOccurred())
Expect(img.Bounds().Size().X).To(Equal(200))
Expect(img.Bounds().Size().Y).To(Equal(200))
})
})
When("EnableWebPEncoding is false and square is false", func() {
BeforeEach(func() {
conf.Server.EnableWebPEncoding = false
})
It("returns PNG if original image is a PNG", func() {
conf.Server.CoverArtPriority = "front.png"
r, _, err := aw.Get(context.Background(), alMultipleCovers.CoverArtID(), 15, false)
Expect(err).ToNot(HaveOccurred())
img, format, err := image.Decode(r)
Expect(err).ToNot(HaveOccurred())
Expect(format).To(Equal("png"))
Expect(img.Bounds().Size().X).To(Equal(15)) Expect(img.Bounds().Size().X).To(Equal(15))
Expect(img.Bounds().Size().Y).To(Equal(15)) Expect(img.Bounds().Size().Y).To(Equal(15))
}) })
@ -463,11 +490,11 @@ var _ = Describe("Artwork", func() {
Expect(img.Bounds().Size().Y).To(Equal(200)) Expect(img.Bounds().Size().Y).To(Equal(200))
}) })
}) })
When("DevJpegCoverArt is true and square is true", func() { When("EnableWebPEncoding is false and square is true", func() {
var alCover model.Album var alCover model.Album
BeforeEach(func() { BeforeEach(func() {
conf.Server.DevJpegCoverArt = true conf.Server.EnableWebPEncoding = false
}) })
It("returns PNG for square mode", func() { It("returns PNG for square mode", func() {
dirName := createImage("png", false, 200) dirName := createImage("png", false, 200)

View File

@ -10,7 +10,6 @@ import (
"time" "time"
"github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/consts"
"github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/request" "github.com/navidrome/navidrome/model/request"
@ -24,7 +23,7 @@ type CacheWarmer interface {
// NewCacheWarmer creates a new CacheWarmer instance. The CacheWarmer will pre-cache Artwork images in the background // NewCacheWarmer creates a new CacheWarmer instance. The CacheWarmer will pre-cache Artwork images in the background
// to speed up the response time when the image is requested by the UI. The cache is pre-populated with the original // to speed up the response time when the image is requested by the UI. The cache is pre-populated with the original
// image size, as well as the size defined in the UICoverArtSize constant. // image size, as well as the size defined by the UICoverArtSize config option.
func NewCacheWarmer(artwork Artwork, cache cache.FileCache) CacheWarmer { func NewCacheWarmer(artwork Artwork, cache cache.FileCache) CacheWarmer {
// If image cache is disabled, return a NOOP implementation // If image cache is disabled, return a NOOP implementation
if conf.Server.ImageCacheSize == "0" || !conf.Server.EnableArtworkPrecache { if conf.Server.ImageCacheSize == "0" || !conf.Server.EnableArtworkPrecache {
@ -38,10 +37,11 @@ func NewCacheWarmer(artwork Artwork, cache cache.FileCache) CacheWarmer {
} }
a := &cacheWarmer{ a := &cacheWarmer{
artwork: artwork, artwork: artwork,
cache: cache, cache: cache,
buffer: make(map[model.ArtworkID]struct{}), buffer: make(map[model.ArtworkID]struct{}),
wakeSignal: make(chan struct{}, 1), wakeSignal: make(chan struct{}, 1),
coverArtSize: conf.Server.UICoverArtSize,
} }
// Create a context with a fake admin user, to be able to pre-cache Playlist CoverArts // Create a context with a fake admin user, to be able to pre-cache Playlist CoverArts
@ -51,11 +51,12 @@ func NewCacheWarmer(artwork Artwork, cache cache.FileCache) CacheWarmer {
} }
type cacheWarmer struct { type cacheWarmer struct {
artwork Artwork artwork Artwork
buffer map[model.ArtworkID]struct{} buffer map[model.ArtworkID]struct{}
mutex sync.Mutex mutex sync.Mutex
cache cache.FileCache cache cache.FileCache
wakeSignal chan struct{} wakeSignal chan struct{}
coverArtSize int
} }
func (a *cacheWarmer) PreCache(artID model.ArtworkID) { func (a *cacheWarmer) PreCache(artID model.ArtworkID) {
@ -142,16 +143,14 @@ func (a *cacheWarmer) doCacheImage(ctx context.Context, id model.ArtworkID) erro
ctx, cancel := context.WithTimeout(ctx, 10*time.Second) ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel() defer cancel()
for _, size := range consts.CacheWarmerImageSizes { size := a.coverArtSize
r, _, err := a.artwork.Get(ctx, id, size, true) r, _, err := a.artwork.Get(ctx, id, size, true)
if err != nil { if err != nil {
return fmt.Errorf("caching id='%s', size=%d: %w", id, size, err) return fmt.Errorf("caching id='%s', size=%d: %w", id, size, err)
}
_, err = io.Copy(io.Discard, r)
r.Close()
return err
} }
return nil _, err = io.Copy(io.Discard, r)
r.Close()
return err
} }
func NoopCacheWarmer() CacheWarmer { func NoopCacheWarmer() CacheWarmer {

View File

@ -12,7 +12,6 @@ import (
"github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/conf/configtest" "github.com/navidrome/navidrome/conf/configtest"
"github.com/navidrome/navidrome/consts"
"github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/utils/cache" "github.com/navidrome/navidrome/utils/cache"
. "github.com/onsi/ginkgo/v2" . "github.com/onsi/ginkgo/v2"
@ -182,7 +181,7 @@ var _ = Describe("CacheWarmer", func() {
Eventually(func() []int { Eventually(func() []int {
return aw.getCachedSizes() return aw.getCachedSizes()
}).Should(ContainElements(consts.UICoverArtSize)) }).Should(ContainElements(conf.Server.UICoverArtSize))
}) })
}) })
}) })

View File

@ -61,7 +61,7 @@ func newAlbumArtworkReader(ctx context.Context, artwork *artwork, artID model.Ar
func (a *albumArtworkReader) Key() string { func (a *albumArtworkReader) Key() string {
hashInput := conf.Server.CoverArtPriority hashInput := conf.Server.CoverArtPriority
if conf.Server.EnableExternalServices { if conf.Server.EnableExternalServices {
hashInput += conf.Server.Agents hashInput = conf.Server.Agents + hashInput
} }
hash := md5.Sum([]byte(hashInput)) hash := md5.Sum([]byte(hashInput))
return fmt.Sprintf( return fmt.Sprintf(

View File

@ -19,6 +19,16 @@ import (
xdraw "golang.org/x/image/draw" xdraw "golang.org/x/image/draw"
) )
func init() {
conf.AddHook(func() {
if err := webp.Dynamic(); err != nil {
log.Debug("Using WASM WebP encoder/decoder", "reason", err)
} else {
log.Debug("Using native libwebp for WebP encoding/decoding")
}
})
}
var bufPool = sync.Pool{ var bufPool = sync.Pool{
New: func() any { New: func() any {
return new(bytes.Buffer) return new(bytes.Buffer)
@ -98,28 +108,26 @@ func (a *resizedArtworkReader) resizeImage(ctx context.Context, reader io.Reader
return nil, 0, fmt.Errorf("reading image data: %w", err) return nil, 0, fmt.Errorf("reading image data: %w", err)
} }
// Preserve animation for animated images (skip for square thumbnails) // Preserve animation for animated images
if !a.square { if isAnimatedGIF(data) {
if isAnimatedGIF(data) { if a.a.ffmpeg.IsAvailable() {
if a.a.ffmpeg.IsAvailable() { // Animated GIF: convert to animated WebP via ffmpeg (with optional resize)
// Animated GIF: convert to animated WebP via ffmpeg (with optional resize) r, err := a.a.ffmpeg.ConvertAnimatedImage(ctx, bytes.NewReader(data), a.size, conf.Server.CoverArtQuality)
r, err := a.a.ffmpeg.ConvertAnimatedImage(ctx, bytes.NewReader(data), a.size, conf.Server.CoverArtQuality) if err == nil {
if err == nil { return r, 0, nil
return r, 0, nil
}
log.Warn(ctx, "Could not convert animated GIF, falling back to static", err)
} }
} else if isAnimatedWebP(data) || isAnimatedPNG(data) { log.Warn(ctx, "Could not convert animated GIF, falling back to static", err)
// Animated WebP/APNG: return original as-is (ffmpeg can't re-encode these)
return bytes.NewReader(data), 0, nil
} }
} 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, a.size, a.square) return resizeStaticImage(data, a.size, a.square)
} }
func resizeStaticImage(data []byte, size int, square bool) (io.Reader, int, error) { func resizeStaticImage(data []byte, size int, square bool) (io.Reader, int, error) {
original, _, err := image.Decode(bytes.NewReader(data)) original, format, err := image.Decode(bytes.NewReader(data))
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
@ -159,14 +167,12 @@ func resizeStaticImage(data []byte, size int, square bool) (io.Reader, int, erro
buf := bufPool.Get().(*bytes.Buffer) buf := bufPool.Get().(*bytes.Buffer)
buf.Reset() buf.Reset()
if conf.Server.DevJpegCoverArt { if conf.Server.EnableWebPEncoding {
if square {
err = png.Encode(buf, dst)
} else {
err = jpeg.Encode(buf, dst, &jpeg.Options{Quality: conf.Server.CoverArtQuality})
}
} else {
err = webp.Encode(buf, dst, webp.Options{Quality: conf.Server.CoverArtQuality}) 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 { if err != nil {
bufPool.Put(buf) bufPool.Put(buf)

View File

@ -54,17 +54,17 @@ var _ = Describe("resizeImage", func() {
Expect(len(output)).To(BeNumerically(">", 0)) Expect(len(output)).To(BeNumerically(">", 0))
}) })
It("skips animation for square thumbnails even with animated GIF", func() { It("preserves animation for square thumbnails with animated GIF", func() {
r.square = true r.square = true
data := createAnimatedGIF(3) data := createAnimatedGIF(3)
result, _, err := r.resizeImage(context.Background(), bytes.NewReader(data)) result, _, err := r.resizeImage(context.Background(), bytes.NewReader(data))
// Should fall through to static resize (not ffmpeg conversion) Expect(err).ToNot(HaveOccurred())
// The minimal test GIF may or may not resize successfully, Expect(result).ToNot(BeNil())
// but ffmpeg should NOT have been called for animated conversion
_ = result // Should have been processed by ffmpeg (mock returns input data)
_ = err output, err := io.ReadAll(result)
// Verify by checking the mock wasn't used for animated conversion: Expect(err).ToNot(HaveOccurred())
// If ffmpeg was called, it would return mock data, not static resize result Expect(output).To(Equal(data))
}) })
}) })
@ -81,13 +81,17 @@ var _ = Describe("resizeImage", func() {
Expect(output).To(Equal(data)) Expect(output).To(Equal(data))
}) })
It("does not passthrough animated WebP for square thumbnails", func() { It("preserves animated WebP for square thumbnails", func() {
r.square = true r.square = true
data := createAnimatedWebPBytes() data := createAnimatedWebPBytes()
// Should fall through to static resize, which will fail on fake WebP data result, _, err := r.resizeImage(context.Background(), bytes.NewReader(data))
_, _, err := r.resizeImage(context.Background(), bytes.NewReader(data)) Expect(err).ToNot(HaveOccurred())
// Static decode will fail on our minimal test WebP bytes (not a real image) Expect(result).ToNot(BeNil())
Expect(err).To(HaveOccurred())
// Should return original data unchanged
output, err := io.ReadAll(result)
Expect(err).ToNot(HaveOccurred())
Expect(output).To(Equal(data))
}) })
}) })
@ -104,15 +108,17 @@ var _ = Describe("resizeImage", func() {
Expect(output).To(Equal(data)) Expect(output).To(Equal(data))
}) })
It("does not passthrough animated PNG for square thumbnails", func() { It("preserves animated PNG for square thumbnails", func() {
r.square = true r.square = true
data := createAPNGBytes() data := createAPNGBytes()
// Should fall through to static resize
result, _, err := r.resizeImage(context.Background(), bytes.NewReader(data)) result, _, err := r.resizeImage(context.Background(), bytes.NewReader(data))
// Static PNG decode should succeed on our APNG (it's a valid PNG) Expect(err).ToNot(HaveOccurred())
if err == nil { Expect(result).ToNot(BeNil())
Expect(result).ToNot(BeNil())
} // Should return original data unchanged
output, err := io.ReadAll(result)
Expect(err).ToNot(HaveOccurred())
Expect(output).To(Equal(data))
}) })
}) })

View File

@ -195,6 +195,8 @@ var staticData = sync.OnceValue(func() insights.Data {
data.Config.EnableArtworkPrecache = conf.Server.EnableArtworkPrecache data.Config.EnableArtworkPrecache = conf.Server.EnableArtworkPrecache
data.Config.EnableArtworkUpload = conf.Server.EnableArtworkUpload data.Config.EnableArtworkUpload = conf.Server.EnableArtworkUpload
data.Config.CoverArtQuality = conf.Server.CoverArtQuality data.Config.CoverArtQuality = conf.Server.CoverArtQuality
data.Config.EnableWebPEncoding = conf.Server.EnableWebPEncoding
data.Config.UICoverArtSize = conf.Server.UICoverArtSize
data.Config.EnableCoverAnimation = conf.Server.EnableCoverAnimation data.Config.EnableCoverAnimation = conf.Server.EnableCoverAnimation
data.Config.EnableNowPlaying = conf.Server.EnableNowPlaying data.Config.EnableNowPlaying = conf.Server.EnableNowPlaying
data.Config.EnableDownloads = conf.Server.EnableDownloads data.Config.EnableDownloads = conf.Server.EnableDownloads

View File

@ -65,6 +65,8 @@ type Data struct {
EnablePrometheus bool `json:"enablePrometheus,omitempty"` EnablePrometheus bool `json:"enablePrometheus,omitempty"`
EnableArtworkUpload bool `json:"enableArtworkUpload,omitempty"` EnableArtworkUpload bool `json:"enableArtworkUpload,omitempty"`
CoverArtQuality int `json:"coverArtQuality,omitempty"` CoverArtQuality int `json:"coverArtQuality,omitempty"`
EnableWebPEncoding bool `json:"enableWebPEncoding,omitempty"`
UICoverArtSize int `json:"uiCoverArtSize,omitempty"`
EnableCoverAnimation bool `json:"enableCoverAnimation,omitempty"` EnableCoverAnimation bool `json:"enableCoverAnimation,omitempty"`
EnableNowPlaying bool `json:"enableNowPlaying,omitempty"` EnableNowPlaying bool `json:"enableNowPlaying,omitempty"`
SessionTimeout uint64 `json:"sessionTimeout,omitempty"` SessionTimeout uint64 `json:"sessionTimeout,omitempty"`

6
go.mod
View File

@ -36,12 +36,12 @@ require (
github.com/kardianos/service v1.2.4 github.com/kardianos/service v1.2.4
github.com/kr/pretty v0.3.1 github.com/kr/pretty v0.3.1
github.com/lestrrat-go/jwx/v3 v3.0.13 github.com/lestrrat-go/jwx/v3 v3.0.13
github.com/mattn/go-sqlite3 v1.14.37 github.com/mattn/go-sqlite3 v1.14.38
github.com/microcosm-cc/bluemonday v1.0.27 github.com/microcosm-cc/bluemonday v1.0.27
github.com/mileusna/useragent v1.3.5 github.com/mileusna/useragent v1.3.5
github.com/onsi/ginkgo/v2 v2.28.1 github.com/onsi/ginkgo/v2 v2.28.1
github.com/onsi/gomega v1.39.1 github.com/onsi/gomega v1.39.1
github.com/pelletier/go-toml/v2 v2.2.4 github.com/pelletier/go-toml/v2 v2.3.0
github.com/pocketbase/dbx v1.12.0 github.com/pocketbase/dbx v1.12.0
github.com/pressly/goose/v3 v3.27.0 github.com/pressly/goose/v3 v3.27.0
github.com/prometheus/client_golang v1.23.2 github.com/prometheus/client_golang v1.23.2
@ -104,7 +104,7 @@ require (
github.com/lestrrat-go/dsig v1.0.0 // indirect github.com/lestrrat-go/dsig v1.0.0 // indirect
github.com/lestrrat-go/dsig-secp256k1 v1.0.0 // indirect github.com/lestrrat-go/dsig-secp256k1 v1.0.0 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/httprc/v3 v3.0.4 // indirect github.com/lestrrat-go/httprc/v3 v3.0.5 // indirect
github.com/lestrrat-go/option/v2 v2.0.0 // indirect github.com/lestrrat-go/option/v2 v2.0.0 // indirect
github.com/maruel/natural v1.3.0 // indirect github.com/maruel/natural v1.3.0 // indirect
github.com/mfridman/interpolate v0.0.2 // indirect github.com/mfridman/interpolate v0.0.2 // indirect

12
go.sum
View File

@ -167,8 +167,8 @@ github.com/lestrrat-go/dsig-secp256k1 v1.0.0 h1:JpDe4Aybfl0soBvoVwjqDbp+9S1Y2OM7
github.com/lestrrat-go/dsig-secp256k1 v1.0.0/go.mod h1:CxUgAhssb8FToqbL8NjSPoGQlnO4w3LG1P0qPWQm/NU= github.com/lestrrat-go/dsig-secp256k1 v1.0.0/go.mod h1:CxUgAhssb8FToqbL8NjSPoGQlnO4w3LG1P0qPWQm/NU=
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
github.com/lestrrat-go/httprc/v3 v3.0.4 h1:pXyH2ppK8GYYggygxJ3TvxpCZnbEUWc9qSwRTTApaLA= github.com/lestrrat-go/httprc/v3 v3.0.5 h1:S+Mb4L2I+bM6JGTibLmxExhyTOqnXjqx+zi9MoXw/TM=
github.com/lestrrat-go/httprc/v3 v3.0.4/go.mod h1:mSMtkZW92Z98M5YoNNztbRGxbXHql7tSitCvaxvo9l0= github.com/lestrrat-go/httprc/v3 v3.0.5/go.mod h1:mSMtkZW92Z98M5YoNNztbRGxbXHql7tSitCvaxvo9l0=
github.com/lestrrat-go/jwx/v3 v3.0.13 h1:AdHKiPIYeCSnOJtvdpipPg/0SuFh9rdkN+HF3O0VdSk= github.com/lestrrat-go/jwx/v3 v3.0.13 h1:AdHKiPIYeCSnOJtvdpipPg/0SuFh9rdkN+HF3O0VdSk=
github.com/lestrrat-go/jwx/v3 v3.0.13/go.mod h1:2m0PV1A9tM4b/jVLMx8rh6rBl7F6WGb3EG2hufN9OQU= github.com/lestrrat-go/jwx/v3 v3.0.13/go.mod h1:2m0PV1A9tM4b/jVLMx8rh6rBl7F6WGb3EG2hufN9OQU=
github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLOcID3Ss= github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLOcID3Ss=
@ -177,8 +177,8 @@ github.com/maruel/natural v1.3.0 h1:VsmCsBmEyrR46RomtgHs5hbKADGRVtliHTyCOLFBpsg=
github.com/maruel/natural v1.3.0/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= github.com/maruel/natural v1.3.0/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.37 h1:3DOZp4cXis1cUIpCfXLtmlGolNLp2VEqhiB/PARNBIg= github.com/mattn/go-sqlite3 v1.14.38 h1:tDUzL85kMvOrvpCt8P64SbGgVFtJB11GPi2AdmITgb4=
github.com/mattn/go-sqlite3 v1.14.37/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.38/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY= github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=
github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg= github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg=
github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE= github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE=
@ -199,8 +199,8 @@ github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI
github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE= github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE=
github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=
github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg= github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=

View File

@ -35,6 +35,7 @@ var fieldMap = map[string]*mappedField{
"releasedate": {field: "media_file.release_date"}, "releasedate": {field: "media_file.release_date"},
"size": {field: "media_file.size"}, "size": {field: "media_file.size"},
"compilation": {field: "media_file.compilation"}, "compilation": {field: "media_file.compilation"},
"missing": {field: "media_file.missing"},
"explicitstatus": {field: "media_file.explicit_status"}, "explicitstatus": {field: "media_file.explicit_status"},
"dateadded": {field: "media_file.created_at"}, "dateadded": {field: "media_file.created_at"},
"datemodified": {field: "media_file.updated_at"}, "datemodified": {field: "media_file.updated_at"},
@ -49,9 +50,11 @@ var fieldMap = map[string]*mappedField{
"catalognumber": {field: "media_file.catalog_num"}, "catalognumber": {field: "media_file.catalog_num"},
"filepath": {field: "media_file.path"}, "filepath": {field: "media_file.path"},
"filetype": {field: "media_file.suffix"}, "filetype": {field: "media_file.suffix"},
"codec": {field: "media_file.codec"},
"duration": {field: "media_file.duration"}, "duration": {field: "media_file.duration"},
"bitrate": {field: "media_file.bit_rate"}, "bitrate": {field: "media_file.bit_rate"},
"bitdepth": {field: "media_file.bit_depth"}, "bitdepth": {field: "media_file.bit_depth"},
"samplerate": {field: "media_file.sample_rate"},
"bpm": {field: "media_file.bpm"}, "bpm": {field: "media_file.bpm"},
"channels": {field: "media_file.channels"}, "channels": {field: "media_file.channels"},
"loved": {field: "COALESCE(annotation.starred, false)"}, "loved": {field: "COALESCE(annotation.starred, false)"},

View File

@ -36,7 +36,9 @@
"bitDepth": "Bitprofundo", "bitDepth": "Bitprofundo",
"sampleRate": "Elprena rapido", "sampleRate": "Elprena rapido",
"missing": "Mankaj", "missing": "Mankaj",
"libraryName": "Biblioteko" "libraryName": "Biblioteko",
"composer": "",
"disc": ""
}, },
"actions": { "actions": {
"addToQueue": "Ludi Poste", "addToQueue": "Ludi Poste",
@ -46,7 +48,8 @@
"download": "Elŝuti", "download": "Elŝuti",
"playNext": "Ludu Poste", "playNext": "Ludu Poste",
"info": "Akiri Informon", "info": "Akiri Informon",
"showInPlaylist": "Montri en Ludlisto" "showInPlaylist": "Montri en Ludlisto",
"instantMix": ""
} }
}, },
"album": { "album": {
@ -328,6 +331,82 @@
"scanInProgress": "Skano progresas...", "scanInProgress": "Skano progresas...",
"noLibrariesAssigned": "Neniuj bibliotekoj asignitaj por ĉi tiu uzanto" "noLibrariesAssigned": "Neniuj bibliotekoj asignitaj por ĉi tiu uzanto"
} }
},
"plugin": {
"name": "",
"fields": {
"id": "",
"name": "",
"description": "",
"version": "Versio",
"author": "Aŭtoro",
"website": "Retejo",
"permissions": "Permesoj",
"enabled": "Ebligite",
"status": "",
"path": "Vojo",
"lastError": "Eraro",
"hasError": "Eraro",
"updatedAt": "Ĝisdatigite",
"createdAt": "",
"configKey": "Ŝlosilo",
"configValue": "",
"allUsers": "",
"selectedUsers": "",
"allLibraries": "",
"selectedLibraries": "",
"allowWriteAccess": ""
},
"sections": {
"status": "",
"info": "",
"configuration": "",
"manifest": "",
"usersPermission": "",
"libraryPermission": ""
},
"status": {
"enabled": "",
"disabled": ""
},
"actions": {
"enable": "",
"disable": "",
"disabledDueToError": "",
"disabledUsersRequired": "",
"disabledLibrariesRequired": "",
"addConfig": "",
"rescan": ""
},
"notifications": {
"enabled": "",
"disabled": "",
"updated": "",
"error": ""
},
"validation": {
"invalidJson": ""
},
"messages": {
"configHelp": "",
"clickPermissions": "",
"noConfig": "",
"allUsersHelp": "",
"noUsers": "",
"permissionReason": "",
"usersRequired": "",
"allLibrariesHelp": "",
"noLibraries": "",
"librariesRequired": "",
"requiredHosts": "",
"configValidationError": "",
"schemaRenderError": "",
"allowWriteAccessHelp": ""
},
"placeholders": {
"configKey": "",
"configValue": ""
}
} }
}, },
"ra": { "ra": {
@ -511,7 +590,14 @@
"remove_all_missing_title": "Forigi ĉiujn mankajn dosierojn", "remove_all_missing_title": "Forigi ĉiujn mankajn dosierojn",
"remove_all_missing_content": "Ĉu vi certas, ke vi volas forigi ĉiujn mankajn dosierojn de la datumbazo? Ĉi tio permanante forigos ĉiujn referencojn al ili, inkluzive iliajn ludnombrojn kaj taksojn.", "remove_all_missing_content": "Ĉu vi certas, ke vi volas forigi ĉiujn mankajn dosierojn de la datumbazo? Ĉi tio permanante forigos ĉiujn referencojn al ili, inkluzive iliajn ludnombrojn kaj taksojn.",
"noSimilarSongsFound": "Neniuj similaj kantoj trovitaj", "noSimilarSongsFound": "Neniuj similaj kantoj trovitaj",
"noTopSongsFound": "Neniuj plej luditaj kantoj trovitaj" "noTopSongsFound": "Neniuj plej luditaj kantoj trovitaj",
"startingInstantMix": "",
"uploadCover": "",
"removeCover": "",
"coverUploaded": "",
"coverRemoved": "",
"coverUploadError": "",
"coverRemoveError": ""
}, },
"menu": { "menu": {
"library": "Biblioteko", "library": "Biblioteko",
@ -597,7 +683,8 @@
"exportSuccess": "Agordoj eksportiĝis al la tondujo en TOML-a formato", "exportSuccess": "Agordoj eksportiĝis al la tondujo en TOML-a formato",
"exportFailed": "Malsukcesis kopii agordojn", "exportFailed": "Malsukcesis kopii agordojn",
"devFlagsHeader": "Programadaj Flagoj (povas ŝanĝiĝi/foriĝi)", "devFlagsHeader": "Programadaj Flagoj (povas ŝanĝiĝi/foriĝi)",
"devFlagsComment": "Ĉi tiuj estas eksperimentaj agordoj kaj eble foriĝos en estontaj versioj" "devFlagsComment": "Ĉi tiuj estas eksperimentaj agordoj kaj eble foriĝos en estontaj versioj",
"downloadToml": ""
} }
}, },
"activity": { "activity": {

View File

@ -23,6 +23,7 @@
"bitDepth": "Bit-sakonera", "bitDepth": "Bit-sakonera",
"sampleRate": "Lagin-tasa", "sampleRate": "Lagin-tasa",
"channels": "Kanalak", "channels": "Kanalak",
"disc": "%{discNumber}. diskoa",
"discSubtitle": "Diskoaren azpititulua", "discSubtitle": "Diskoaren azpititulua",
"starred": "Gogokoa", "starred": "Gogokoa",
"comment": "Iruzkina", "comment": "Iruzkina",
@ -355,7 +356,8 @@
"allUsers": "Baimendu erabiltzaile guztiak", "allUsers": "Baimendu erabiltzaile guztiak",
"selectedUsers": "Hautatutako erabiltzaileak", "selectedUsers": "Hautatutako erabiltzaileak",
"allLibraries": "Baimendu liburutegi guztiak", "allLibraries": "Baimendu liburutegi guztiak",
"selectedLibraries": "Hautatutako liburutegiak" "selectedLibraries": "Hautatutako liburutegiak",
"allowWriteAccess": "Eman idazteko baimena"
}, },
"sections": { "sections": {
"status": "Egoera", "status": "Egoera",
@ -400,6 +402,7 @@
"allLibrariesHelp": "Gaituta dagoenean, pluginak liburutegi guztietara izango du sarbidea, baita etorkizunean sortuko direnetara ere.", "allLibrariesHelp": "Gaituta dagoenean, pluginak liburutegi guztietara izango du sarbidea, baita etorkizunean sortuko direnetara ere.",
"noLibraries": "Ez da liburutegirik hautatu", "noLibraries": "Ez da liburutegirik hautatu",
"librariesRequired": "Plugin honek liburutegien informaziora sarbidea behar du. Hautatu zein liburutegi atzitu dezakeen pluginak, edo gaitu 'Baimendu liburutegi guztiak'.", "librariesRequired": "Plugin honek liburutegien informaziora sarbidea behar du. Hautatu zein liburutegi atzitu dezakeen pluginak, edo gaitu 'Baimendu liburutegi guztiak'.",
"allowWriteAccessHelp": "Gaituta dagoenean, pluginak liburutegien direktorioko fitxategiak moldatu ditzake. Defektuz, pluginek bakarrik irakurtzeko baimena dute.",
"requiredHosts": "Beharrezko ostatatzaileak" "requiredHosts": "Beharrezko ostatatzaileak"
}, },
"placeholders": { "placeholders": {
@ -554,6 +557,12 @@
} }
}, },
"message": { "message": {
"uploadCover": "Igo azala",
"removeCover": "Kendu azala",
"coverUploaded": "Diskoaren azala eguneratu da",
"coverRemoved": "Diskoaren azala kendu da",
"coverUploadError": "Errorea diskoaren azala igotzean",
"coverRemoveError": "Errorea diskoaren azala kentzean",
"note": "OHARRA", "note": "OHARRA",
"transcodingDisabled": "Segurtasun arrazoiak direla-eta, transkodeketaren ezarpenak web-interfazearen bidez aldatzea ezgaituta dago. Transkodeketa-aukerak aldatu (editatu edo gehitu) nahi badituzu, berrabiarazi zerbitzaria konfigurazio-aukeraren %{config}-arekin.", "transcodingDisabled": "Segurtasun arrazoiak direla-eta, transkodeketaren ezarpenak web-interfazearen bidez aldatzea ezgaituta dago. Transkodeketa-aukerak aldatu (editatu edo gehitu) nahi badituzu, berrabiarazi zerbitzaria konfigurazio-aukeraren %{config}-arekin.",
"transcodingEnabled": "Navidrome %{config}-ekin martxan dago eta, beraz, web-interfazeko transkodeketa-ataletik sistema-komandoak exekuta daitezke. Segurtasun arrazoiak tarteko, ezgaitzea gomendatzen dugu, eta transkodeketa-aukerak konfiguratzen ari zarenean bakarrik gaitzea.", "transcodingEnabled": "Navidrome %{config}-ekin martxan dago eta, beraz, web-interfazeko transkodeketa-ataletik sistema-komandoak exekuta daitezke. Segurtasun arrazoiak tarteko, ezgaitzea gomendatzen dugu, eta transkodeketa-aukerak konfiguratzen ari zarenean bakarrik gaitzea.",
@ -673,6 +682,7 @@
"currentValue": "Uneko balioa", "currentValue": "Uneko balioa",
"configurationFile": "Konfigurazio-fitxategia", "configurationFile": "Konfigurazio-fitxategia",
"exportToml": "Esportatu konfigurazioa (TOML)", "exportToml": "Esportatu konfigurazioa (TOML)",
"downloadToml": "Deskargatu konfigurazioa (TOML)",
"exportSuccess": "Konfigurazioa arbelera esportatu da TOML formatuan", "exportSuccess": "Konfigurazioa arbelera esportatu da TOML formatuan",
"exportFailed": "Konfigurazioa kopiatzeak huts egin du", "exportFailed": "Konfigurazioa kopiatzeak huts egin du",
"devFlagsHeader": "Garapen-adierazleak (aldatu/kendu litezke)", "devFlagsHeader": "Garapen-adierazleak (aldatu/kendu litezke)",

View File

@ -37,7 +37,8 @@
"sampleRate": "Sample waarde", "sampleRate": "Sample waarde",
"missing": "Ontbrekend", "missing": "Ontbrekend",
"libraryName": "Bibliotheek", "libraryName": "Bibliotheek",
"composer": "" "composer": "Componist",
"disc": "Schijf %{discNumber}"
}, },
"actions": { "actions": {
"addToQueue": "Voeg toe aan wachtrij", "addToQueue": "Voeg toe aan wachtrij",
@ -48,7 +49,7 @@
"playNext": "Volgende", "playNext": "Volgende",
"info": "Meer info", "info": "Meer info",
"showInPlaylist": "Toon in afspeellijst", "showInPlaylist": "Toon in afspeellijst",
"instantMix": "" "instantMix": "Instant mix"
} }
}, },
"album": { "album": {
@ -350,10 +351,11 @@
"createdAt": "Geinstalleerd", "createdAt": "Geinstalleerd",
"configKey": "Sleutel", "configKey": "Sleutel",
"configValue": "Waarde", "configValue": "Waarde",
"allUsers": "Alle gebruikers toelaten", "allUsers": "Sta toe voor alle gebruikers",
"selectedUsers": "Geselecteerde gebruikers", "selectedUsers": "Geselecteerde gebruikers",
"allLibraries": "Alle bibliotheken toestaan", "allLibraries": "Sta toe voor alle bibliotheken",
"selectedLibraries": "Geselecteerde bibliotheken" "selectedLibraries": "Geselecteerde bibliotheken",
"allowWriteAccess": "Sta schrijftoegang toe"
}, },
"sections": { "sections": {
"status": "Status", "status": "Status",
@ -379,26 +381,27 @@
"notifications": { "notifications": {
"enabled": "Plugin actief", "enabled": "Plugin actief",
"disabled": "Plugin niet actief", "disabled": "Plugin niet actief",
"updated": "Plugin geupdate", "updated": "Plugin bijgewerkt",
"error": "Fout bij updaten plugin" "error": "Fout bij updaten plugin"
}, },
"validation": { "validation": {
"invalidJson": "Configuratie moet geldige JSON zijn" "invalidJson": "Configuratie moet geldige JSON zijn"
}, },
"messages": { "messages": {
"configHelp": "", "configHelp": "Configureer de plug-in met key-value paren. Leeglaten als de plug-in niet geconfigueerd hoeft te worden.",
"clickPermissions": "Klik op permissie voor details", "clickPermissions": "Klik op permissie voor details",
"noConfig": "Geen configuratie ingesteld", "noConfig": "Geen configuratie ingesteld",
"allUsersHelp": "", "allUsersHelp": "Als dit aanstaat heeft de plug-in toegang tot alle gebruikers, inclusief toekomstige.",
"noUsers": "Geen gebruikers geselecteerd", "noUsers": "Geen gebruikers geselecteerd",
"permissionReason": "Reden", "permissionReason": "Reden",
"usersRequired": "", "usersRequired": "Deze plug-in heeft toegang nodig tot gebruikersinformatie. Selecteer welke gebruikers de plug-in toegang toe heeft, of schakel 'sta toe voor alle gebruikers' in.",
"allLibrariesHelp": "", "allLibrariesHelp": "Als dit aanstaat, heeft de plug-in toegang tot alle bibliotheken, inclusief toekomstige.",
"noLibraries": "Geen bibliotheken geselecteerd", "noLibraries": "Geen bibliotheken geselecteerd",
"librariesRequired": "", "librariesRequired": "Deze plug-in heeft toegang nodig tot bibliotheek informatie. Selecteer welke bibliotheken de plug-in toegang to heeft, of schakel 'sta toe voor alle bibliotheken' in.",
"requiredHosts": "Benodigde hosts", "requiredHosts": "Benodigde hosts",
"configValidationError": "", "configValidationError": "Configuratiecheck mislukt",
"schemaRenderError": "" "schemaRenderError": "Kan het configuratieformulier niet verwerken. Het plugin schema is wellicht ongeldig.",
"allowWriteAccessHelp": "Met dit ingeschakeld, kan de plug-in bestanden bewerken in de bibliotheekmappen. Standaard kunnen plug-ins alleen lezen."
}, },
"placeholders": { "placeholders": {
"configKey": "Sleutel", "configKey": "Sleutel",
@ -588,7 +591,13 @@
"remove_all_missing_content": "Weet je zeker dat je alle ontbrekende bestanden van de database wil verwijderen? Dit wist permanent al hun referenties inclusief afspeel tellers en beoordelingen.", "remove_all_missing_content": "Weet je zeker dat je alle ontbrekende bestanden van de database wil verwijderen? Dit wist permanent al hun referenties inclusief afspeel tellers en beoordelingen.",
"noSimilarSongsFound": "Geen vergelijkbare nummers gevonden", "noSimilarSongsFound": "Geen vergelijkbare nummers gevonden",
"noTopSongsFound": "Geen beste nummers gevonden", "noTopSongsFound": "Geen beste nummers gevonden",
"startingInstantMix": "" "startingInstantMix": "Laden van Instant mix...",
"uploadCover": "Albumhoes toevoegen",
"removeCover": "Verwijder albumhoes",
"coverUploaded": "Albumhoes bijgewerkt",
"coverRemoved": "Albumhoes verwijderd",
"coverUploadError": "Fout bij het toevoegen albumhoes",
"coverRemoveError": "Fout bij verwijderen albumhoes"
}, },
"menu": { "menu": {
"library": "Bibliotheek", "library": "Bibliotheek",
@ -674,7 +683,8 @@
"exportSuccess": "Configuratie geëxporteerd naar klembord in TOML formaat", "exportSuccess": "Configuratie geëxporteerd naar klembord in TOML formaat",
"exportFailed": "Kopiëren van configuratie mislukt", "exportFailed": "Kopiëren van configuratie mislukt",
"devFlagsHeader": "Ontwikkelaarsinstellingen (onder voorbehoud)", "devFlagsHeader": "Ontwikkelaarsinstellingen (onder voorbehoud)",
"devFlagsComment": "Dit zijn experimentele instellingen en worden mogelijk in latere versies verwijderd" "devFlagsComment": "Dit zijn experimentele instellingen en worden mogelijk in latere versies verwijderd",
"downloadToml": "Download configuratie (TOML)"
} }
}, },
"activity": { "activity": {

View File

@ -6,6 +6,7 @@ import (
"net/http" "net/http"
"path" "path"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/consts" "github.com/navidrome/navidrome/consts"
"github.com/navidrome/navidrome/core/auth" "github.com/navidrome/navidrome/core/auth"
"github.com/navidrome/navidrome/core/publicurl" "github.com/navidrome/navidrome/core/publicurl"
@ -81,7 +82,7 @@ func checkShareError(ctx context.Context, w http.ResponseWriter, err error, id s
func (pub *Router) mapShareInfo(r *http.Request, s model.Share) *model.Share { func (pub *Router) mapShareInfo(r *http.Request, s model.Share) *model.Share {
s.URL = ShareURL(r, s.ID) s.URL = ShareURL(r, s.ID)
s.ImageURL = publicurl.ImageURL(r, s.CoverArtID(), consts.UICoverArtSize) s.ImageURL = publicurl.ImageURL(r, s.CoverArtID(), conf.Server.UICoverArtSize)
for i := range s.Tracks { for i := range s.Tracks {
s.Tracks[i].ID = encodeMediafileShare(s, s.Tracks[i].ID) s.Tracks[i].ID = encodeMediafileShare(s, s.Tracks[i].ID)
} }

View File

@ -55,6 +55,7 @@ func serveIndex(ds model.DataStore, fs fs.FS, shareInfo *model.Share) http.Handl
"defaultLanguage": conf.Server.DefaultLanguage, "defaultLanguage": conf.Server.DefaultLanguage,
"defaultUIVolume": conf.Server.DefaultUIVolume, "defaultUIVolume": conf.Server.DefaultUIVolume,
"uiSearchDebounceMs": conf.Server.UISearchDebounceMs, "uiSearchDebounceMs": conf.Server.UISearchDebounceMs,
"uiCoverArtSize": conf.Server.UICoverArtSize,
"enableCoverAnimation": conf.Server.EnableCoverAnimation, "enableCoverAnimation": conf.Server.EnableCoverAnimation,
"enableNowPlaying": conf.Server.EnableNowPlaying, "enableNowPlaying": conf.Server.EnableNowPlaying,
"gaTrackingId": conf.Server.GATrackingID, "gaTrackingId": conf.Server.GATrackingID,

View File

@ -86,6 +86,7 @@ var _ = Describe("serveIndex", func() {
Entry("defaultLanguage", func() { conf.Server.DefaultLanguage = "pt" }, "defaultLanguage", "pt"), Entry("defaultLanguage", func() { conf.Server.DefaultLanguage = "pt" }, "defaultLanguage", "pt"),
Entry("defaultUIVolume", func() { conf.Server.DefaultUIVolume = 45 }, "defaultUIVolume", float64(45)), Entry("defaultUIVolume", func() { conf.Server.DefaultUIVolume = 45 }, "defaultUIVolume", float64(45)),
Entry("uiSearchDebounceMs", func() { conf.Server.UISearchDebounceMs = 500 }, "uiSearchDebounceMs", float64(500)), Entry("uiSearchDebounceMs", func() { conf.Server.UISearchDebounceMs = 500 }, "uiSearchDebounceMs", float64(500)),
Entry("uiCoverArtSize", func() { conf.Server.UICoverArtSize = 300 }, "uiCoverArtSize", float64(300)),
Entry("enableCoverAnimation", func() { conf.Server.EnableCoverAnimation = true }, "enableCoverAnimation", true), Entry("enableCoverAnimation", func() { conf.Server.EnableCoverAnimation = true }, "enableCoverAnimation", true),
Entry("enableNowPlaying", func() { conf.Server.EnableNowPlaying = true }, "enableNowPlaying", true), Entry("enableNowPlaying", func() { conf.Server.EnableNowPlaying = true }, "enableNowPlaying", true),
Entry("gaTrackingId", func() { conf.Server.GATrackingID = "UA-12345" }, "gaTrackingId", "UA-12345"), Entry("gaTrackingId", func() { conf.Server.GATrackingID = "UA-12345" }, "gaTrackingId", "UA-12345"),

View File

@ -159,6 +159,10 @@ func (api *Router) buildPlaylist(ctx context.Context, p model.Playlist) response
} }
func buildOSPlaylist(ctx context.Context, p model.Playlist) *responses.OpenSubsonicPlaylist { func buildOSPlaylist(ctx context.Context, p model.Playlist) *responses.OpenSubsonicPlaylist {
player, ok := request.PlayerFrom(ctx)
if ok && isClientInList(conf.Server.Subsonic.LegacyClients, player.Client) {
return nil
}
pls := responses.OpenSubsonicPlaylist{} pls := responses.OpenSubsonicPlaylist{}
if p.IsSmartPlaylist() { if p.IsSmartPlaylist() {

View File

@ -128,6 +128,23 @@ var _ = Describe("buildPlaylist", func() {
}) })
}) })
Context("with legacy client", func() {
BeforeEach(func() {
conf.Server.Subsonic.LegacyClients = "legacy-client"
player := model.Player{Client: "legacy-client"}
ctx = request.WithPlayer(ctx, player)
})
It("returns all standard fields but no OpenSubsonic extensions", func() {
result := router.buildPlaylist(ctx, playlist)
Expect(result.Comment).To(Equal("Test comment"))
Expect(result.Owner).To(Equal("admin"))
Expect(result.Public).To(BeTrue())
Expect(result.OpenSubsonicPlaylist).To(BeNil())
})
})
Context("when no player in context", func() { Context("when no player in context", func() {
It("returns all fields", func() { It("returns all fields", func() {
result := router.buildPlaylist(ctx, playlist) result := router.buildPlaylist(ctx, playlist)
@ -213,6 +230,23 @@ var _ = Describe("buildPlaylist", func() {
Expect(result.ValidUntil).To(Equal(&validUntil)) Expect(result.ValidUntil).To(Equal(&validUntil))
}) })
}) })
Context("with legacy client", func() {
BeforeEach(func() {
conf.Server.Subsonic.LegacyClients = "legacy-client"
player := model.Player{Client: "legacy-client"}
ctx = request.WithPlayer(ctx, player)
})
It("returns standard fields but no OpenSubsonic extensions", func() {
result := router.buildPlaylist(ctx, playlist)
Expect(result.Comment).To(Equal("Test comment"))
Expect(result.Owner).To(Equal("admin"))
Expect(result.Public).To(BeTrue())
Expect(result.OpenSubsonicPlaylist).To(BeNil())
})
})
}) })
}) })

View File

@ -75,8 +75,12 @@ func (api *Router) GetInternetRadios(r *http.Request) (*responses.Subsonic, erro
continue continue
} }
// Add coverArt if not legacy client // Add coverArt if not legacy client
var coverArt string
if g.UploadedImage != "" {
coverArt = g.CoverArtID().String()
}
res[i].OpenSubsonicRadio = &responses.OpenSubsonicRadio{ res[i].OpenSubsonicRadio = &responses.OpenSubsonicRadio{
CoverArt: g.UploadedImage, CoverArt: coverArt,
} }
} }

View File

@ -71,7 +71,7 @@ var _ = Describe("Radio", func() {
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
Expect(response.InternetRadioStations.Radios).To(HaveLen(2)) Expect(response.InternetRadioStations.Radios).To(HaveLen(2))
Expect(response.InternetRadioStations.Radios[0].OpenSubsonicRadio).ToNot(BeNil()) Expect(response.InternetRadioStations.Radios[0].OpenSubsonicRadio).ToNot(BeNil())
Expect(response.InternetRadioStations.Radios[0].CoverArt).To(Equal("rd-1_cover.jpg")) Expect(response.InternetRadioStations.Radios[0].CoverArt).To(Equal("ra-rd-1_0"))
Expect(response.InternetRadioStations.Radios[1].OpenSubsonicRadio).ToNot(BeNil()) Expect(response.InternetRadioStations.Radios[1].OpenSubsonicRadio).ToNot(BeNil())
Expect(response.InternetRadioStations.Radios[1].CoverArt).To(BeEmpty()) Expect(response.InternetRadioStations.Radios[1].CoverArt).To(BeEmpty())
}) })
@ -129,7 +129,7 @@ var _ = Describe("Radio", func() {
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
Expect(response.InternetRadioStations.Radios[0].OpenSubsonicRadio).ToNot(BeNil()) Expect(response.InternetRadioStations.Radios[0].OpenSubsonicRadio).ToNot(BeNil())
Expect(response.InternetRadioStations.Radios[0].CoverArt).To(Equal("rd-1_cover.jpg")) Expect(response.InternetRadioStations.Radios[0].CoverArt).To(Equal("ra-rd-1_0"))
}) })
}) })

View File

@ -18,7 +18,7 @@ import {
useTranslate, useTranslate,
} from 'react-admin' } from 'react-admin'
import Lightbox from 'react-image-lightbox' import Lightbox from 'react-image-lightbox'
import { COVER_ART_SIZE } from '../consts' import config from '../config'
import 'react-image-lightbox/style.css' import 'react-image-lightbox/style.css'
import subsonic from '../subsonic' import subsonic from '../subsonic'
import { import {
@ -32,7 +32,6 @@ import {
useAlbumsPerPage, useAlbumsPerPage,
useImageLoadingState, useImageLoadingState,
} from '../common' } from '../common'
import config from '../config'
import { formatFullDate, intersperse } from '../utils' import { formatFullDate, intersperse } from '../utils'
import AlbumExternalLinks from './AlbumExternalLinks' import AlbumExternalLinks from './AlbumExternalLinks'
import { SafeHTML } from '../common/SafeHTML' import { SafeHTML } from '../common/SafeHTML'
@ -255,7 +254,7 @@ const AlbumDetails = (props) => {
}) })
}, [record]) }, [record])
const imageUrl = subsonic.getCoverArtUrl(record, COVER_ART_SIZE) const imageUrl = subsonic.getCoverArtUrl(record, config.uiCoverArtSize)
const fullImageUrl = subsonic.getCoverArtUrl(record) const fullImageUrl = subsonic.getCoverArtUrl(record)
return ( return (

View File

@ -20,7 +20,8 @@ import {
OverflowTooltip, OverflowTooltip,
useImageUrl, useImageUrl,
} from '../common' } from '../common'
import { COVER_ART_SIZE, DraggableTypes } from '../consts' import config from '../config'
import { DraggableTypes } from '../consts'
import clsx from 'clsx' import clsx from 'clsx'
import { AlbumDatesField } from './AlbumDatesField.jsx' import { AlbumDatesField } from './AlbumDatesField.jsx'
@ -135,7 +136,7 @@ const Cover = withContentRect('bounds')(({
[record], [record],
) )
const url = subsonic.getCoverArtUrl(record, COVER_ART_SIZE, true) const url = subsonic.getCoverArtUrl(record, config.uiCoverArtSize, true)
const { imgUrl, loading: imageLoading } = useImageUrl(url) const { imgUrl, loading: imageLoading } = useImageUrl(url)
return ( return (

View File

@ -15,7 +15,6 @@ import {
import Lightbox from 'react-image-lightbox' import Lightbox from 'react-image-lightbox'
import ExpandInfoDialog from '../dialogs/ExpandInfoDialog' import ExpandInfoDialog from '../dialogs/ExpandInfoDialog'
import AlbumInfo from '../album/AlbumInfo' import AlbumInfo from '../album/AlbumInfo'
import { COVER_ART_SIZE } from '../consts'
import subsonic from '../subsonic' import subsonic from '../subsonic'
import { SafeHTML } from '../common/SafeHTML' import { SafeHTML } from '../common/SafeHTML'
@ -110,7 +109,7 @@ const DesktopArtistDetails = ({ artistInfo, record, biography }) => {
<CardMedia <CardMedia
key={record.id} key={record.id}
component="img" component="img"
src={subsonic.getCoverArtUrl(record, COVER_ART_SIZE)} src={subsonic.getCoverArtUrl(record, config.uiCoverArtSize)}
className={`${classes.cover} ${imageLoading ? classes.coverLoading : ''}`} className={`${classes.cover} ${imageLoading ? classes.coverLoading : ''}`}
onClick={handleOpenLightbox} onClick={handleOpenLightbox}
onLoad={handleImageLoad} onLoad={handleImageLoad}

View File

@ -11,7 +11,6 @@ import {
useImageLoadingState, useImageLoadingState,
} from '../common' } from '../common'
import Lightbox from 'react-image-lightbox' import Lightbox from 'react-image-lightbox'
import { COVER_ART_SIZE } from '../consts'
import subsonic from '../subsonic' import subsonic from '../subsonic'
import { SafeHTML } from '../common/SafeHTML' import { SafeHTML } from '../common/SafeHTML'
@ -113,7 +112,7 @@ const MobileArtistDetails = ({ artistInfo, biography, record }) => {
<CardMedia <CardMedia
key={record.id} key={record.id}
component="img" component="img"
src={subsonic.getCoverArtUrl(record, COVER_ART_SIZE)} src={subsonic.getCoverArtUrl(record, config.uiCoverArtSize)}
className={`${classes.cover} ${imageLoading ? classes.coverLoading : ''}`} className={`${classes.cover} ${imageLoading ? classes.coverLoading : ''}`}
onClick={handleOpenLightbox} onClick={handleOpenLightbox}
onLoad={handleImageLoad} onLoad={handleImageLoad}

View File

@ -2,7 +2,7 @@ import { useRecordContext } from 'react-admin'
import { Avatar } from '@material-ui/core' import { Avatar } from '@material-ui/core'
import { makeStyles } from '@material-ui/core/styles' import { makeStyles } from '@material-ui/core/styles'
import clsx from 'clsx' import clsx from 'clsx'
import { COVER_ART_SIZE } from '../consts' import config from '../config'
import subsonic from '../subsonic' import subsonic from '../subsonic'
import { useImageUrl } from './useImageUrl' import { useImageUrl } from './useImageUrl'
@ -28,7 +28,7 @@ export const CoverArtAvatar = ({
const record = recordProp || recordContext const record = recordProp || recordContext
const square = variant !== 'circular' const square = variant !== 'circular'
const url = record const url = record
? subsonic.getCoverArtUrl(record, COVER_ART_SIZE, square) ? subsonic.getCoverArtUrl(record, config.uiCoverArtSize, square)
: null : null
const { imgUrl } = useImageUrl(url) const { imgUrl } = useImageUrl(url)
if (!record) return null if (!record) return null

View File

@ -21,6 +21,7 @@ const defaultConfig = {
defaultLanguage: '', defaultLanguage: '',
defaultUIVolume: 100, defaultUIVolume: 100,
uiSearchDebounceMs: 200, uiSearchDebounceMs: 200,
uiCoverArtSize: 600,
enableUserEditing: true, enableUserEditing: true,
enableArtworkUpload: true, enableArtworkUpload: true,
enableSharing: true, enableSharing: true,

View File

@ -26,8 +26,6 @@ DraggableTypes.ALL.push(
export const RADIO_PLACEHOLDER_IMAGE = 'internet-radio-icon.svg' export const RADIO_PLACEHOLDER_IMAGE = 'internet-radio-icon.svg'
export const COVER_ART_SIZE = 600
export const DEFAULT_SHARE_BITRATE = 128 export const DEFAULT_SHARE_BITRATE = 128
export const BITRATE_CHOICES = [ export const BITRATE_CHOICES = [

View File

@ -18,7 +18,7 @@ import {
OverflowTooltip, OverflowTooltip,
useImageLoadingState, useImageLoadingState,
} from '../common' } from '../common'
import { COVER_ART_SIZE } from '../consts' import config from '../config'
import subsonic from '../subsonic' import subsonic from '../subsonic'
const useStyles = makeStyles( const useStyles = makeStyles(
@ -107,7 +107,7 @@ const PlaylistDetails = (props) => {
handleCloseLightbox, handleCloseLightbox,
} = useImageLoadingState(record.id) } = useImageLoadingState(record.id)
const imageUrl = subsonic.getCoverArtUrl(record, COVER_ART_SIZE, true) const imageUrl = subsonic.getCoverArtUrl(record, config.uiCoverArtSize, true)
const fullImageUrl = subsonic.getCoverArtUrl(record) const fullImageUrl = subsonic.getCoverArtUrl(record)
return ( return (

View File

@ -11,7 +11,8 @@ import { makeStyles } from '@material-ui/core/styles'
import { urlValidate } from '../utils/validations' import { urlValidate } from '../utils/validations'
import { Title, ImageUploadOverlay, useImageLoadingState } from '../common' import { Title, ImageUploadOverlay, useImageLoadingState } from '../common'
import subsonic from '../subsonic' import subsonic from '../subsonic'
import { COVER_ART_SIZE, RADIO_PLACEHOLDER_IMAGE } from '../consts' import config from '../config'
import { RADIO_PLACEHOLDER_IMAGE } from '../consts'
const useStyles = makeStyles({ const useStyles = makeStyles({
coverParent: { coverParent: {
@ -83,7 +84,7 @@ const RadioCoverArt = ({ record }) => {
{record.uploadedImage ? ( {record.uploadedImage ? (
<CardMedia <CardMedia
component="img" component="img"
src={subsonic.getCoverArtUrl(record, COVER_ART_SIZE, true)} src={subsonic.getCoverArtUrl(record, config.uiCoverArtSize, true)}
className={`${classes.cover} ${imageLoading ? classes.coverLoading : ''}`} className={`${classes.cover} ${imageLoading ? classes.coverLoading : ''}`}
onLoad={handleImageLoad} onLoad={handleImageLoad}
onError={handleImageError} onError={handleImageError}

View File

@ -1,5 +1,6 @@
import subsonic from '../subsonic' import subsonic from '../subsonic'
import { COVER_ART_SIZE, RADIO_PLACEHOLDER_IMAGE } from '../consts' import config from '../config'
import { RADIO_PLACEHOLDER_IMAGE } from '../consts'
export async function songFromRadio(radio) { export async function songFromRadio(radio) {
if (!radio) { if (!radio) {
@ -8,7 +9,7 @@ export async function songFromRadio(radio) {
let cover = RADIO_PLACEHOLDER_IMAGE let cover = RADIO_PLACEHOLDER_IMAGE
if (radio.uploadedImage) { if (radio.uploadedImage) {
cover = subsonic.getCoverArtUrl(radio, COVER_ART_SIZE, true) cover = subsonic.getCoverArtUrl(radio, config.uiCoverArtSize, true)
} else { } else {
// Try favicon as fallback // Try favicon as fallback
try { try {

View File

@ -1,5 +1,5 @@
import { vi } from 'vitest' import { vi } from 'vitest'
import { COVER_ART_SIZE } from '../consts' import config from '../config'
import subsonic from './index' import subsonic from './index'
describe('getCoverArtUrl', () => { describe('getCoverArtUrl', () => {
@ -31,7 +31,11 @@ describe('getCoverArtUrl', () => {
updatedAt: '2023-01-01T00:00:00Z', updatedAt: '2023-01-01T00:00:00Z',
} }
const url = subsonic.getCoverArtUrl(playlistRecord, COVER_ART_SIZE, true) const url = subsonic.getCoverArtUrl(
playlistRecord,
config.uiCoverArtSize,
true,
)
expect(url).toContain('pl-playlist-123') expect(url).toContain('pl-playlist-123')
expect(url).toContain('size=600') expect(url).toContain('size=600')
@ -45,7 +49,11 @@ describe('getCoverArtUrl', () => {
sync: true, sync: true,
} }
const url = subsonic.getCoverArtUrl(playlistRecord, COVER_ART_SIZE, true) const url = subsonic.getCoverArtUrl(
playlistRecord,
config.uiCoverArtSize,
true,
)
expect(url).toContain('pl-playlist-123') expect(url).toContain('pl-playlist-123')
expect(url).toContain('size=600') expect(url).toContain('size=600')
@ -60,7 +68,11 @@ describe('getCoverArtUrl', () => {
updatedAt: '2023-01-01T00:00:00Z', updatedAt: '2023-01-01T00:00:00Z',
} }
const url = subsonic.getCoverArtUrl(albumRecord, COVER_ART_SIZE, true) const url = subsonic.getCoverArtUrl(
albumRecord,
config.uiCoverArtSize,
true,
)
expect(url).toContain('al-album-123') expect(url).toContain('al-album-123')
expect(url).toContain('size=600') expect(url).toContain('size=600')
@ -74,7 +86,7 @@ describe('getCoverArtUrl', () => {
updatedAt: '2023-01-01T00:00:00Z', updatedAt: '2023-01-01T00:00:00Z',
} }
const url = subsonic.getCoverArtUrl(songRecord, COVER_ART_SIZE, true) const url = subsonic.getCoverArtUrl(songRecord, config.uiCoverArtSize, true)
expect(url).toContain('mf-song-123') expect(url).toContain('mf-song-123')
expect(url).toContain('size=600') expect(url).toContain('size=600')
@ -87,7 +99,11 @@ describe('getCoverArtUrl', () => {
updatedAt: '2023-01-01T00:00:00Z', updatedAt: '2023-01-01T00:00:00Z',
} }
const url = subsonic.getCoverArtUrl(artistRecord, COVER_ART_SIZE, true) const url = subsonic.getCoverArtUrl(
artistRecord,
config.uiCoverArtSize,
true,
)
expect(url).toContain('ar-artist-123') expect(url).toContain('ar-artist-123')
expect(url).toContain('size=600') expect(url).toContain('size=600')