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 ' ')
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
export CGO_CFLAGS_ALLOW=--define-prefix

View File

@ -70,6 +70,7 @@ type configOptions struct {
MPVCmdTemplate string
CoverArtPriority string
CoverArtQuality int
EnableWebPEncoding bool
ArtistArtPriority string
ArtistImageFolder string
DiscArtPriority string
@ -87,6 +88,7 @@ type configOptions struct {
DefaultLanguage string
DefaultUIVolume int
UISearchDebounceMs int
UICoverArtSize int
EnableReplayGain bool
EnableCoverAnimation bool
EnableNowPlaying bool
@ -141,7 +143,6 @@ type configOptions struct {
DevOptimizeDB bool
DevPreserveUnicodeInExternalCalls bool
DevEnableMediaFileProbe bool
DevJpegCoverArt bool
}
type scannerOptions struct {
@ -424,6 +425,13 @@ func Load(noConfigDump bool) {
// Removed options
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
for _, hook := range hooks {
hook()
@ -712,10 +720,13 @@ func setViperDefaults() {
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("ffmpegpath", "")
viper.SetDefault("mpvpath", "")
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("coverartquality", 75)
viper.SetDefault("enablewebpencoding", false)
viper.SetDefault("artistartpriority", "artist.*, album/artist.*, external")
viper.SetDefault("artistimagefolder", "")
viper.SetDefault("discartpriority", "disc*.*, cd*.*, cover.*, folder.*, front.*, discsubtitle, embedded")
viper.SetDefault("lyricspriority", ".lrc,.txt,embedded")
viper.SetDefault("enablegravatar", false)
@ -726,6 +737,7 @@ func setViperDefaults() {
viper.SetDefault("defaultlanguage", "")
viper.SetDefault("defaultuivolume", consts.DefaultUIVolume)
viper.SetDefault("uisearchdebouncems", consts.DefaultUISearchDebounceMs)
viper.SetDefault("uicoverartsize", consts.DefaultUICoverArtSize)
viper.SetDefault("enablereplaygain", true)
viper.SetDefault("enablecoveranimation", true)
viper.SetDefault("enablenowplaying", true)
@ -794,6 +806,7 @@ func setViperDefaults() {
viper.SetDefault("plugins.enabled", true)
viper.SetDefault("plugins.cachesize", "200MB")
viper.SetDefault("plugins.autoreload", false)
viper.SetDefault("plugins.loglevel", "")
// DevFlags. These are used to enable/disable debugging and incomplete features
viper.SetDefault("devlogsourceline", false)
@ -807,7 +820,7 @@ func setViperDefaults() {
viper.SetDefault("devuishowconfig", true)
viper.SetDefault("devneweventstream", true)
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("devartworkthrottlebacklogtimeout", consts.RequestThrottleBacklogTimeout)
viper.SetDefault("devartistinfotimetolive", consts.ArtistInfoTimeToLive)
@ -823,7 +836,6 @@ func setViperDefaults() {
viper.SetDefault("devoptimizedb", true)
viper.SetDefault("devpreserveunicodeinexternalcalls", false)
viper.SetDefault("devenablemediafileprobe", true)
viper.SetDefault("devjpegcoverart", false)
}
func init() {

View File

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

View File

@ -380,24 +380,24 @@ var _ = Describe("Artwork", 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"
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("webp"))
Expect(format).To(Equal("png"))
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() {
It("returns JPEG 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(format).To(Equal("jpeg"))
Expect(err).ToNot(HaveOccurred())
Expect(img.Bounds().Size().X).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().Y).To(Equal(size))
},
Entry("portrait png image", "png", "webp", false, 200),
Entry("landscape png image", "png", "webp", true, 200),
Entry("portrait jpg image", "jpg", "webp", false, 200),
Entry("landscape jpg image", "jpg", "webp", true, 200),
Entry("portrait png image", "png", "png", false, 200),
Entry("landscape png image", "png", "png", true, 200),
Entry("portrait jpg image", "jpg", "png", false, 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() {
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"
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("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().Y).To(Equal(15))
})
@ -463,11 +490,11 @@ var _ = Describe("Artwork", func() {
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
BeforeEach(func() {
conf.Server.DevJpegCoverArt = true
conf.Server.EnableWebPEncoding = false
})
It("returns PNG for square mode", func() {
dirName := createImage("png", false, 200)

View File

@ -10,7 +10,6 @@ import (
"time"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/consts"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"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
// 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 {
// If image cache is disabled, return a NOOP implementation
if conf.Server.ImageCacheSize == "0" || !conf.Server.EnableArtworkPrecache {
@ -38,10 +37,11 @@ func NewCacheWarmer(artwork Artwork, cache cache.FileCache) CacheWarmer {
}
a := &cacheWarmer{
artwork: artwork,
cache: cache,
buffer: make(map[model.ArtworkID]struct{}),
wakeSignal: make(chan struct{}, 1),
artwork: artwork,
cache: cache,
buffer: make(map[model.ArtworkID]struct{}),
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
@ -51,11 +51,12 @@ func NewCacheWarmer(artwork Artwork, cache cache.FileCache) CacheWarmer {
}
type cacheWarmer struct {
artwork Artwork
buffer map[model.ArtworkID]struct{}
mutex sync.Mutex
cache cache.FileCache
wakeSignal chan struct{}
artwork Artwork
buffer map[model.ArtworkID]struct{}
mutex sync.Mutex
cache cache.FileCache
wakeSignal chan struct{}
coverArtSize int
}
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)
defer cancel()
for _, size := range consts.CacheWarmerImageSizes {
r, _, err := a.artwork.Get(ctx, id, size, true)
if err != nil {
return fmt.Errorf("caching id='%s', size=%d: %w", id, size, err)
}
_, err = io.Copy(io.Discard, r)
r.Close()
return err
size := a.coverArtSize
r, _, err := a.artwork.Get(ctx, id, size, true)
if err != nil {
return fmt.Errorf("caching id='%s', size=%d: %w", id, size, err)
}
return nil
_, err = io.Copy(io.Discard, r)
r.Close()
return err
}
func NoopCacheWarmer() CacheWarmer {

View File

@ -12,7 +12,6 @@ import (
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/conf/configtest"
"github.com/navidrome/navidrome/consts"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/utils/cache"
. "github.com/onsi/ginkgo/v2"
@ -182,7 +181,7 @@ var _ = Describe("CacheWarmer", func() {
Eventually(func() []int {
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 {
hashInput := conf.Server.CoverArtPriority
if conf.Server.EnableExternalServices {
hashInput += conf.Server.Agents
hashInput = conf.Server.Agents + hashInput
}
hash := md5.Sum([]byte(hashInput))
return fmt.Sprintf(

View File

@ -19,6 +19,16 @@ import (
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{
New: func() any {
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)
}
// Preserve animation for animated images (skip for square thumbnails)
if !a.square {
if isAnimatedGIF(data) {
if a.a.ffmpeg.IsAvailable() {
// 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)
if err == nil {
return r, 0, nil
}
log.Warn(ctx, "Could not convert animated GIF, falling back to static", err)
// Preserve animation for animated images
if isAnimatedGIF(data) {
if a.a.ffmpeg.IsAvailable() {
// 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)
if err == nil {
return r, 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
log.Warn(ctx, "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, a.size, a.square)
}
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 {
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.Reset()
if conf.Server.DevJpegCoverArt {
if square {
err = png.Encode(buf, dst)
} else {
err = jpeg.Encode(buf, dst, &jpeg.Options{Quality: conf.Server.CoverArtQuality})
}
} else {
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)

View File

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

View File

@ -65,6 +65,8 @@ type Data struct {
EnablePrometheus bool `json:"enablePrometheus,omitempty"`
EnableArtworkUpload bool `json:"enableArtworkUpload,omitempty"`
CoverArtQuality int `json:"coverArtQuality,omitempty"`
EnableWebPEncoding bool `json:"enableWebPEncoding,omitempty"`
UICoverArtSize int `json:"uiCoverArtSize,omitempty"`
EnableCoverAnimation bool `json:"enableCoverAnimation,omitempty"`
EnableNowPlaying bool `json:"enableNowPlaying,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/kr/pretty v0.3.1
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/mileusna/useragent v1.3.5
github.com/onsi/ginkgo/v2 v2.28.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/pressly/goose/v3 v3.27.0
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-secp256k1 v1.0.0 // 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/maruel/natural v1.3.0 // 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/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
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.4/go.mod h1:mSMtkZW92Z98M5YoNNztbRGxbXHql7tSitCvaxvo9l0=
github.com/lestrrat-go/httprc/v3 v3.0.5 h1:S+Mb4L2I+bM6JGTibLmxExhyTOqnXjqx+zi9MoXw/TM=
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/go.mod h1:2m0PV1A9tM4b/jVLMx8rh6rBl7F6WGb3EG2hufN9OQU=
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/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-sqlite3 v1.14.37 h1:3DOZp4cXis1cUIpCfXLtmlGolNLp2VEqhiB/PARNBIg=
github.com/mattn/go-sqlite3 v1.14.37/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.38 h1:tDUzL85kMvOrvpCt8P64SbGgVFtJB11GPi2AdmITgb4=
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/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg=
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/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=
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.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM=
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/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
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"},
"size": {field: "media_file.size"},
"compilation": {field: "media_file.compilation"},
"missing": {field: "media_file.missing"},
"explicitstatus": {field: "media_file.explicit_status"},
"dateadded": {field: "media_file.created_at"},
"datemodified": {field: "media_file.updated_at"},
@ -49,9 +50,11 @@ var fieldMap = map[string]*mappedField{
"catalognumber": {field: "media_file.catalog_num"},
"filepath": {field: "media_file.path"},
"filetype": {field: "media_file.suffix"},
"codec": {field: "media_file.codec"},
"duration": {field: "media_file.duration"},
"bitrate": {field: "media_file.bit_rate"},
"bitdepth": {field: "media_file.bit_depth"},
"samplerate": {field: "media_file.sample_rate"},
"bpm": {field: "media_file.bpm"},
"channels": {field: "media_file.channels"},
"loved": {field: "COALESCE(annotation.starred, false)"},

View File

@ -36,7 +36,9 @@
"bitDepth": "Bitprofundo",
"sampleRate": "Elprena rapido",
"missing": "Mankaj",
"libraryName": "Biblioteko"
"libraryName": "Biblioteko",
"composer": "",
"disc": ""
},
"actions": {
"addToQueue": "Ludi Poste",
@ -46,7 +48,8 @@
"download": "Elŝuti",
"playNext": "Ludu Poste",
"info": "Akiri Informon",
"showInPlaylist": "Montri en Ludlisto"
"showInPlaylist": "Montri en Ludlisto",
"instantMix": ""
}
},
"album": {
@ -328,6 +331,82 @@
"scanInProgress": "Skano progresas...",
"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": {
@ -511,7 +590,14 @@
"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.",
"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": {
"library": "Biblioteko",
@ -597,7 +683,8 @@
"exportSuccess": "Agordoj eksportiĝis al la tondujo en TOML-a formato",
"exportFailed": "Malsukcesis kopii agordojn",
"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": {

View File

@ -23,6 +23,7 @@
"bitDepth": "Bit-sakonera",
"sampleRate": "Lagin-tasa",
"channels": "Kanalak",
"disc": "%{discNumber}. diskoa",
"discSubtitle": "Diskoaren azpititulua",
"starred": "Gogokoa",
"comment": "Iruzkina",
@ -355,7 +356,8 @@
"allUsers": "Baimendu erabiltzaile guztiak",
"selectedUsers": "Hautatutako erabiltzaileak",
"allLibraries": "Baimendu liburutegi guztiak",
"selectedLibraries": "Hautatutako liburutegiak"
"selectedLibraries": "Hautatutako liburutegiak",
"allowWriteAccess": "Eman idazteko baimena"
},
"sections": {
"status": "Egoera",
@ -400,6 +402,7 @@
"allLibrariesHelp": "Gaituta dagoenean, pluginak liburutegi guztietara izango du sarbidea, baita etorkizunean sortuko direnetara ere.",
"noLibraries": "Ez da liburutegirik hautatu",
"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"
},
"placeholders": {
@ -554,6 +557,12 @@
}
},
"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",
"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.",
@ -673,6 +682,7 @@
"currentValue": "Uneko balioa",
"configurationFile": "Konfigurazio-fitxategia",
"exportToml": "Esportatu konfigurazioa (TOML)",
"downloadToml": "Deskargatu konfigurazioa (TOML)",
"exportSuccess": "Konfigurazioa arbelera esportatu da TOML formatuan",
"exportFailed": "Konfigurazioa kopiatzeak huts egin du",
"devFlagsHeader": "Garapen-adierazleak (aldatu/kendu litezke)",

View File

@ -37,7 +37,8 @@
"sampleRate": "Sample waarde",
"missing": "Ontbrekend",
"libraryName": "Bibliotheek",
"composer": ""
"composer": "Componist",
"disc": "Schijf %{discNumber}"
},
"actions": {
"addToQueue": "Voeg toe aan wachtrij",
@ -48,7 +49,7 @@
"playNext": "Volgende",
"info": "Meer info",
"showInPlaylist": "Toon in afspeellijst",
"instantMix": ""
"instantMix": "Instant mix"
}
},
"album": {
@ -350,10 +351,11 @@
"createdAt": "Geinstalleerd",
"configKey": "Sleutel",
"configValue": "Waarde",
"allUsers": "Alle gebruikers toelaten",
"allUsers": "Sta toe voor alle gebruikers",
"selectedUsers": "Geselecteerde gebruikers",
"allLibraries": "Alle bibliotheken toestaan",
"selectedLibraries": "Geselecteerde bibliotheken"
"allLibraries": "Sta toe voor alle bibliotheken",
"selectedLibraries": "Geselecteerde bibliotheken",
"allowWriteAccess": "Sta schrijftoegang toe"
},
"sections": {
"status": "Status",
@ -379,26 +381,27 @@
"notifications": {
"enabled": "Plugin actief",
"disabled": "Plugin niet actief",
"updated": "Plugin geupdate",
"updated": "Plugin bijgewerkt",
"error": "Fout bij updaten plugin"
},
"validation": {
"invalidJson": "Configuratie moet geldige JSON zijn"
},
"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",
"noConfig": "Geen configuratie ingesteld",
"allUsersHelp": "",
"allUsersHelp": "Als dit aanstaat heeft de plug-in toegang tot alle gebruikers, inclusief toekomstige.",
"noUsers": "Geen gebruikers geselecteerd",
"permissionReason": "Reden",
"usersRequired": "",
"allLibrariesHelp": "",
"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": "Als dit aanstaat, heeft de plug-in toegang tot alle bibliotheken, inclusief toekomstige.",
"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",
"configValidationError": "",
"schemaRenderError": ""
"configValidationError": "Configuratiecheck mislukt",
"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": {
"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.",
"noSimilarSongsFound": "Geen vergelijkbare 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": {
"library": "Bibliotheek",
@ -674,7 +683,8 @@
"exportSuccess": "Configuratie geëxporteerd naar klembord in TOML formaat",
"exportFailed": "Kopiëren van configuratie mislukt",
"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": {

View File

@ -6,6 +6,7 @@ import (
"net/http"
"path"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/consts"
"github.com/navidrome/navidrome/core/auth"
"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 {
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 {
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,
"defaultUIVolume": conf.Server.DefaultUIVolume,
"uiSearchDebounceMs": conf.Server.UISearchDebounceMs,
"uiCoverArtSize": conf.Server.UICoverArtSize,
"enableCoverAnimation": conf.Server.EnableCoverAnimation,
"enableNowPlaying": conf.Server.EnableNowPlaying,
"gaTrackingId": conf.Server.GATrackingID,

View File

@ -86,6 +86,7 @@ var _ = Describe("serveIndex", func() {
Entry("defaultLanguage", func() { conf.Server.DefaultLanguage = "pt" }, "defaultLanguage", "pt"),
Entry("defaultUIVolume", func() { conf.Server.DefaultUIVolume = 45 }, "defaultUIVolume", float64(45)),
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("enableNowPlaying", func() { conf.Server.EnableNowPlaying = true }, "enableNowPlaying", true),
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 {
player, ok := request.PlayerFrom(ctx)
if ok && isClientInList(conf.Server.Subsonic.LegacyClients, player.Client) {
return nil
}
pls := responses.OpenSubsonicPlaylist{}
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() {
It("returns all fields", func() {
result := router.buildPlaylist(ctx, playlist)
@ -213,6 +230,23 @@ var _ = Describe("buildPlaylist", func() {
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
}
// Add coverArt if not legacy client
var coverArt string
if g.UploadedImage != "" {
coverArt = g.CoverArtID().String()
}
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(response.InternetRadioStations.Radios).To(HaveLen(2))
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].CoverArt).To(BeEmpty())
})
@ -129,7 +129,7 @@ var _ = Describe("Radio", func() {
Expect(err).ToNot(HaveOccurred())
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,
} from 'react-admin'
import Lightbox from 'react-image-lightbox'
import { COVER_ART_SIZE } from '../consts'
import config from '../config'
import 'react-image-lightbox/style.css'
import subsonic from '../subsonic'
import {
@ -32,7 +32,6 @@ import {
useAlbumsPerPage,
useImageLoadingState,
} from '../common'
import config from '../config'
import { formatFullDate, intersperse } from '../utils'
import AlbumExternalLinks from './AlbumExternalLinks'
import { SafeHTML } from '../common/SafeHTML'
@ -255,7 +254,7 @@ const AlbumDetails = (props) => {
})
}, [record])
const imageUrl = subsonic.getCoverArtUrl(record, COVER_ART_SIZE)
const imageUrl = subsonic.getCoverArtUrl(record, config.uiCoverArtSize)
const fullImageUrl = subsonic.getCoverArtUrl(record)
return (

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -18,7 +18,7 @@ import {
OverflowTooltip,
useImageLoadingState,
} from '../common'
import { COVER_ART_SIZE } from '../consts'
import config from '../config'
import subsonic from '../subsonic'
const useStyles = makeStyles(
@ -107,7 +107,7 @@ const PlaylistDetails = (props) => {
handleCloseLightbox,
} = 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)
return (

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import { vi } from 'vitest'
import { COVER_ART_SIZE } from '../consts'
import config from '../config'
import subsonic from './index'
describe('getCoverArtUrl', () => {
@ -31,7 +31,11 @@ describe('getCoverArtUrl', () => {
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('size=600')
@ -45,7 +49,11 @@ describe('getCoverArtUrl', () => {
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('size=600')
@ -60,7 +68,11 @@ describe('getCoverArtUrl', () => {
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('size=600')
@ -74,7 +86,7 @@ describe('getCoverArtUrl', () => {
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('size=600')
@ -87,7 +99,11 @@ describe('getCoverArtUrl', () => {
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('size=600')