mirror of
https://github.com/navidrome/navidrome.git
synced 2026-05-03 06:51:16 +00:00
Merge branch 'master' into package-lock/integrit
This commit is contained in:
commit
66ba2d82af
4
Makefile
4
Makefile
@ -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
|
||||||
|
|||||||
@ -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() {
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
6
go.mod
@ -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
12
go.sum
@ -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=
|
||||||
|
|||||||
@ -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)"},
|
||||||
|
|||||||
@ -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": {
|
||||||
|
|||||||
@ -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)",
|
||||||
|
|||||||
@ -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": {
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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"),
|
||||||
|
|||||||
@ -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() {
|
||||||
|
|||||||
@ -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())
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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"))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -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 (
|
||||||
|
|||||||
@ -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 (
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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 = [
|
||||||
|
|||||||
@ -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 (
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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')
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user