From e457f2130632aa745ef4942b7686792d36742ed1 Mon Sep 17 00:00:00 2001 From: Deluan Date: Tue, 18 Mar 2025 12:43:48 -0400 Subject: [PATCH 01/30] chore(server): show square flag in resize artwork logs Signed-off-by: Deluan --- core/artwork/reader_resized.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/artwork/reader_resized.go b/core/artwork/reader_resized.go index 46d0f8866..83e6e25c2 100644 --- a/core/artwork/reader_resized.go +++ b/core/artwork/reader_resized.go @@ -63,12 +63,12 @@ func (a *resizedArtworkReader) Reader(ctx context.Context) (io.ReadCloser, strin resized, origSize, err := resizeImage(orig, a.size, a.square) if resized == nil { - log.Trace(ctx, "Image smaller than requested size", "artID", a.artID, "original", origSize, "resized", a.size) + log.Trace(ctx, "Image smaller than requested size", "artID", a.artID, "original", origSize, "resized", a.size, "square", a.square) } else { - log.Trace(ctx, "Resizing artwork", "artID", a.artID, "original", origSize, "resized", a.size) + log.Trace(ctx, "Resizing artwork", "artID", a.artID, "original", origSize, "resized", a.size, "square", a.square) } if err != nil { - log.Warn(ctx, "Could not resize image. Will return image as is", "artID", a.artID, "size", a.size, err) + log.Warn(ctx, "Could not resize image. Will return image as is", "artID", a.artID, "size", a.size, "square", a.square, err) } if err != nil || resized == nil { // if we couldn't resize the image, return the original From 1ed893010756be1efbaac4dff52b29d56107d4a3 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Tue, 18 Mar 2025 22:23:04 +0000 Subject: [PATCH 02/30] fix(msi): don't override custom ini config (#3836) Previously addLine would add-or-update, resulting in the custom settings being overriden on upgrade. createLine will only add to the ini if the key doesn't already exist. --- release/wix/navidrome.wxs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/release/wix/navidrome.wxs b/release/wix/navidrome.wxs index 22ad93f86..ec8b164e8 100644 --- a/release/wix/navidrome.wxs +++ b/release/wix/navidrome.wxs @@ -43,9 +43,9 @@ - - - + + + From 0147bb5f12a659daea8abeeee445788de25e4e04 Mon Sep 17 00:00:00 2001 From: Deluan Date: Tue, 18 Mar 2025 19:12:07 -0400 Subject: [PATCH 03/30] chore(deps): upgrade viper to 1.20.0, add tests for the supported config formats Signed-off-by: Deluan --- conf/configuration.go | 5 ++++ conf/configuration_test.go | 50 ++++++++++++++++++++++++++++++++++++++ conf/export_test.go | 5 ++++ conf/testdata/cfg.ini | 6 +++++ conf/testdata/cfg.json | 12 +++++++++ conf/testdata/cfg.toml | 5 ++++ conf/testdata/cfg.yaml | 7 ++++++ go.mod | 8 +++--- go.sum | 17 +++++-------- 9 files changed, 99 insertions(+), 16 deletions(-) create mode 100644 conf/configuration_test.go create mode 100644 conf/export_test.go create mode 100644 conf/testdata/cfg.ini create mode 100644 conf/testdata/cfg.json create mode 100644 conf/testdata/cfg.toml create mode 100644 conf/testdata/cfg.yaml diff --git a/conf/configuration.go b/conf/configuration.go index e1e6b2f67..9abb7e163 100644 --- a/conf/configuration.go +++ b/conf/configuration.go @@ -10,6 +10,7 @@ import ( "time" "github.com/bmatcuk/doublestar/v4" + "github.com/go-viper/encoding/ini" "github.com/kr/pretty" "github.com/navidrome/navidrome/consts" "github.com/navidrome/navidrome/log" @@ -549,6 +550,10 @@ func init() { } func InitConfig(cfgFile string) { + codecRegistry := viper.NewCodecRegistry() + _ = codecRegistry.RegisterCodec("ini", ini.Codec{}) + viper.SetOptions(viper.WithCodecRegistry(codecRegistry)) + cfgFile = getConfigFile(cfgFile) if cfgFile != "" { // Use config file from the flag. diff --git a/conf/configuration_test.go b/conf/configuration_test.go new file mode 100644 index 000000000..f57764709 --- /dev/null +++ b/conf/configuration_test.go @@ -0,0 +1,50 @@ +package conf_test + +import ( + "fmt" + "path/filepath" + "testing" + + . "github.com/navidrome/navidrome/conf" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/spf13/viper" +) + +func TestConfiguration(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Configuration Suite") +} + +var _ = Describe("Configuration", func() { + BeforeEach(func() { + // Reset viper configuration + viper.Reset() + viper.SetDefault("datafolder", GinkgoT().TempDir()) + viper.SetDefault("loglevel", "error") + ResetConf() + }) + + DescribeTable("should load configuration from", + func(format string) { + filename := filepath.Join("testdata", "cfg."+format) + + // Initialize config with the test file + InitConfig(filename) + // Load the configuration (with noConfigDump=true) + Load(true) + + // Execute the format-specific assertions + Expect(Server.MusicFolder).To(Equal(fmt.Sprintf("/%s/music", format))) + Expect(Server.UIWelcomeMessage).To(Equal("Welcome " + format)) + Expect(Server.Tags["custom"].Aliases).To(Equal([]string{format, "test"})) + + // The config file used should be the one we created + Expect(Server.ConfigFile).To(Equal(filename)) + }, + Entry("TOML format", "toml"), + Entry("YAML format", "yaml"), + Entry("INI format", "ini"), + Entry("JSON format", "json"), + ) +}) diff --git a/conf/export_test.go b/conf/export_test.go new file mode 100644 index 000000000..0bd7819eb --- /dev/null +++ b/conf/export_test.go @@ -0,0 +1,5 @@ +package conf + +func ResetConf() { + Server = &configOptions{} +} diff --git a/conf/testdata/cfg.ini b/conf/testdata/cfg.ini new file mode 100644 index 000000000..cec7d3c70 --- /dev/null +++ b/conf/testdata/cfg.ini @@ -0,0 +1,6 @@ +[default] +MusicFolder = /ini/music +UIWelcomeMessage = Welcome ini + +[Tags] +Custom.Aliases = ini,test \ No newline at end of file diff --git a/conf/testdata/cfg.json b/conf/testdata/cfg.json new file mode 100644 index 000000000..37cf74f08 --- /dev/null +++ b/conf/testdata/cfg.json @@ -0,0 +1,12 @@ +{ + "musicFolder": "/json/music", + "uiWelcomeMessage": "Welcome json", + "Tags": { + "custom": { + "aliases": [ + "json", + "test" + ] + } + } +} \ No newline at end of file diff --git a/conf/testdata/cfg.toml b/conf/testdata/cfg.toml new file mode 100644 index 000000000..1dc852b18 --- /dev/null +++ b/conf/testdata/cfg.toml @@ -0,0 +1,5 @@ +musicFolder = "/toml/music" +uiWelcomeMessage = "Welcome toml" + +[Tags.custom] +aliases = ["toml", "test"] diff --git a/conf/testdata/cfg.yaml b/conf/testdata/cfg.yaml new file mode 100644 index 000000000..38b98d4aa --- /dev/null +++ b/conf/testdata/cfg.yaml @@ -0,0 +1,7 @@ +musicFolder: "/yaml/music" +uiWelcomeMessage: "Welcome yaml" +Tags: + custom: + aliases: + - yaml + - test diff --git a/go.mod b/go.mod index f88a05abc..158409f53 100644 --- a/go.mod +++ b/go.mod @@ -26,6 +26,7 @@ require ( github.com/go-chi/cors v1.2.1 github.com/go-chi/httprate v0.14.1 github.com/go-chi/jwtauth/v5 v5.3.2 + github.com/go-viper/encoding/ini v0.1.1 github.com/gohugoio/hashstructure v0.5.0 github.com/google/go-pipeline v0.0.0-20230411140531-6cbedfc1d3fc github.com/google/uuid v1.6.0 @@ -50,7 +51,7 @@ require ( github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.9.1 - github.com/spf13/viper v1.19.0 + github.com/spf13/viper v1.20.0 github.com/stretchr/testify v1.10.0 github.com/unrolled/secure v1.17.0 github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 @@ -76,13 +77,13 @@ require ( github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect + github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7 // indirect github.com/google/subcommands v1.2.0 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect - github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/klauspost/compress v1.17.11 // indirect @@ -94,9 +95,7 @@ require ( github.com/lestrrat-go/httprc v1.0.6 // indirect github.com/lestrrat-go/iter v1.0.2 // indirect github.com/lestrrat-go/option v1.0.1 // indirect - github.com/magiconair/properties v1.8.9 // indirect github.com/mfridman/interpolate v0.0.2 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/ogier/pflag v0.0.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect @@ -105,7 +104,6 @@ require ( github.com/prometheus/procfs v0.15.1 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/sagikazarmark/locafero v0.7.0 // indirect - github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/segmentio/asm v1.2.0 // indirect github.com/sethvargo/go-retry v0.3.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect diff --git a/go.sum b/go.sum index 61d6ab157..b4d8b72aa 100644 --- a/go.sum +++ b/go.sum @@ -70,6 +70,10 @@ github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpv github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/go-viper/encoding/ini v0.1.1 h1:MVWY7B2XNw7lnOqHutGRc97bF3rP7omOdgjdMPAJgbs= +github.com/go-viper/encoding/ini v0.1.1/go.mod h1:Pfi4M2V1eAGJVZ5q6FrkHPhtHED2YgLlXhvgMVrB+YQ= +github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= +github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/gohugoio/hashstructure v0.5.0 h1:G2fjSBU36RdwEJBWJ+919ERvOVqAg9tfcYp47K9swqg= @@ -98,11 +102,8 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jellydator/ttlcache/v3 v3.3.0 h1:BdoC9cE81qXfrxeb9eoJi9dWrdhSuwXMAnHTbnBm4Wc= @@ -141,8 +142,6 @@ github.com/lestrrat-go/jwx/v2 v2.1.4 h1:uBCMmJX8oRZStmKuMMOFb0Yh9xmEMgNJLgjuKKt4 github.com/lestrrat-go/jwx/v2 v2.1.4/go.mod h1:nWRbDFR1ALG2Z6GJbBXzfQaYyvn751KuuyySN2yR6is= github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= -github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM= -github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/matoous/go-nanoid/v2 v2.1.0 h1:P64+dmq21hhWdtvZfEAofnvJULaRR1Yib0+PnU669bE= github.com/matoous/go-nanoid/v2 v2.1.0/go.mod h1:KlbGNQ+FhrUNIHUxZdL63t7tl4LaPkZNpUULS8H4uVM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= @@ -155,8 +154,6 @@ github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwX github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/mileusna/useragent v1.3.5 h1:SJM5NzBmh/hO+4LGeATKpaEX9+b4vcGg2qXGLiNGDws= github.com/mileusna/useragent v1.3.5/go.mod h1:3d8TOmwL/5I8pJjyVDteHtgDGcefrFUX4ccGOMKNYYc= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= @@ -199,8 +196,6 @@ github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 h1:OkMGxebDj github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs= github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= -github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= -github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE= @@ -222,8 +217,8 @@ github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= -github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= +github.com/spf13/viper v1.20.0 h1:zrxIyR3RQIOsarIrgL8+sAvALXul9jeEPa06Y0Ph6vY= +github.com/spf13/viper v1.20.0/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= From ee2c2b19e95917dc20c1b086d2a90baccafdb232 Mon Sep 17 00:00:00 2001 From: Deluan Date: Wed, 19 Mar 2025 20:18:56 -0400 Subject: [PATCH 04/30] fix(dockerfile): remove the healthcheck, it gives more headaches than benefits. Signed-off-by: Deluan --- Dockerfile | 1 - 1 file changed, 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 9dda15cc6..0ec64f769 100644 --- a/Dockerfile +++ b/Dockerfile @@ -138,7 +138,6 @@ ENV GODEBUG="asyncpreemptoff=1" RUN touch /.nddockerenv EXPOSE ${ND_PORT} -HEALTHCHECK CMD wget -O- http://localhost:${ND_PORT}/ping || exit 1 WORKDIR /app ENTRYPOINT ["/app/navidrome"] From cd552a55efc812a2bc9b3302125146f9a7171e66 Mon Sep 17 00:00:00 2001 From: Deluan Date: Wed, 19 Mar 2025 22:15:20 -0400 Subject: [PATCH 05/30] fix(scanner): pass datafolder and cachefolder to scanner subprocess Fix #3831 Signed-off-by: Deluan --- scanner/external.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scanner/external.go b/scanner/external.go index b00c67cb9..c4a29efa3 100644 --- a/scanner/external.go +++ b/scanner/external.go @@ -33,6 +33,8 @@ func (s *scannerExternal) scanAll(ctx context.Context, fullScan bool, progress c cmd := exec.CommandContext(ctx, exe, "scan", "--nobanner", "--subprocess", "--configfile", conf.Server.ConfigFile, + "--datafolder", conf.Server.DataFolder, + "--cachefolder", conf.Server.CacheFolder, If(fullScan, "--full", "")) in, out := io.Pipe() From 491210ac1207239292181c31fdaf1b5b00fb48c2 Mon Sep 17 00:00:00 2001 From: Deluan Date: Thu, 20 Mar 2025 12:39:40 -0400 Subject: [PATCH 06/30] fix(scanner): ignore NaN ReplayGain values Fix: https://github.com/navidrome/navidrome/issues/3858 Signed-off-by: Deluan --- model/metadata/metadata.go | 2 +- model/metadata/metadata_test.go | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/model/metadata/metadata.go b/model/metadata/metadata.go index 3d5d64dd1..471c2434c 100644 --- a/model/metadata/metadata.go +++ b/model/metadata/metadata.go @@ -120,7 +120,7 @@ func (md Metadata) first(key model.TagName) string { func float(value string, def ...float64) float64 { v, err := strconv.ParseFloat(value, 64) - if err != nil || v == math.Inf(-1) || v == math.Inf(1) { + if err != nil || v == math.Inf(-1) || math.IsInf(v, 1) || math.IsNaN(v) { if len(def) > 0 { return def[0] } diff --git a/model/metadata/metadata_test.go b/model/metadata/metadata_test.go index f3478ccba..5d9c4a3ed 100644 --- a/model/metadata/metadata_test.go +++ b/model/metadata/metadata_test.go @@ -264,6 +264,7 @@ var _ = Describe("Metadata", func() { Entry("1.2dB", "1.2dB", 1.2), Entry("Infinity", "Infinity", 0.0), Entry("Invalid value", "INVALID VALUE", 0.0), + Entry("NaN", "NaN", 0.0), ) DescribeTable("Peak", func(tagValue string, expected float64) { @@ -275,6 +276,7 @@ var _ = Describe("Metadata", func() { Entry("Invalid dB suffix", "0.7dB", 1.0), Entry("Infinity", "Infinity", 1.0), Entry("Invalid value", "INVALID VALUE", 1.0), + Entry("NaN", "NaN", 1.0), ) DescribeTable("getR128GainValue", func(tagValue string, expected float64) { From 59ece403931373a085378537da904a5d162b488f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deluan=20Quint=C3=A3o?= Date: Thu, 20 Mar 2025 19:26:40 -0400 Subject: [PATCH 07/30] fix(server): better embedded artwork extraction with ffmpeg (#3860) - `-map 0:v` selects all video streams from the input - `-map -0:V` excludes all "main" video streams (capital V) This combination effectively selects only the attached pictures Signed-off-by: Deluan --- core/ffmpeg/ffmpeg.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/ffmpeg/ffmpeg.go b/core/ffmpeg/ffmpeg.go index bb57e5101..2e0d5a4b7 100644 --- a/core/ffmpeg/ffmpeg.go +++ b/core/ffmpeg/ffmpeg.go @@ -29,7 +29,7 @@ func New() FFmpeg { } const ( - extractImageCmd = "ffmpeg -i %s -an -vcodec copy -f image2pipe -" + extractImageCmd = "ffmpeg -i %s -map 0:v -map -0:V -vcodec copy -f image2pipe -" probeCmd = "ffmpeg %s -f ffmetadata" ) From d78c6f6a04df4f8c2b6758d0991c894e859852cc Mon Sep 17 00:00:00 2001 From: Deluan Date: Thu, 20 Mar 2025 22:10:42 -0400 Subject: [PATCH 08/30] fix(subsonic): ArtistID3 should contain list of AlbumID3 Signed-off-by: Deluan --- server/subsonic/browsing.go | 2 +- server/subsonic/filter/filters.go | 4 ++-- server/subsonic/responses/responses.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/server/subsonic/browsing.go b/server/subsonic/browsing.go index 82bf50dc5..d46c7937d 100644 --- a/server/subsonic/browsing.go +++ b/server/subsonic/browsing.go @@ -424,7 +424,7 @@ func (api *Router) buildArtist(r *http.Request, artist *model.Artist) (*response return nil, err } - a.Album = slice.MapWithArg(albums, ctx, childFromAlbum) + a.Album = slice.MapWithArg(albums, ctx, buildAlbumID3) return a, nil } diff --git a/server/subsonic/filter/filters.go b/server/subsonic/filter/filters.go index 8a333c8e2..1b5416695 100644 --- a/server/subsonic/filter/filters.go +++ b/server/subsonic/filter/filters.go @@ -48,11 +48,11 @@ func AlbumsByArtist() Options { func AlbumsByArtistID(artistId string) Options { filters := []Sqlizer{ - persistence.Exists("json_tree(Participants, '$.albumartist')", Eq{"value": artistId}), + persistence.Exists("json_tree(participants, '$.albumartist')", Eq{"value": artistId}), } if conf.Server.Subsonic.ArtistParticipations { filters = append(filters, - persistence.Exists("json_tree(Participants, '$.artist')", Eq{"value": artistId}), + persistence.Exists("json_tree(participants, '$.artist')", Eq{"value": artistId}), ) } return addDefaultFilters(Options{ diff --git a/server/subsonic/responses/responses.go b/server/subsonic/responses/responses.go index c0f499ef2..0d22ef50b 100644 --- a/server/subsonic/responses/responses.go +++ b/server/subsonic/responses/responses.go @@ -284,7 +284,7 @@ type OpenSubsonicAlbumID3 struct { type ArtistWithAlbumsID3 struct { ArtistID3 - Album []Child `xml:"album" json:"album,omitempty"` + Album []AlbumID3 `xml:"album" json:"album,omitempty"` } type AlbumWithSongsID3 struct { From 1e1dce92b6a2a508f35a30d8ff8ac60274a780ce Mon Sep 17 00:00:00 2001 From: Xabi <888924+xabirequejo@users.noreply.github.com> Date: Sat, 22 Mar 2025 17:29:43 +0100 Subject: [PATCH 09/30] fix(ui): update Basque translation (#3864) * Update Basque localisation added missing strings * Update eu.json --- resources/i18n/eu.json | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/resources/i18n/eu.json b/resources/i18n/eu.json index a28e5751d..067310c14 100644 --- a/resources/i18n/eu.json +++ b/resources/i18n/eu.json @@ -216,6 +216,7 @@ "username": "Partekatzailea:", "url": "URLa", "description": "Deskribapena", + "downloadable": "Deskargatzea ahalbidetu?", "contents": "Edukia", "expiresAt": "Iraungitze-data:", "lastVisitedAt": "Azkenekoz bisitatu zen:", @@ -223,22 +224,24 @@ "format": "Formatua", "maxBitRate": "Gehienezko bit tasa", "updatedAt": "Eguneratze-data:", - "createdAt": "Sortze-data:", - "downloadable": "Deskargatzea ahalbidetu?" - } + "createdAt": "Sortze-data:" + }, + "notifications": {}, + "actions": {} }, "missing": { - "name": "", + "name": "Fitxategia falta da|||| Fitxategiak falta dira", + "empty": "Ez da fitxategirik falta", "fields": { - "path": "", - "size": "", - "updatedAt": "" + "path": "Bidea", + "size": "Tamaina", + "updatedAt": "Desagertze-data:" }, "actions": { - "remove": "" + "remove": "Kendu" }, "notifications": { - "removed": "" + "removed": "Faltan zeuden fitxategiak kendu dira" } } }, @@ -509,4 +512,4 @@ "current_song": "Uneko abestia" } } -} \ No newline at end of file +} From 63dc0e2062723171d2b54de3b7a232f5f6b6fb16 Mon Sep 17 00:00:00 2001 From: Nicolas Derive Date: Sat, 22 Mar 2025 17:31:32 +0100 Subject: [PATCH 10/30] =?UTF-8?q?fix(ui):=20update=20Fran=C3=A7ais,=20reor?= =?UTF-8?q?der=20translation=20according=20to=20en.json=20template=20(#383?= =?UTF-8?q?9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update french translation and reorder the file the same way as the en.json template, making comparison easier. --- resources/i18n/fr.json | 108 +++++++++++++++++++++++++++++------------ 1 file changed, 77 insertions(+), 31 deletions(-) diff --git a/resources/i18n/fr.json b/resources/i18n/fr.json index 7f8403bc3..50bc0d449 100644 --- a/resources/i18n/fr.json +++ b/resources/i18n/fr.json @@ -25,8 +25,13 @@ "quality": "Qualité", "bpm": "BPM", "playDate": "Derniers joués", - "channels": "Canaux", - "createdAt": "Date d'ajout" + "createdAt": "Date d'ajout", + "grouping": "Regroupement", + "mood": "Humeur", + "participants": "Participants supplémentaires", + "tags": "Étiquettes supplémentaires", + "mappedTags": "Étiquettes correspondantes", + "rawTags": "Étiquettes brutes" }, "actions": { "addToQueue": "Ajouter à la file", @@ -46,29 +51,35 @@ "duration": "Durée", "songCount": "Nombre de pistes", "playCount": "Nombre d'écoutes", + "size": "Taille", "name": "Nom", "genre": "Genre", "compilation": "Compilation", "year": "Année", + "originalDate": "Original", + "releaseDate": "Sortie", + "releases": "Sortie |||| Sorties", + "released": "Sortie", "updatedAt": "Mis à jour le", "comment": "Commentaire", "rating": "Classement", "createdAt": "Date d'ajout", - "size": "Taille", - "originalDate": "Original", - "releaseDate": "Sortie", - "releases": "Sortie |||| Sorties", - "released": "Sortie" + "recordLabel": "Label", + "catalogNum": "Numéro de catalogue", + "releaseType": "Type", + "grouping": "Regroupement", + "media": "Média", + "mood": "Humeur" }, "actions": { "playAll": "Lire", "playNext": "Lire ensuite", "addToQueue": "Ajouter à la file", + "share": "Partager", "shuffle": "Mélanger", "addToPlaylist": "Ajouter à la playlist", "download": "Télécharger", - "info": "Plus d'informations", - "share": "Partager" + "info": "Plus d'informations" }, "lists": { "all": "Tous", @@ -86,10 +97,26 @@ "name": "Nom", "albumCount": "Nombre d'albums", "songCount": "Nombre de pistes", + "size": "Taille", "playCount": "Lectures", "rating": "Classement", "genre": "Genre", - "size": "Taille" + "role": "Rôle" + }, + "roles": { + "albumartist": "Artiste de l'album |||| Artistes de l'album", + "artist": "Artiste |||| Artistes", + "composer": "Compositeur |||| Compositeurs", + "conductor": "Chef d'orchestre |||| Chefs d'orchestre", + "lyricist": "Parolier |||| Paroliers", + "arranger": "Arrangeur |||| Arrangeurs", + "producer": "Producteur |||| Producteurs", + "director": "Réalisateur |||| Réalisateurs", + "engineer": "Ingénieur |||| Ingénieurs", + "mixer": "Mixeur |||| Mixeurs", + "remixer": "Remixeur |||| Remixeurs", + "djmixer": "Mixeur DJ |||| Mixeurs DJ", + "performer": "Interprète |||| Interprètes" } }, "user": { @@ -98,6 +125,7 @@ "userName": "Nom d'utilisateur", "isAdmin": "Administrateur", "lastLoginAt": "Dernière connexion", + "lastAccessAt": "Dernier accès", "updatedAt": "Dernière mise à jour", "name": "Nom", "password": "Mot de passe", @@ -105,8 +133,7 @@ "changePassword": "Changer le mot de passe ?", "currentPassword": "Mot de passe actuel", "newPassword": "Nouveau mot de passe", - "token": "Token", - "lastAccessAt": "Dernier accès" + "token": "Token" }, "helperTexts": { "name": "Les changements liés à votre nom ne seront reflétés qu'à la prochaine connexion" @@ -152,7 +179,7 @@ "public": "Publique", "updatedAt": "Mise à jour le", "createdAt": "Créée le", - "songCount": "Titres", + "songCount": "Morceaux", "comment": "Commentaire", "sync": "Import automatique", "path": "Importer depuis" @@ -188,6 +215,7 @@ "username": "Partagé(e) par", "url": "Lien URL", "description": "Description", + "downloadable": "Autoriser les téléchargements ?", "contents": "Contenu", "expiresAt": "Expire le", "lastVisitedAt": "Visité pour la dernière fois", @@ -195,8 +223,24 @@ "format": "Format", "maxBitRate": "Bitrate maximum", "updatedAt": "Mis à jour le", - "createdAt": "Créé le", - "downloadable": "Autoriser les téléchargements ?" + "createdAt": "Créé le" + }, + "notifications": {}, + "actions": {} + }, + "missing": { + "name": "Fichier manquant|||| Fichiers manquants", + "empty": "Aucun fichier manquant", + "fields": { + "path": "Chemin", + "size": "Taille", + "updatedAt": "A disparu le" + }, + "actions": { + "remove": "Supprimer" + }, + "notifications": { + "removed": "Fichier(s) manquant(s) supprimé(s)" } } }, @@ -235,6 +279,7 @@ "add": "Ajouter", "back": "Retour", "bulk_actions": "%{smart_count} sélectionné |||| %{smart_count} sélectionnés", + "bulk_actions_mobile": "1 |||| %{smart_count}", "cancel": "Annuler", "clear_input_value": "Vider le champ", "clone": "Dupliquer", @@ -258,7 +303,6 @@ "close_menu": "Fermer le menu", "unselect": "Désélectionner", "skip": "Ignorer", - "bulk_actions_mobile": "1 |||| %{smart_count}", "share": "Partager", "download": "Télécharger" }, @@ -273,10 +317,10 @@ "error": "Un problème est survenu", "list": "%{name}", "loading": "Chargement", - "not_found": "Page manquante", + "not_found": "Introuvable", "show": "%{name} #%{id}", "empty": "Pas encore de %{name}.", - "invite": "Voulez-vous en créer ?" + "invite": "Voulez-vous en créer un ?" }, "input": { "file": { @@ -353,29 +397,31 @@ "noPlaylistsAvailable": "Aucune playlist", "delete_user_title": "Supprimer l'utilisateur '%{name}'", "delete_user_content": "Êtes-vous sûr(e) de vouloir supprimer cet utilisateur et ses données associées (y compris ses playlists et préférences) ?", + "remove_missing_title": "Supprimer les fichiers manquants", + "remove_missing_content": "Êtes-vous sûr(e) de vouloir supprimer les fichiers manquants sélectionnés de la base de données ? Ceci supprimera définiviement toute référence à ceux-ci, y compris leurs nombres d'écoutes et leurs notations.", "notifications_blocked": "Votre navigateur bloque les notifications de ce site", "notifications_not_available": "Votre navigateur ne permet pas d'afficher les notifications sur le bureau ou vous n'accédez pas à Navidrome via HTTPS", "lastfmLinkSuccess": "Last.fm a été correctement relié et le scrobble a été activé", "lastfmLinkFailure": "Last.fm n'a pas pu être correctement relié", "lastfmUnlinkSuccess": "Last.fm n'est plus relié et le scrobble a été désactivé", "lastfmUnlinkFailure": "Erreur pendant la suppression du lien avec Last.fm", + "listenBrainzLinkSuccess": "La liaison et le scrobble avec ListenBrainz sont maintenant activés pour l'utilisateur : %{user}", + "listenBrainzLinkFailure": "Échec lors de la liaison avec ListenBrainz : %{error}", + "listenBrainzUnlinkSuccess": "La liaison et le scrobble avec ListenBrainz sont maintenant désactivés", + "listenBrainzUnlinkFailure": "Échec lors de la désactivation de la liaison avec ListenBrainz", "openIn": { "lastfm": "Ouvrir dans Last.fm", "musicbrainz": "Ouvrir dans MusicBrainz" }, "lastfmLink": "Lire plus...", - "listenBrainzLinkSuccess": "La liaison et le scrobble avec ListenBrainz sont maintenant activés pour l'utilisateur : %{user}", - "listenBrainzLinkFailure": "Échec lors de la liaison avec ListenBrainz : %{error}", - "listenBrainzUnlinkSuccess": "La liaison et le scrobble avec ListenBrainz sont maintenant désactivés", - "listenBrainzUnlinkFailure": "Échec lors de la désactivation de la liaison avec ListenBrainz", - "downloadOriginalFormat": "Télécharger au format original", "shareOriginalFormat": "Partager avec le format original", "shareDialogTitle": "Partager %{resource} '%{name}'", "shareBatchDialogTitle": "Partager 1 %{resource} |||| Partager %{smart_count} %{resource}", + "shareCopyToClipboard": "Copier vers le presse-papier : Ctrl+C, Enter", "shareSuccess": "Lien copié vers le presse-papier : %{url}", "shareFailure": "Erreur en copiant le lien %{url} vers le presse-papier", "downloadDialogTitle": "Télécharger %{resource} '%{name}' (%{size})", - "shareCopyToClipboard": "Copier vers le presse-papier : Ctrl+C, Enter" + "downloadOriginalFormat": "Télécharger au format original" }, "menu": { "library": "Bibliothèque", @@ -389,6 +435,7 @@ "language": "Langue", "defaultView": "Vue par défaut", "desktop_notifications": "Notifications de bureau", + "lastfmNotConfigured": "La clef API de Last.fm n'est pas configurée", "lastfmScrobbling": "Scrobbler vers Last.fm", "listenBrainzScrobbling": "Scrobbler vers ListenBrainz", "replaygain": "Mode ReplayGain", @@ -397,14 +444,13 @@ "none": "Désactivé", "album": "Utiliser le gain de l'album", "track": "Utiliser le gain des pistes" - }, - "lastfmNotConfigured": "La clef API de Last.fm n'est pas configurée" + } } }, "albumList": "Albums", - "about": "À propos", "playlists": "Playlists", - "sharedPlaylists": "Playlists partagées" + "sharedPlaylists": "Playlists partagées", + "about": "À propos" }, "player": { "playListsText": "File de lecture", @@ -459,10 +505,10 @@ "toggle_play": "Lecture/Pause", "prev_song": "Morceau précédent", "next_song": "Morceau suivant", + "current_song": "Aller à la chanson en cours", "vol_up": "Augmenter le volume", "vol_down": "Baisser le volume", - "toggle_love": "Ajouter/Enlever le morceau des favoris", - "current_song": "Aller à la chanson en cours" + "toggle_love": "Ajouter/Enlever le morceau des favoris" } } -} \ No newline at end of file +} From be7cb59dc5255b845f491326ce936e7ab6165819 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deluan=20Quint=C3=A3o?= Date: Sat, 22 Mar 2025 12:34:35 -0400 Subject: [PATCH 11/30] fix(scanner): allow disabling splitting with the `Tags` config option (#3869) Signed-off-by: Deluan --- model/metadata/map_participants.go | 12 ++++++++++-- model/tag_mappings.go | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/model/metadata/map_participants.go b/model/metadata/map_participants.go index 9305d8791..a871f64fa 100644 --- a/model/metadata/map_participants.go +++ b/model/metadata/map_participants.go @@ -176,7 +176,11 @@ func (md Metadata) getRoleValues(role model.TagName) []string { if len(values) == 0 { return nil } - if conf := model.TagRolesConf(); len(conf.Split) > 0 { + conf := model.TagMainMappings()[role] + if conf.Split == nil { + conf = model.TagRolesConf() + } + if len(conf.Split) > 0 { values = conf.SplitTagValue(values) return filterDuplicatedOrEmptyValues(values) } @@ -193,7 +197,11 @@ func (md Metadata) getArtistValues(single, multi model.TagName) []string { if len(vSingle) != 1 { return vSingle } - if conf := model.TagArtistsConf(); len(conf.Split) > 0 { + conf := model.TagMainMappings()[single] + if conf.Split == nil { + conf = model.TagArtistsConf() + } + if len(conf.Split) > 0 { vSingle = conf.SplitTagValue(vSingle) return filterDuplicatedOrEmptyValues(vSingle) } diff --git a/model/tag_mappings.go b/model/tag_mappings.go index d8caa0c5d..d11b58fdc 100644 --- a/model/tag_mappings.go +++ b/model/tag_mappings.go @@ -201,7 +201,7 @@ func loadTagMappings() { aliases = oldValue.Aliases } split := cfg.Split - if len(split) == 0 { + if split == nil { split = oldValue.Split } c := TagConf{ From b386981b7f06b00ab154ba2136d4a316c6a08558 Mon Sep 17 00:00:00 2001 From: Deluan Date: Sat, 22 Mar 2025 15:07:51 -0400 Subject: [PATCH 12/30] fix(scanner): better log message when AutoImportPlaylists is disabled Fix #3861 Signed-off-by: Deluan --- scanner/phase_4_playlists.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scanner/phase_4_playlists.go b/scanner/phase_4_playlists.go index c3e76cb8c..c98b51ee6 100644 --- a/scanner/phase_4_playlists.go +++ b/scanner/phase_4_playlists.go @@ -45,8 +45,12 @@ func (p *phasePlaylists) producer() ppl.Producer[*model.Folder] { } func (p *phasePlaylists) produce(put func(entry *model.Folder)) error { + if !conf.Server.AutoImportPlaylists { + log.Info(p.ctx, "Playlists will not be imported, AutoImportPlaylists is set to false") + return nil + } u, _ := request.UserFrom(p.ctx) - if !conf.Server.AutoImportPlaylists || !u.IsAdmin { + if !u.IsAdmin { log.Warn(p.ctx, "Playlists will not be imported, as there are no admin users yet, "+ "Please create an admin user first, and then update the playlists for them to be imported") return nil From 3f9d17349594042997dd33835bf51552680b29ce Mon Sep 17 00:00:00 2001 From: Deluan Date: Sat, 22 Mar 2025 15:48:07 -0400 Subject: [PATCH 13/30] fix(scanner): support ID3v2 embedded images in WAV files Fix #3867 Signed-off-by: Deluan --- adapters/taglib/taglib_wrapper.cpp | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/adapters/taglib/taglib_wrapper.cpp b/adapters/taglib/taglib_wrapper.cpp index 188a8b7d7..4c5a9fa1e 100644 --- a/adapters/taglib/taglib_wrapper.cpp +++ b/adapters/taglib/taglib_wrapper.cpp @@ -201,41 +201,42 @@ int taglib_read(const FILENAME_CHAR_T *filename, unsigned long id) { char has_cover(const TagLib::FileRef f) { char hasCover = 0; // ----- MP3 - if (TagLib::MPEG::File * - mp3File{dynamic_cast(f.file())}) { + if (TagLib::MPEG::File * mp3File{dynamic_cast(f.file())}) { if (mp3File->ID3v2Tag()) { const auto &frameListMap{mp3File->ID3v2Tag()->frameListMap()}; hasCover = !frameListMap["APIC"].isEmpty(); } } // ----- FLAC - else if (TagLib::FLAC::File * - flacFile{dynamic_cast(f.file())}) { + else if (TagLib::FLAC::File * flacFile{dynamic_cast(f.file())}) { hasCover = !flacFile->pictureList().isEmpty(); } // ----- MP4 - else if (TagLib::MP4::File * - mp4File{dynamic_cast(f.file())}) { + else if (TagLib::MP4::File * mp4File{dynamic_cast(f.file())}) { auto &coverItem{mp4File->tag()->itemMap()["covr"]}; TagLib::MP4::CoverArtList coverArtList{coverItem.toCoverArtList()}; hasCover = !coverArtList.isEmpty(); } // ----- Ogg - else if (TagLib::Ogg::Vorbis::File * - vorbisFile{dynamic_cast(f.file())}) { + else if (TagLib::Ogg::Vorbis::File * vorbisFile{dynamic_cast(f.file())}) { hasCover = !vorbisFile->tag()->pictureList().isEmpty(); } // ----- Opus - else if (TagLib::Ogg::Opus::File * - opusFile{dynamic_cast(f.file())}) { + else if (TagLib::Ogg::Opus::File * opusFile{dynamic_cast(f.file())}) { hasCover = !opusFile->tag()->pictureList().isEmpty(); } // ----- WMA - if (TagLib::ASF::File * - asfFile{dynamic_cast(f.file())}) { + else if (TagLib::ASF::File * asfFile{dynamic_cast(f.file())}) { const TagLib::ASF::Tag *tag{asfFile->tag()}; hasCover = tag && tag->attributeListMap().contains("WM/Picture"); } + // ----- WAV + else if (TagLib::RIFF::WAV::File * wavFile{ dynamic_cast(f.file()) }) { + if (wavFile->hasID3v2Tag()) { + const auto& frameListMap{ wavFile->ID3v2Tag()->frameListMap() }; + hasCover = !frameListMap["APIC"].isEmpty(); + } + } return hasCover; } From 296259d781ff2ff12b9dbed3231164ad19fe4004 Mon Sep 17 00:00:00 2001 From: Deluan Date: Sat, 22 Mar 2025 15:48:29 -0400 Subject: [PATCH 14/30] feat(ui): show bitDepth in song info dialog Signed-off-by: Deluan --- resources/i18n/pt.json | 1 + ui/src/common/SongInfo.jsx | 3 ++- ui/src/i18n/en.json | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/resources/i18n/pt.json b/resources/i18n/pt.json index c3c65ca57..d856391ff 100644 --- a/resources/i18n/pt.json +++ b/resources/i18n/pt.json @@ -18,6 +18,7 @@ "size": "Tamanho", "updatedAt": "Últ. Atualização", "bitRate": "Bitrate", + "bitDepth": "Profundidade de bits", "discSubtitle": "Sub-título do disco", "starred": "Favorita", "comment": "Comentário", diff --git a/ui/src/common/SongInfo.jsx b/ui/src/common/SongInfo.jsx index d94685633..5adc1ebf0 100644 --- a/ui/src/common/SongInfo.jsx +++ b/ui/src/common/SongInfo.jsx @@ -74,6 +74,7 @@ export const SongInfo = (props) => { ), compilation: , bitRate: , + bitDepth: , channels: , size: , updatedAt: , @@ -91,7 +92,7 @@ export const SongInfo = (props) => { roles.push([name, record.participants[name].length]) } - const optionalFields = ['discSubtitle', 'comment', 'bpm', 'genre'] + const optionalFields = ['discSubtitle', 'comment', 'bpm', 'genre', 'bitDepth'] optionalFields.forEach((field) => { !record[field] && delete data[field] }) diff --git a/ui/src/i18n/en.json b/ui/src/i18n/en.json index cd377932c..678e42cd4 100644 --- a/ui/src/i18n/en.json +++ b/ui/src/i18n/en.json @@ -18,6 +18,7 @@ "size": "File size", "updatedAt": "Updated at", "bitRate": "Bit rate", + "bitDepth": "Bit depth", "channels": "Channels", "discSubtitle": "Disc Subtitle", "starred": "Favourite", From 264d73d73e2169b2e865e123ec52997593ce6492 Mon Sep 17 00:00:00 2001 From: Deluan Date: Sat, 22 Mar 2025 17:08:03 -0400 Subject: [PATCH 15/30] fix(server): don't break if the ND_CONFIGFILE does not exist Signed-off-by: Deluan --- conf/configuration.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/conf/configuration.go b/conf/configuration.go index 9abb7e163..6f4b2d594 100644 --- a/conf/configuration.go +++ b/conf/configuration.go @@ -577,9 +577,17 @@ func InitConfig(cfgFile string) { } } +// getConfigFile returns the path to the config file, either from the flag or from the environment variable. +// If it is defined in the environment variable, it will check if the file exists. func getConfigFile(cfgFile string) string { if cfgFile != "" { return cfgFile } - return os.Getenv("ND_CONFIGFILE") + cfgFile = os.Getenv("ND_CONFIGFILE") + if cfgFile != "" { + if _, err := os.Stat(cfgFile); err == nil { + return cfgFile + } + } + return "" } From 1c691ac0e6d4b30feb0e491d1268988180877558 Mon Sep 17 00:00:00 2001 From: Deluan Date: Sat, 22 Mar 2025 17:33:56 -0400 Subject: [PATCH 16/30] feat(docker): automatically loads a navidrome.toml file from /data, if available Signed-off-by: Deluan --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index 0ec64f769..4b4c3d18c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -133,6 +133,7 @@ COPY --from=build /out/navidrome /app/ VOLUME ["/data", "/music"] ENV ND_MUSICFOLDER=/music ENV ND_DATAFOLDER=/data +ENV ND_CONFIGFILE=/data/navidrome.toml ENV ND_PORT=4533 ENV GODEBUG="asyncpreemptoff=1" RUN touch /.nddockerenv From 57e0f6d3ea2212650c4716836d06fbc29d26405e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deluan=20Quint=C3=A3o?= Date: Sun, 23 Mar 2025 10:53:21 -0400 Subject: [PATCH 17/30] feat(server): custom ArtistJoiner config (#3873) * feat(server): custom ArtistJoiner config Signed-off-by: Deluan * refactor(ui): organize ArtistLinkField, add tests Signed-off-by: Deluan * feat(ui): use display artist * feat(ui): use display artist Signed-off-by: Deluan --------- Signed-off-by: Deluan --- conf/configuration.go | 2 + model/metadata/map_participants.go | 5 +- ui/src/common/ArtistLinkField.jsx | 78 +++++--- ui/src/common/ArtistLinkField.test.jsx | 238 +++++++++++++++++++++++++ 4 files changed, 297 insertions(+), 26 deletions(-) create mode 100644 ui/src/common/ArtistLinkField.test.jsx diff --git a/conf/configuration.go b/conf/configuration.go index 6f4b2d594..a75581fca 100644 --- a/conf/configuration.go +++ b/conf/configuration.go @@ -129,6 +129,7 @@ type scannerOptions struct { WatcherWait time.Duration ScanOnStartup bool Extractor string + ArtistJoiner string GenreSeparators string // Deprecated: Use Tags.genre.Split instead GroupAlbumReleases bool // Deprecated: Use PID.Album instead } @@ -495,6 +496,7 @@ func init() { viper.SetDefault("scanner.extractor", consts.DefaultScannerExtractor) viper.SetDefault("scanner.watcherwait", consts.DefaultWatcherWait) viper.SetDefault("scanner.scanonstartup", true) + viper.SetDefault("scanner.artistjoiner", consts.ArtistJoiner) viper.SetDefault("scanner.genreseparators", "") viper.SetDefault("scanner.groupalbumreleases", false) diff --git a/model/metadata/map_participants.go b/model/metadata/map_participants.go index a871f64fa..e8be6aaab 100644 --- a/model/metadata/map_participants.go +++ b/model/metadata/map_participants.go @@ -4,6 +4,7 @@ import ( "cmp" "strings" + "github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/consts" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/utils/str" @@ -210,8 +211,8 @@ func (md Metadata) getArtistValues(single, multi model.TagName) []string { func (md Metadata) mapDisplayName(singularTagName, pluralTagName model.TagName) string { return cmp.Or( - strings.Join(md.tags[singularTagName], consts.ArtistJoiner), - strings.Join(md.tags[pluralTagName], consts.ArtistJoiner), + strings.Join(md.tags[singularTagName], conf.Server.Scanner.ArtistJoiner), + strings.Join(md.tags[pluralTagName], conf.Server.Scanner.ArtistJoiner), ) } diff --git a/ui/src/common/ArtistLinkField.jsx b/ui/src/common/ArtistLinkField.jsx index 053cd25aa..60832eb40 100644 --- a/ui/src/common/ArtistLinkField.jsx +++ b/ui/src/common/ArtistLinkField.jsx @@ -63,38 +63,70 @@ const parseAndReplaceArtists = ( export const ArtistLinkField = ({ record, className, limit, source }) => { const role = source.toLowerCase() - const artists = record['participants'] - ? record['participants'][role] - : [{ name: record[source], id: record[source + 'Id'] }] - // When showing artists for a track, add any remixers to the list of artists - if ( - role === 'artist' && - record['participants'] && - record['participants']['remixer'] - ) { - record['participants']['remixer'].forEach((remixer) => { - artists.push(remixer) - }) - } + // Get artists array with fallback + let artists = record?.participants?.[role] || [] + const remixers = + role === 'artist' && record?.participants?.remixer + ? record.participants.remixer.slice(0, 2) + : [] - if (role === 'albumartist') { + // Use parseAndReplaceArtists for artist and albumartist roles + if ((role === 'artist' || role === 'albumartist') && record[source]) { const artistsLinks = parseAndReplaceArtists( record[source], artists, className, ) + if (artistsLinks.length > 0) { + // For artist role, append remixers if available, avoiding duplicates + if (role === 'artist' && remixers.length > 0) { + // Track which artists are already displayed to avoid duplicates + const displayedArtistIds = new Set( + artists.map((artist) => artist.id).filter(Boolean), + ) + + // Only add remixers that aren't already in the artists list + const uniqueRemixers = remixers.filter( + (remixer) => remixer.id && !displayedArtistIds.has(remixer.id), + ) + + if (uniqueRemixers.length > 0) { + artistsLinks.push(' • ') + uniqueRemixers.forEach((remixer, index) => { + if (index > 0) artistsLinks.push(' • ') + artistsLinks.push( + , + ) + }) + } + } + return
{artistsLinks}
} } - // Dedupe artists, only shows the first 3 + // Fall back to regular handling + if (artists.length === 0 && record[source]) { + artists = [{ name: record[source], id: record[source + 'Id'] }] + } + + // For artist role, combine artists and remixers before deduplication + const allArtists = role === 'artist' ? [...artists, ...remixers] : artists + + // Dedupe artists and collect subroles const seen = new Map() const dedupedArtists = [] let limitedShow = false - for (const artist of artists ?? []) { + for (const artist of allArtists) { + if (!artist?.id) continue + if (!seen.has(artist.id)) { if (dedupedArtists.length < limit) { seen.set(artist.id, dedupedArtists.length) @@ -107,22 +139,20 @@ export const ArtistLinkField = ({ record, className, limit, source }) => { } } else { const position = seen.get(artist.id) - - if (position !== -1) { - const existing = dedupedArtists[position] - if (artist.subRole && !existing.subroles.includes(artist.subRole)) { - existing.subroles.push(artist.subRole) - } + const existing = dedupedArtists[position] + if (artist.subRole && !existing.subroles.includes(artist.subRole)) { + existing.subroles.push(artist.subRole) } } } + // Create artist links const artistsList = dedupedArtists.map((artist) => ( - + )) if (limitedShow) { - artistsList.push(...) + artistsList.push(...) } return <>{intersperse(artistsList, ' • ')} diff --git a/ui/src/common/ArtistLinkField.test.jsx b/ui/src/common/ArtistLinkField.test.jsx new file mode 100644 index 000000000..09fdf64a4 --- /dev/null +++ b/ui/src/common/ArtistLinkField.test.jsx @@ -0,0 +1,238 @@ +import React from 'react' +import { render, screen } from '@testing-library/react' +import { describe, it, expect, beforeEach, vi } from 'vitest' +import { ArtistLinkField } from './ArtistLinkField' +import { intersperse } from '../utils/index.js' + +// Mock dependencies +vi.mock('react-redux', () => ({ + useDispatch: vi.fn(() => vi.fn()), +})) + +vi.mock('./useGetHandleArtistClick', () => ({ + useGetHandleArtistClick: vi.fn(() => (id) => `/artist/${id}`), +})) + +vi.mock('../utils/index.js', () => ({ + intersperse: vi.fn((arr) => arr), +})) + +vi.mock('@material-ui/core', () => ({ + withWidth: () => (Component) => { + const WithWidthComponent = (props) => + WithWidthComponent.displayName = `WithWidth(${Component.displayName || Component.name || 'Component'})` + return WithWidthComponent + }, +})) + +vi.mock('react-admin', () => ({ + Link: ({ children, to, ...props }) => ( + + {children} + + ), +})) + +describe('ArtistLinkField', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + describe('when rendering artists', () => { + it('renders artists from participants when available', () => { + const record = { + participants: { + artist: [ + { id: '1', name: 'Artist 1' }, + { id: '2', name: 'Artist 2' }, + ], + }, + } + + render() + + expect(screen.getByText('Artist 1')).toBeInTheDocument() + expect(screen.getByText('Artist 2')).toBeInTheDocument() + }) + + it('falls back to record[source] when participants not available', () => { + const record = { + artist: 'Fallback Artist', + artistId: '123', + } + + render() + + expect(screen.getByText('Fallback Artist')).toBeInTheDocument() + }) + + it('handles empty artists array', () => { + const record = { + participants: { + artist: [], + }, + } + + render() + + expect(intersperse).toHaveBeenCalledWith([], ' • ') + }) + }) + + describe('when handling remixers', () => { + it('adds remixers when showing artist role', () => { + const record = { + participants: { + artist: [{ id: '1', name: 'Artist 1' }], + remixer: [{ id: '2', name: 'Remixer 1' }], + }, + } + + render() + + expect(screen.getByText('Artist 1')).toBeInTheDocument() + expect(screen.getByText('Remixer 1')).toBeInTheDocument() + }) + + it('limits remixers to maximum of 2', () => { + const record = { + participants: { + artist: [{ id: '1', name: 'Artist 1' }], + remixer: [ + { id: '2', name: 'Remixer 1' }, + { id: '3', name: 'Remixer 2' }, + { id: '4', name: 'Remixer 3' }, + ], + }, + } + + render() + + expect(screen.getByText('Artist 1')).toBeInTheDocument() + expect(screen.getByText('Remixer 1')).toBeInTheDocument() + expect(screen.getByText('Remixer 2')).toBeInTheDocument() + expect(screen.queryByText('Remixer 3')).not.toBeInTheDocument() + }) + + it('deduplicates artists and remixers', () => { + const record = { + participants: { + artist: [{ id: '1', name: 'Duplicate Person' }], + remixer: [{ id: '1', name: 'Duplicate Person' }], + }, + } + + render() + + const links = screen.getAllByRole('link') + expect(links).toHaveLength(1) + expect(links[0]).toHaveTextContent('Duplicate Person') + }) + }) + + describe('when using parseAndReplaceArtists', () => { + it('uses parseAndReplaceArtists when role is albumartist', () => { + const record = { + albumArtist: 'Group Artist', + participants: { + albumartist: [{ id: '1', name: 'Group Artist' }], + }, + } + + render() + + expect(screen.getByText('Group Artist')).toBeInTheDocument() + expect(screen.getByRole('link')).toHaveAttribute('href', '/artist/1') + }) + + it('uses parseAndReplaceArtists when role is artist', () => { + const record = { + artist: 'Main Artist', + participants: { + artist: [{ id: '1', name: 'Main Artist' }], + }, + } + + render() + + expect(screen.getByText('Main Artist')).toBeInTheDocument() + expect(screen.getByRole('link')).toHaveAttribute('href', '/artist/1') + }) + + it('adds remixers after parseAndReplaceArtists for artist role', () => { + const record = { + artist: 'Main Artist', + participants: { + artist: [{ id: '1', name: 'Main Artist' }], + remixer: [{ id: '2', name: 'Remixer 1' }], + }, + } + + render() + + const links = screen.getAllByRole('link') + expect(links).toHaveLength(2) + expect(links[0]).toHaveAttribute('href', '/artist/1') + expect(links[1]).toHaveAttribute('href', '/artist/2') + }) + }) + + describe('when handling artist deduplication', () => { + it('deduplicates artists with the same id', () => { + const record = { + participants: { + artist: [ + { id: '1', name: 'Duplicate Artist' }, + { id: '1', name: 'Duplicate Artist', subRole: 'Vocals' }, + ], + }, + } + + render() + + const links = screen.getAllByRole('link') + expect(links).toHaveLength(1) + expect(links[0]).toHaveTextContent('Duplicate Artist (Vocals)') + }) + + it('aggregates subroles for the same artist', () => { + const record = { + participants: { + artist: [ + { id: '1', name: 'Multi-Role Artist', subRole: 'Vocals' }, + { id: '1', name: 'Multi-Role Artist', subRole: 'Guitar' }, + ], + }, + } + + render() + + expect( + screen.getByText('Multi-Role Artist (Vocals, Guitar)'), + ).toBeInTheDocument() + }) + }) + + describe('when limiting displayed artists', () => { + it('limits the number of artists displayed', () => { + const record = { + participants: { + artist: [ + { id: '1', name: 'Artist 1' }, + { id: '2', name: 'Artist 2' }, + { id: '3', name: 'Artist 3' }, + { id: '4', name: 'Artist 4' }, + ], + }, + } + + render() + + expect(screen.getByText('Artist 1')).toBeInTheDocument() + expect(screen.getByText('Artist 2')).toBeInTheDocument() + expect(screen.getByText('Artist 3')).toBeInTheDocument() + expect(screen.queryByText('Artist 4')).not.toBeInTheDocument() + expect(screen.getByText('...')).toBeInTheDocument() + }) + }) +}) From 223e88d481b4b302b15bdccc464bf5615600e54f Mon Sep 17 00:00:00 2001 From: Deluan Date: Sun, 23 Mar 2025 11:37:20 -0400 Subject: [PATCH 18/30] chore: remove some BFR-related TODOs that are not valid anymore Signed-off-by: Deluan --- conf/configuration.go | 1 - core/artwork/artwork_internal_test.go | 2 +- core/scrobbler/play_tracker_test.go | 1 - model/album.go | 2 +- model/mediafile.go | 4 ++-- persistence/album_repository.go | 1 - persistence/artist_repository.go | 3 +-- persistence/mediafile_repository.go | 1 - persistence/persistence_suite_test.go | 7 ------- persistence/playlist_repository_test.go | 2 +- scanner/controller.go | 1 - server/subsonic/helpers.go | 1 - 12 files changed, 6 insertions(+), 20 deletions(-) diff --git a/conf/configuration.go b/conf/configuration.go index a75581fca..08008105d 100644 --- a/conf/configuration.go +++ b/conf/configuration.go @@ -306,7 +306,6 @@ func Load(noConfigDump bool) { disableExternalServices() } - // BFR Remove before release if Server.Scanner.Extractor != consts.DefaultScannerExtractor { log.Warn(fmt.Sprintf("Extractor '%s' is not implemented, using 'taglib'", Server.Scanner.Extractor)) Server.Scanner.Extractor = consts.DefaultScannerExtractor diff --git a/core/artwork/artwork_internal_test.go b/core/artwork/artwork_internal_test.go index 65228ace5..462027082 100644 --- a/core/artwork/artwork_internal_test.go +++ b/core/artwork/artwork_internal_test.go @@ -15,7 +15,7 @@ import ( . "github.com/onsi/gomega" ) -// BFR Fix tests +// TODO Fix tests var _ = XDescribe("Artwork", func() { var aw *artwork var ds model.DataStore diff --git a/core/scrobbler/play_tracker_test.go b/core/scrobbler/play_tracker_test.go index a4bd7cec2..da1f96864 100644 --- a/core/scrobbler/play_tracker_test.go +++ b/core/scrobbler/play_tracker_test.go @@ -239,7 +239,6 @@ func (f *fakeScrobbler) Scrobble(ctx context.Context, userId string, s Scrobble) return nil } -// BFR This is duplicated in a few places func _p(id, name string, sortName ...string) model.Participant { p := model.Participant{Artist: model.Artist{ID: id, Name: name}} if len(sortName) > 0 { diff --git a/model/album.go b/model/album.go index 4ac976e24..c9dc022cb 100644 --- a/model/album.go +++ b/model/album.go @@ -17,7 +17,7 @@ type Album struct { Name string `structs:"name" json:"name"` EmbedArtPath string `structs:"embed_art_path" json:"-"` AlbumArtistID string `structs:"album_artist_id" json:"albumArtistId"` // Deprecated, use Participants - // BFR Rename to AlbumArtistDisplayName + // AlbumArtist is the display name used for the album artist. AlbumArtist string `structs:"album_artist" json:"albumArtist"` MaxYear int `structs:"max_year" json:"maxYear"` MinYear int `structs:"min_year" json:"minYear"` diff --git a/model/mediafile.go b/model/mediafile.go index 795657466..896442436 100644 --- a/model/mediafile.go +++ b/model/mediafile.go @@ -31,10 +31,10 @@ type MediaFile struct { Title string `structs:"title" json:"title"` Album string `structs:"album" json:"album"` ArtistID string `structs:"artist_id" json:"artistId"` // Deprecated: Use Participants instead - // BFR Rename to ArtistDisplayName + // Artist is the display name used for the artist. Artist string `structs:"artist" json:"artist"` AlbumArtistID string `structs:"album_artist_id" json:"albumArtistId"` // Deprecated: Use Participants instead - // BFR Rename to AlbumArtistDisplayName + // AlbumArtist is the display name used for the album artist. AlbumArtist string `structs:"album_artist" json:"albumArtist"` AlbumID string `structs:"album_id" json:"albumId"` HasCoverArt bool `structs:"has_cover_art" json:"hasCoverArt"` diff --git a/persistence/album_repository.go b/persistence/album_repository.go index b29a44701..0f2a46dec 100644 --- a/persistence/album_repository.go +++ b/persistence/album_repository.go @@ -184,7 +184,6 @@ func allRolesFilter(_ string, value interface{}) Sqlizer { func (r *albumRepository) CountAll(options ...model.QueryOptions) (int64, error) { sql := r.newSelect() sql = r.withAnnotation(sql, "album.id") - // BFR WithParticipants (for filtering by name)? return r.count(sql, options...) } diff --git a/persistence/artist_repository.go b/persistence/artist_repository.go index dd3f31b00..7602be381 100644 --- a/persistence/artist_repository.go +++ b/persistence/artist_repository.go @@ -85,7 +85,7 @@ func (a *dbArtist) PostMapArgs(m map[string]any) error { m["full_text"] = formatFullText(a.Name, a.SortArtistName) // Do not override the sort_artist_name and mbz_artist_id fields if they are empty - // BFR: Better way to handle this? + // TODO: Better way to handle this? if v, ok := m["sort_artist_name"]; !ok || v.(string) == "" { delete(m, "sort_artist_name") } @@ -134,7 +134,6 @@ func roleFilter(_ string, role any) Sqlizer { func (r *artistRepository) selectArtist(options ...model.QueryOptions) SelectBuilder { query := r.newSelect(options...).Columns("artist.*") query = r.withAnnotation(query, "artist.id") - // BFR How to handle counts and sizes (per role)? return query } diff --git a/persistence/mediafile_repository.go b/persistence/mediafile_repository.go index ef4507877..ebf07ce17 100644 --- a/persistence/mediafile_repository.go +++ b/persistence/mediafile_repository.go @@ -105,7 +105,6 @@ var mediaFileFilter = sync.OnceValue(func() map[string]filterFunc { func (r *mediaFileRepository) CountAll(options ...model.QueryOptions) (int64, error) { query := r.newSelect() query = r.withAnnotation(query, "media_file.id") - // BFR WithParticipants (for filtering by name)? return r.count(query, options...) } diff --git a/persistence/persistence_suite_test.go b/persistence/persistence_suite_test.go index 609904b49..43e4c292b 100644 --- a/persistence/persistence_suite_test.go +++ b/persistence/persistence_suite_test.go @@ -29,13 +29,6 @@ func TestPersistence(t *testing.T) { RunSpecs(t, "Persistence Suite") } -// BFR Test tags -//var ( -// genreElectronic = model.Genre{ID: "gn-1", Name: "Electronic"} -// genreRock = model.Genre{ID: "gn-2", Name: "Rock"} -// testGenres = model.Genres{genreElectronic, genreRock} -//) - func mf(mf model.MediaFile) model.MediaFile { mf.Tags = model.Tags{} mf.LibraryID = 1 diff --git a/persistence/playlist_repository_test.go b/persistence/playlist_repository_test.go index 85a87ece7..5a82964c9 100644 --- a/persistence/playlist_repository_test.go +++ b/persistence/playlist_repository_test.go @@ -145,7 +145,7 @@ var _ = Describe("PlaylistRepository", func() { }) }) - // BFR Validate these tests + // TODO Validate these tests XContext("child smart playlists", func() { When("refresh day has expired", func() { It("should refresh tracks for smart playlist referenced in parent smart playlist criteria", func() { diff --git a/scanner/controller.go b/scanner/controller.go index 84ea8e606..e3e008483 100644 --- a/scanner/controller.go +++ b/scanner/controller.go @@ -98,7 +98,6 @@ type ProgressInfo struct { type scanner interface { scanAll(ctx context.Context, fullScan bool, progress chan<- *ProgressInfo) - // BFR: scanFolders(ctx context.Context, lib model.Lib, folders []string, progress chan<- *ScannerStatus) } type controller struct { diff --git a/server/subsonic/helpers.go b/server/subsonic/helpers.go index fa98a985b..56b65f894 100644 --- a/server/subsonic/helpers.go +++ b/server/subsonic/helpers.go @@ -235,7 +235,6 @@ func osChildFromMediaFile(ctx context.Context, mf model.MediaFile) *responses.Op child.BitDepth = int32(mf.BitDepth) child.Genres = toItemGenres(mf.Genres) child.Moods = mf.Tags.Values(model.TagMood) - // BFR What if Child is an Album and not a Song? child.DisplayArtist = mf.Artist child.Artists = artistRefs(mf.Participants[model.RoleArtist]) child.DisplayAlbumArtist = mf.AlbumArtist From 1806552ef67994ddbf79ca35c223a1739bfb3a81 Mon Sep 17 00:00:00 2001 From: Deluan Date: Sun, 23 Mar 2025 11:53:43 -0400 Subject: [PATCH 19/30] chore: remove more outdated TODOs Signed-off-by: Deluan --- model/criteria/operators_test.go | 1 - model/player.go | 1 - server/subsonic/browsing.go | 4 +--- 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/model/criteria/operators_test.go b/model/criteria/operators_test.go index 575b9c3f8..e6082de44 100644 --- a/model/criteria/operators_test.go +++ b/model/criteria/operators_test.go @@ -46,7 +46,6 @@ var _ = Describe("Operators", func() { Entry("notInPlaylist", NotInPlaylist{"id": "deadbeef-dead-beef"}, "media_file.id NOT IN "+ "(SELECT media_file_id FROM playlist_tracks pl LEFT JOIN playlist on pl.playlist_id = playlist.id WHERE (pl.playlist_id = ? AND playlist.public = ?))", "deadbeef-dead-beef", 1), - // TODO These may be flaky Entry("inTheLast", InTheLast{"lastPlayed": 30}, "annotation.play_date > ?", StartOfPeriod(30, time.Now())), Entry("notInTheLast", NotInTheLast{"lastPlayed": 30}, "(annotation.play_date < ? OR annotation.play_date IS NULL)", StartOfPeriod(30, time.Now())), diff --git a/model/player.go b/model/player.go index ee7346b66..39ea99d1a 100644 --- a/model/player.go +++ b/model/player.go @@ -28,5 +28,4 @@ type PlayerRepository interface { Put(p *Player) error CountAll(...QueryOptions) (int64, error) CountByClient(...QueryOptions) (map[string]int64, error) - // TODO: Add CountAll method. Useful at least for metrics. } diff --git a/server/subsonic/browsing.go b/server/subsonic/browsing.go index d46c7937d..edc45a7c7 100644 --- a/server/subsonic/browsing.go +++ b/server/subsonic/browsing.go @@ -252,9 +252,7 @@ func (api *Router) GetSong(r *http.Request) (*responses.Subsonic, error) { func (api *Router) GetGenres(r *http.Request) (*responses.Subsonic, error) { ctx := r.Context() - // TODO Put back when album_count is available - //genres, err := api.ds.Genre(ctx).GetAll(model.QueryOptions{Sort: "song_count, album_count, name desc", Order: "desc"}) - genres, err := api.ds.Genre(ctx).GetAll(model.QueryOptions{Sort: "song_count, name desc", Order: "desc"}) + genres, err := api.ds.Genre(ctx).GetAll(model.QueryOptions{Sort: "song_count, album_count, name desc", Order: "desc"}) if err != nil { log.Error(r, err) return nil, err From 3a0ce6aafa53f87b29e82ff526785215e41c86d8 Mon Sep 17 00:00:00 2001 From: Deluan Date: Sun, 23 Mar 2025 12:36:38 -0400 Subject: [PATCH 20/30] fix(scanner): elapsed time for folder processing is wrong in the logs Signed-off-by: Deluan --- scanner/walk_dir_tree.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scanner/walk_dir_tree.go b/scanner/walk_dir_tree.go index 323ba0392..ba87f2628 100644 --- a/scanner/walk_dir_tree.go +++ b/scanner/walk_dir_tree.go @@ -70,7 +70,6 @@ func newFolderEntry(job *scanJob, path string) *folderEntry { albumIDMap: make(map[string]string), updTime: job.popLastUpdate(id), } - f.elapsed.Start() return f } @@ -115,6 +114,8 @@ func walkFolder(ctx context.Context, job *scanJob, currentFolder string, ignoreP "images", maps.Keys(folder.imageFiles), "playlists", folder.numPlaylists, "imagesUpdatedAt", folder.imagesUpdatedAt, "updTime", folder.updTime, "modTime", folder.modTime, "numChildren", len(children)) folder.path = dir + folder.elapsed.Start() + results <- folder return nil From d331ee904b06fbc7ef4ecc54044f0f2ad3a61c9d Mon Sep 17 00:00:00 2001 From: Deluan Date: Mon, 24 Mar 2025 15:08:17 -0400 Subject: [PATCH 21/30] fix(ui): sort playlist by `year` fix #3878 Signed-off-by: Deluan --- persistence/playlist_track_repository.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/persistence/playlist_track_repository.go b/persistence/playlist_track_repository.go index 69a2449c6..ea1977f3d 100644 --- a/persistence/playlist_track_repository.go +++ b/persistence/playlist_track_repository.go @@ -51,11 +51,13 @@ func (r *playlistRepository) Tracks(playlistId string, refreshSmartPlaylist bool }) p.setSortMappings( map[string]string{ - "id": "playlist_tracks.id", - "artist": "order_artist_name", - "album": "order_album_name, order_album_artist_name", - "title": "order_title", - "duration": "duration", // To make sure the field will be whitelisted + "id": "playlist_tracks.id", + "artist": "order_artist_name", + "album": "order_album_name, order_album_artist_name", + "title": "order_title", + // To make sure these fields will be whitelisted + "duration": "duration", + "year": "year", }, "f") // TODO I don't like this solution, but I won't change it now as it's not the focus of BFR. From 55ce28b2c63599c38bd0eae066caa457b6605129 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deluan=20Quint=C3=A3o?= Date: Mon, 24 Mar 2025 15:22:59 -0400 Subject: [PATCH 22/30] fix(bfr): force upgrade to read all folders. (#3871) * chore(scanner): add trace logs Signed-off-by: Deluan * fix(bfr): force upgrade to read all folders. It was skipping folders for certain timezones Signed-off-by: Deluan --------- Signed-off-by: Deluan --- db/migrations/20241026183640_support_new_scanner.go | 4 +++- scanner/phase_1_folders.go | 10 ++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/db/migrations/20241026183640_support_new_scanner.go b/db/migrations/20241026183640_support_new_scanner.go index 3e7c47f54..251b27f63 100644 --- a/db/migrations/20241026183640_support_new_scanner.go +++ b/db/migrations/20241026183640_support_new_scanner.go @@ -164,7 +164,9 @@ join library on media_file.library_id = library.id`, string(os.PathSeparator))) return nil } - stmt, err := tx.PrepareContext(ctx, "insert into folder (id, library_id, path, name, parent_id) values (?, ?, ?, ?, ?)") + stmt, err := tx.PrepareContext(ctx, + "insert into folder (id, library_id, path, name, parent_id, updated_at) values (?, ?, ?, ?, ?, '0000-00-00 00:00:00')", + ) if err != nil { return err } diff --git a/scanner/phase_1_folders.go b/scanner/phase_1_folders.go index ca7851f17..ae0d906de 100644 --- a/scanner/phase_1_folders.go +++ b/scanner/phase_1_folders.go @@ -150,6 +150,14 @@ func (p *phaseFolders) producer() ppl.Producer[*folderEntry] { Path: folder.path, Phase: "1", }) + + // Log folder info + log.Trace(p.ctx, "Scanner: Checking folder state", " folder", folder.path, "_updTime", folder.updTime, + "_modTime", folder.modTime, "_lastScanStartedAt", folder.job.lib.LastScanStartedAt, + "numAudioFiles", len(folder.audioFiles), "numImageFiles", len(folder.imageFiles), + "numPlaylists", folder.numPlaylists, "numSubfolders", folder.numSubFolders) + + // Check if folder is outdated if folder.isOutdated() { if !p.state.fullScan { if folder.hasNoFiles() && folder.isNew() { @@ -161,6 +169,8 @@ func (p *phaseFolders) producer() ppl.Producer[*folderEntry] { totalChanged++ folder.elapsed.Stop() put(folder) + } else { + log.Trace(p.ctx, "Scanner: Skipping up-to-date folder", "folder", folder.path, "lastUpdate", folder.modTime, "lib", job.lib.Name) } } total += job.numFolders.Load() From 651ce163c70fee1141b3d75aea289ca54655bb3e Mon Sep 17 00:00:00 2001 From: Deluan Date: Mon, 24 Mar 2025 16:41:54 -0400 Subject: [PATCH 23/30] fix(ui): sort playlist by `album_artist`, `bpm` and `channels` fix #3878 Signed-off-by: Deluan --- persistence/playlist_track_repository.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/persistence/playlist_track_repository.go b/persistence/playlist_track_repository.go index ea1977f3d..d33bd5113 100644 --- a/persistence/playlist_track_repository.go +++ b/persistence/playlist_track_repository.go @@ -51,13 +51,16 @@ func (r *playlistRepository) Tracks(playlistId string, refreshSmartPlaylist bool }) p.setSortMappings( map[string]string{ - "id": "playlist_tracks.id", - "artist": "order_artist_name", - "album": "order_album_name, order_album_artist_name", - "title": "order_title", + "id": "playlist_tracks.id", + "artist": "order_artist_name", + "album_artist": "order_album_artist_name", + "album": "order_album_name, order_album_artist_name", + "title": "order_title", // To make sure these fields will be whitelisted "duration": "duration", "year": "year", + "bpm": "bpm", + "channels": "channels", }, "f") // TODO I don't like this solution, but I won't change it now as it's not the focus of BFR. From 9e9465567d4e4cd1e18a09d99786e1c91ee64178 Mon Sep 17 00:00:00 2001 From: matteo00gm <76428629+matteo00gm@users.noreply.github.com> Date: Mon, 24 Mar 2025 22:49:23 +0100 Subject: [PATCH 24/30] fix(ui): update Italian translations (#3885) --- resources/i18n/it.json | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/resources/i18n/it.json b/resources/i18n/it.json index edc3cc69e..aaaa2f8c2 100644 --- a/resources/i18n/it.json +++ b/resources/i18n/it.json @@ -53,12 +53,12 @@ "updatedAt": "Ultimo aggiornamento", "comment": "Commento", "rating": "Valutazione", - "createdAt": "", - "size": "", + "createdAt": "Data di creazione", + "size": "Dimensione", "originalDate": "", - "releaseDate": "", - "releases": "", - "released": "" + "releaseDate": "Data di pubblicazione", + "releases": "Pubblicazione |||| Pubblicazioni", + "released": "Pubblicato" }, "actions": { "playAll": "Riproduci", @@ -68,7 +68,7 @@ "addToPlaylist": "Aggiungi alla Playlist", "download": "Scarica", "info": "Informazioni", - "share": "" + "share": "Condividi" }, "lists": { "all": "Tutti", @@ -89,7 +89,7 @@ "playCount": "Riproduzioni", "rating": "Valutazione", "genre": "Genere", - "size": "" + "size": "Dimensione" } }, "user": { @@ -160,8 +160,8 @@ "selectPlaylist": "Aggiungi tracce alla playlist:", "addNewPlaylist": "Aggiungi \"%{name}\"", "export": "Esporta", - "makePublic": "", - "makePrivate": "" + "makePublic": "Rendi Pubblica", + "makePrivate": "Rendi Privata" }, "message": { "duplicate_song": "Aggiungere i duplicati", @@ -169,9 +169,9 @@ } }, "radio": { - "name": "", + "name": "Radio |||| Radio", "fields": { - "name": "", + "name": "Nome", "streamUrl": "", "homePageUrl": "", "updatedAt": "", From c837838d58c03e7dc0ed6e24736623d7b3b277d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deluan=20Quint=C3=A3o?= Date: Mon, 24 Mar 2025 17:52:03 -0400 Subject: [PATCH 25/30] fix(ui): update French, Polish, Turkish translations from POEditor (#3834) Co-authored-by: navidrome-bot --- resources/i18n/fr.json | 68 +++++++++++++++++++++--------------------- resources/i18n/pl.json | 54 ++++++++++++++++++++++++++++++--- resources/i18n/tr.json | 6 ++-- 3 files changed, 88 insertions(+), 40 deletions(-) diff --git a/resources/i18n/fr.json b/resources/i18n/fr.json index 50bc0d449..4060a789d 100644 --- a/resources/i18n/fr.json +++ b/resources/i18n/fr.json @@ -25,13 +25,15 @@ "quality": "Qualité", "bpm": "BPM", "playDate": "Derniers joués", + "channels": "Canaux", "createdAt": "Date d'ajout", "grouping": "Regroupement", "mood": "Humeur", "participants": "Participants supplémentaires", "tags": "Étiquettes supplémentaires", "mappedTags": "Étiquettes correspondantes", - "rawTags": "Étiquettes brutes" + "rawTags": "Étiquettes brutes", + "bitDepth": "Profondeur de bit" }, "actions": { "addToQueue": "Ajouter à la file", @@ -51,19 +53,19 @@ "duration": "Durée", "songCount": "Nombre de pistes", "playCount": "Nombre d'écoutes", - "size": "Taille", "name": "Nom", "genre": "Genre", "compilation": "Compilation", "year": "Année", - "originalDate": "Original", - "releaseDate": "Sortie", - "releases": "Sortie |||| Sorties", - "released": "Sortie", "updatedAt": "Mis à jour le", "comment": "Commentaire", "rating": "Classement", "createdAt": "Date d'ajout", + "size": "Taille", + "originalDate": "Original", + "releaseDate": "Sortie", + "releases": "Sortie |||| Sorties", + "released": "Sortie", "recordLabel": "Label", "catalogNum": "Numéro de catalogue", "releaseType": "Type", @@ -75,11 +77,11 @@ "playAll": "Lire", "playNext": "Lire ensuite", "addToQueue": "Ajouter à la file", - "share": "Partager", "shuffle": "Mélanger", "addToPlaylist": "Ajouter à la playlist", "download": "Télécharger", - "info": "Plus d'informations" + "info": "Plus d'informations", + "share": "Partager" }, "lists": { "all": "Tous", @@ -97,10 +99,10 @@ "name": "Nom", "albumCount": "Nombre d'albums", "songCount": "Nombre de pistes", - "size": "Taille", "playCount": "Lectures", "rating": "Classement", "genre": "Genre", + "size": "Taille", "role": "Rôle" }, "roles": { @@ -125,7 +127,6 @@ "userName": "Nom d'utilisateur", "isAdmin": "Administrateur", "lastLoginAt": "Dernière connexion", - "lastAccessAt": "Dernier accès", "updatedAt": "Dernière mise à jour", "name": "Nom", "password": "Mot de passe", @@ -133,7 +134,8 @@ "changePassword": "Changer le mot de passe ?", "currentPassword": "Mot de passe actuel", "newPassword": "Nouveau mot de passe", - "token": "Token" + "token": "Token", + "lastAccessAt": "Dernier accès" }, "helperTexts": { "name": "Les changements liés à votre nom ne seront reflétés qu'à la prochaine connexion" @@ -215,7 +217,6 @@ "username": "Partagé(e) par", "url": "Lien URL", "description": "Description", - "downloadable": "Autoriser les téléchargements ?", "contents": "Contenu", "expiresAt": "Expire le", "lastVisitedAt": "Visité pour la dernière fois", @@ -223,14 +224,12 @@ "format": "Format", "maxBitRate": "Bitrate maximum", "updatedAt": "Mis à jour le", - "createdAt": "Créé le" - }, - "notifications": {}, - "actions": {} + "createdAt": "Créé le", + "downloadable": "Autoriser les téléchargements ?" + } }, "missing": { "name": "Fichier manquant|||| Fichiers manquants", - "empty": "Aucun fichier manquant", "fields": { "path": "Chemin", "size": "Taille", @@ -241,7 +240,8 @@ }, "notifications": { "removed": "Fichier(s) manquant(s) supprimé(s)" - } + }, + "empty": "Aucun fichier manquant" } }, "ra": { @@ -279,7 +279,6 @@ "add": "Ajouter", "back": "Retour", "bulk_actions": "%{smart_count} sélectionné |||| %{smart_count} sélectionnés", - "bulk_actions_mobile": "1 |||| %{smart_count}", "cancel": "Annuler", "clear_input_value": "Vider le champ", "clone": "Dupliquer", @@ -303,6 +302,7 @@ "close_menu": "Fermer le menu", "unselect": "Désélectionner", "skip": "Ignorer", + "bulk_actions_mobile": "1 |||| %{smart_count}", "share": "Partager", "download": "Télécharger" }, @@ -397,31 +397,31 @@ "noPlaylistsAvailable": "Aucune playlist", "delete_user_title": "Supprimer l'utilisateur '%{name}'", "delete_user_content": "Êtes-vous sûr(e) de vouloir supprimer cet utilisateur et ses données associées (y compris ses playlists et préférences) ?", - "remove_missing_title": "Supprimer les fichiers manquants", - "remove_missing_content": "Êtes-vous sûr(e) de vouloir supprimer les fichiers manquants sélectionnés de la base de données ? Ceci supprimera définiviement toute référence à ceux-ci, y compris leurs nombres d'écoutes et leurs notations.", "notifications_blocked": "Votre navigateur bloque les notifications de ce site", "notifications_not_available": "Votre navigateur ne permet pas d'afficher les notifications sur le bureau ou vous n'accédez pas à Navidrome via HTTPS", "lastfmLinkSuccess": "Last.fm a été correctement relié et le scrobble a été activé", "lastfmLinkFailure": "Last.fm n'a pas pu être correctement relié", "lastfmUnlinkSuccess": "Last.fm n'est plus relié et le scrobble a été désactivé", "lastfmUnlinkFailure": "Erreur pendant la suppression du lien avec Last.fm", - "listenBrainzLinkSuccess": "La liaison et le scrobble avec ListenBrainz sont maintenant activés pour l'utilisateur : %{user}", - "listenBrainzLinkFailure": "Échec lors de la liaison avec ListenBrainz : %{error}", - "listenBrainzUnlinkSuccess": "La liaison et le scrobble avec ListenBrainz sont maintenant désactivés", - "listenBrainzUnlinkFailure": "Échec lors de la désactivation de la liaison avec ListenBrainz", "openIn": { "lastfm": "Ouvrir dans Last.fm", "musicbrainz": "Ouvrir dans MusicBrainz" }, "lastfmLink": "Lire plus...", + "listenBrainzLinkSuccess": "La liaison et le scrobble avec ListenBrainz sont maintenant activés pour l'utilisateur : %{user}", + "listenBrainzLinkFailure": "Échec lors de la liaison avec ListenBrainz : %{error}", + "listenBrainzUnlinkSuccess": "La liaison et le scrobble avec ListenBrainz sont maintenant désactivés", + "listenBrainzUnlinkFailure": "Échec lors de la désactivation de la liaison avec ListenBrainz", + "downloadOriginalFormat": "Télécharger au format original", "shareOriginalFormat": "Partager avec le format original", "shareDialogTitle": "Partager %{resource} '%{name}'", "shareBatchDialogTitle": "Partager 1 %{resource} |||| Partager %{smart_count} %{resource}", - "shareCopyToClipboard": "Copier vers le presse-papier : Ctrl+C, Enter", "shareSuccess": "Lien copié vers le presse-papier : %{url}", "shareFailure": "Erreur en copiant le lien %{url} vers le presse-papier", "downloadDialogTitle": "Télécharger %{resource} '%{name}' (%{size})", - "downloadOriginalFormat": "Télécharger au format original" + "shareCopyToClipboard": "Copier vers le presse-papier : Ctrl+C, Enter", + "remove_missing_title": "Supprimer les fichiers manquants", + "remove_missing_content": "Êtes-vous sûr(e) de vouloir supprimer les fichiers manquants sélectionnés de la base de données ? Ceci supprimera définitivement toute référence à ceux-ci, y compris leurs nombres d'écoutes et leurs notations" }, "menu": { "library": "Bibliothèque", @@ -435,7 +435,6 @@ "language": "Langue", "defaultView": "Vue par défaut", "desktop_notifications": "Notifications de bureau", - "lastfmNotConfigured": "La clef API de Last.fm n'est pas configurée", "lastfmScrobbling": "Scrobbler vers Last.fm", "listenBrainzScrobbling": "Scrobbler vers ListenBrainz", "replaygain": "Mode ReplayGain", @@ -444,13 +443,14 @@ "none": "Désactivé", "album": "Utiliser le gain de l'album", "track": "Utiliser le gain des pistes" - } + }, + "lastfmNotConfigured": "La clef API de Last.fm n'est pas configurée" } }, "albumList": "Albums", + "about": "À propos", "playlists": "Playlists", - "sharedPlaylists": "Playlists partagées", - "about": "À propos" + "sharedPlaylists": "Playlists partagées" }, "player": { "playListsText": "File de lecture", @@ -505,10 +505,10 @@ "toggle_play": "Lecture/Pause", "prev_song": "Morceau précédent", "next_song": "Morceau suivant", - "current_song": "Aller à la chanson en cours", "vol_up": "Augmenter le volume", "vol_down": "Baisser le volume", - "toggle_love": "Ajouter/Enlever le morceau des favoris" + "toggle_love": "Ajouter/Enlever le morceau des favoris", + "current_song": "Aller à la chanson en cours" } } -} +} \ No newline at end of file diff --git a/resources/i18n/pl.json b/resources/i18n/pl.json index fe29f0e08..a9a128abc 100644 --- a/resources/i18n/pl.json +++ b/resources/i18n/pl.json @@ -26,7 +26,14 @@ "bpm": "BPM", "playDate": "Ostatnio Odtwarzane", "channels": "Kanały", - "createdAt": "Data dodania" + "createdAt": "Data dodania", + "grouping": "", + "mood": "", + "participants": "", + "tags": "", + "mappedTags": "", + "rawTags": "", + "bitDepth": "" }, "actions": { "addToQueue": "Odtwarzaj Później", @@ -58,7 +65,13 @@ "originalDate": "Pierwotna Data", "releaseDate": "Data Wydania", "releases": "Wydanie |||| Wydania", - "released": "Wydany" + "released": "Wydany", + "recordLabel": "", + "catalogNum": "", + "releaseType": "", + "grouping": "", + "media": "", + "mood": "" }, "actions": { "playAll": "Odtwarzaj", @@ -89,7 +102,23 @@ "playCount": "Liczba Odtworzeń", "rating": "Ocena", "genre": "Gatunek", - "size": "Rozmiar" + "size": "Rozmiar", + "role": "" + }, + "roles": { + "albumartist": "", + "artist": "", + "composer": "", + "conductor": "", + "lyricist": "", + "arranger": "", + "producer": "Producent |||| Producenci", + "director": "Reżyser |||| Reżyserzy", + "engineer": "Inżynier |||| Inżynierowie", + "mixer": "Mikser |||| Mikserzy", + "remixer": "Remixer |||| Remixerzy", + "djmixer": "Didżej |||| Didżerzy", + "performer": "Wykonawca |||| Wykonawcy" } }, "user": { @@ -198,6 +227,21 @@ "createdAt": "Stworzono", "downloadable": "Zezwolić Na Pobieranie?" } + }, + "missing": { + "name": "Brakujący Plik|||| Brakujące Pliki", + "fields": { + "path": "Ścieżka", + "size": "Rozmiar", + "updatedAt": "Zniknął na" + }, + "actions": { + "remove": "Usuń" + }, + "notifications": { + "removed": "Usunięto brakujące pliki" + }, + "empty": "" } }, "ra": { @@ -375,7 +419,9 @@ "shareSuccess": "Adres URL skopiowany do schowka: %{url}", "shareFailure": "Błąd podczas kopiowania URL %{url} do schowka", "downloadDialogTitle": "Pobierz %{resource} '%{name}' (%{size})", - "shareCopyToClipboard": "Skopiuj do schowka: Ctrl+C, Enter" + "shareCopyToClipboard": "Skopiuj do schowka: Ctrl+C, Enter", + "remove_missing_title": "Usuń brakujące dane", + "remove_missing_content": "Czy na pewno chcesz usunąć wybrane brakujące pliki z bazy danych? Spowoduje to trwałe usunięcie wszystkich powiązań, takich jak liczba odtworzeń i oceny." }, "menu": { "library": "Biblioteka", diff --git a/resources/i18n/tr.json b/resources/i18n/tr.json index f138f6730..2ae07b614 100644 --- a/resources/i18n/tr.json +++ b/resources/i18n/tr.json @@ -32,7 +32,8 @@ "participants": "Ek katılımcılar", "tags": "Ek Etiketler", "mappedTags": "Eşlenen etiketler", - "rawTags": "Ham etiketler" + "rawTags": "Ham etiketler", + "bitDepth": "" }, "actions": { "addToQueue": "Oynatma Sırasına Ekle", @@ -239,7 +240,8 @@ }, "notifications": { "removed": "Eksik dosya(lar) kaldırıldı" - } + }, + "empty": "Eksik Dosya Yok" } }, "ra": { From 112ea281d94d75036f82a2e75d95e4f21da864a4 Mon Sep 17 00:00:00 2001 From: Michachatz <121869403+michatec@users.noreply.github.com> Date: Tue, 25 Mar 2025 22:33:33 +0100 Subject: [PATCH 26/30] feat(ui): add Greek translation (#3892) Signed-off-by: Deluan --- resources/i18n/el.json | 514 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 514 insertions(+) create mode 100644 resources/i18n/el.json diff --git a/resources/i18n/el.json b/resources/i18n/el.json new file mode 100644 index 000000000..86ccf7c06 --- /dev/null +++ b/resources/i18n/el.json @@ -0,0 +1,514 @@ +{ + "languageName": "Ελληνικά", + "resources": { + "song": { + "name": "Τραγούδι |||| Τραγούδια", + "fields": { + "albumArtist": "Καλλιτεχνης Αλμπουμ", + "duration": "Διαρκεια", + "trackNumber": "#", + "playCount": "Αναπαραγωγες", + "title": "Τιτλος", + "artist": "Καλλιτεχνης", + "album": "Αλμπουμ", + "path": "Διαδρομη αρχειου", + "genre": "Ειδος", + "compilation": "Συλλογή", + "year": "Ετος", + "size": "Μεγεθος αρχειου", + "updatedAt": "Ενημερωθηκε", + "bitRate": "Ρυθμός Bit", + "discSubtitle": "Υπότιτλοι Δίσκου", + "starred": "Αγαπημένο", + "comment": "Σχόλιο", + "rating": "Βαθμολογια", + "quality": "Ποιοτητα", + "bpm": "BPM", + "playDate": "Παίχτηκε Τελευταία", + "channels": "Κανάλια", + "createdAt": "Ημερομηνία προσθήκης", + "grouping": "Ομαδοποίηση", + "mood": "Διάθεση", + "participants": "Πρόσθετοι συμμετέχοντες", + "tags": "Πρόσθετες Ετικέτες", + "mappedTags": "Χαρτογραφημένες ετικέτες", + "rawTags": "Ακατέργαστες ετικέτες", + "bitDepth": "Λίγο βάθος" + }, + "actions": { + "addToQueue": "Αναπαραγωγη Μετα", + "playNow": "Αναπαραγωγή Τώρα", + "addToPlaylist": "Προσθήκη στη λίστα αναπαραγωγής", + "shuffleAll": "Ανακατεμα ολων", + "download": "Ληψη", + "playNext": "Επόμενη Αναπαραγωγή", + "info": "Εμφάνιση Πληροφοριών" + } + }, + "album": { + "name": "Άλμπουμ |||| Άλμπουμ", + "fields": { + "albumArtist": "Καλλιτεχνης Αλμπουμ", + "artist": "Καλλιτεχνης", + "duration": "Διαρκεια", + "songCount": "Τραγουδια", + "playCount": "Αναπαραγωγες", + "name": "Ονομα", + "genre": "Ειδος", + "compilation": "Συλλογη", + "year": "Ετος", + "updatedAt": "Ενημερωθηκε", + "comment": "Σχόλιο", + "rating": "Βαθμολογια", + "createdAt": "Ημερομηνία προσθήκης", + "size": "Μέγεθος", + "originalDate": "Πρωτότυπο", + "releaseDate": "Κυκλοφόρησε", + "releases": "Έκδοση |||| Εκδόσεις", + "released": "Κυκλοφόρησε", + "recordLabel": "Επιγραφή", + "catalogNum": "Αριθμός καταλόγου", + "releaseType": "Τύπος", + "grouping": "Ομαδοποίηση", + "media": "Μέσα", + "mood": "Διάθεση" + }, + "actions": { + "playAll": "Αναπαραγωγή", + "playNext": "Αναπαραγωγη Μετα", + "addToQueue": "Αναπαραγωγη Αργοτερα", + "shuffle": "Ανακατεμα", + "addToPlaylist": "Προσθηκη στη λιστα αναπαραγωγης", + "download": "Ληψη", + "info": "Εμφάνιση Πληροφοριών", + "share": "Μερίδιο" + }, + "lists": { + "all": "Όλα", + "random": "Τυχαία", + "recentlyAdded": "Νέες Προσθήκες", + "recentlyPlayed": "Παίχτηκαν Πρόσφατα", + "mostPlayed": "Παίζονται Συχνά", + "starred": "Αγαπημένα", + "topRated": "Κορυφαία" + } + }, + "artist": { + "name": "Καλλιτέχνης |||| Καλλιτέχνες", + "fields": { + "name": "Ονομα", + "albumCount": "Αναπαραγωγές Αλμπουμ", + "songCount": "Αναπαραγωγες Τραγουδιου", + "playCount": "Αναπαραγωγες", + "rating": "Βαθμολογια", + "genre": "Είδος", + "size": "Μέγεθος", + "role": "Ρόλος" + }, + "roles": { + "albumartist": "Καλλιτέχνης Άλμπουμ |||| Καλλιτέχνες άλμπουμ", + "artist": "Καλλιτέχνης |||| Καλλιτέχνες", + "composer": "Συνθέτης |||| Συνθέτες", + "conductor": "Μαέστρος |||| Μαέστροι", + "lyricist": "Στιχουργός |||| Στιχουργοί", + "arranger": "Τακτοποιητής |||| Τακτοποιητές", + "producer": "Παραγωγός |||| Παραγωγοί", + "director": "Διευθυντής |||| Διευθυντές", + "engineer": "Μηχανικός |||| Μηχανικοί", + "mixer": "Μίξερ |||| Μίξερ", + "remixer": "Ρεμίξερ |||| Ρεμίξερ", + "djmixer": "Dj Μίξερ |||| Dj Μίξερ", + "performer": "Εκτελεστής |||| Ερμηνευτές" + } + }, + "user": { + "name": "Χρήστης |||| Χρήστες", + "fields": { + "userName": "Ονομα Χρηστη", + "isAdmin": "Ειναι Διαχειριστης", + "lastLoginAt": "Τελευταια συνδεση στις", + "updatedAt": "Ενημερωθηκε", + "name": "Όνομα", + "password": "Κωδικός Πρόσβασης", + "createdAt": "Δημιουργήθηκε στις", + "changePassword": "Αλλαγή Κωδικού Πρόσβασης;", + "currentPassword": "Υπάρχων Κωδικός Πρόσβασης", + "newPassword": "Νέος Κωδικός Πρόσβασης", + "token": "Token", + "lastAccessAt": "Τελευταία Πρόσβαση" + }, + "helperTexts": { + "name": "Αλλαγές στο όνομα σας θα εφαρμοστούν στην επόμενη σύνδεση" + }, + "notifications": { + "created": "Ο χρήστης δημιουργήθηκε", + "updated": "Ο χρήστης ενημερώθηκε", + "deleted": "Ο χρήστης διαγράφηκε" + }, + "message": { + "listenBrainzToken": "Εισάγετε το token του χρήστη σας στο ListenBrainz.", + "clickHereForToken": "Κάντε κλικ εδώ για να αποκτήσετε το token σας" + } + }, + "player": { + "name": "Συσκευή Αναπαραγωγής |||| Συσκευές Αναπαραγωγής", + "fields": { + "name": "Όνομα", + "transcodingId": "Διακωδικοποίηση", + "maxBitRate": "Μεγ. Ρυθμός Bit", + "client": "Πελάτης", + "userName": "Ονομα Χρηστη", + "lastSeen": "Τελευταια προβολη στις", + "reportRealPath": "Αναφορά Πραγματικής Διαδρομής", + "scrobbleEnabled": "Αποστολή scrobbles σε εξωτερικές συσκευές" + } + }, + "transcoding": { + "name": "Διακωδικοποίηση |||| Διακωδικοποιήσεις", + "fields": { + "name": "Όνομα", + "targetFormat": "Μορφη Προορισμου", + "defaultBitRate": "Προκαθορισμένος Ρυθμός Bit", + "command": "Εντολή" + } + }, + "playlist": { + "name": "Λίστα αναπαραγωγής |||| Λίστες αναπαραγωγής", + "fields": { + "name": "Όνομα", + "duration": "Διάρκεια", + "ownerName": "Ιδιοκτήτης", + "public": "Δημόσιο", + "updatedAt": "Ενημερωθηκε", + "createdAt": "Δημιουργήθηκε στις", + "songCount": "Τραγούδια", + "comment": "Σχόλιο", + "sync": "Αυτόματη εισαγωγή", + "path": "Εισαγωγή από" + }, + "actions": { + "selectPlaylist": "Επιλέξτε μια λίστα αναπαραγωγής:", + "addNewPlaylist": "Δημιουργία \"%{name}\"", + "export": "Εξαγωγη", + "makePublic": "Να γίνει δημόσιο", + "makePrivate": "Να γίνει ιδιωτικό" + }, + "message": { + "duplicate_song": "Προσθήκη διπλοεγγραφών τραγουδιών", + "song_exist": "Υπάρχουν διπλοεγγραφές στην λίστα αναπαραγωγής. Θέλετε να προστεθούν οι διπλοεγγραφές ή να τις παραβλέψετε;" + } + }, + "radio": { + "name": "Ραδιόφωνο ||| Ραδιόφωνο", + "fields": { + "name": "Όνομα", + "streamUrl": "Ρεύμα URL", + "homePageUrl": "Αρχική σελίδα URL", + "updatedAt": "Ενημερώθηκε στις", + "createdAt": "Δημιουργήθηκε στις" + }, + "actions": { + "playNow": "Αναπαραγωγή" + } + }, + "share": { + "name": "Μοιραστείτε |||| Μερίδια", + "fields": { + "username": "Κοινή χρήση από", + "url": "URL", + "description": "Περιγραφή", + "contents": "Περιεχόμενα", + "expiresAt": "Λήγει", + "lastVisitedAt": "Τελευταία Επίσκεψη", + "visitCount": "Επισκέψεις", + "format": "Μορφή", + "maxBitRate": "Μέγ. Ρυθμός Bit", + "updatedAt": "Ενημερώθηκε στις", + "createdAt": "Δημιουργήθηκε στις", + "downloadable": "Επιτρέπονται οι λήψεις?" + } + }, + "missing": { + "name": "Λείπει αρχείο |||| Λείπουν αρχεία", + "fields": { + "path": "Διαδρομή", + "size": "Μέγεθος", + "updatedAt": "Εξαφανίστηκε" + }, + "actions": { + "remove": "Αφαίρεση" + }, + "notifications": { + "removed": "Λείπει αρχείο(α) αφαιρέθηκε" + }, + "empty": "Δεν λείπουν αρχεία" + } + }, + "ra": { + "auth": { + "welcome1": "Σας ευχαριστούμε που εγκαταστήσατε το Navidrome!", + "welcome2": "Για να ξεκινήσετε, δημιουργήστε έναν χρήστη ως διαχειριστή", + "confirmPassword": "Επιβεβαίωση κωδικού πρόσβασης", + "buttonCreateAdmin": "Δημιουργία Διαχειριστή", + "auth_check_error": "Παρακαλούμε συνδεθείτε για να συννεχίσετε", + "user_menu": "Προφίλ", + "username": "Ονομα Χρηστη", + "password": "Κωδικός Πρόσβασης", + "sign_in": "Σύνδεση", + "sign_in_error": "Η αυθεντικοποίηση απέτυχε, παρακαλούμε προσπαθήστε ξανά", + "logout": "Αποσύνδεση", + "insightsCollectionNote": "Το Navidrome συλλέγει ανώνυμα δεδομένα χρήσης σε\nβοηθήσουν στη βελτίωση του έργου. Κάντε κλικ [εδώ] για να μάθετε\nπερισσότερα και να εξαιρεθείτε αν θέλετε" + }, + "validation": { + "invalidChars": "Παρακαλούμε χρησημοποιήστε μόνο γράμματα και αριθμούς", + "passwordDoesNotMatch": "Ο κωδικός πρόσβασης δεν ταιριάζει", + "required": "Υποχρεωτικό", + "minLength": "Πρέπει να είναι %{min} χαρακτήρες τουλάχιστον", + "maxLength": "Πρέπει να είναι %{max} χαρακτήρες ή λιγότερο", + "minValue": "Πρέπει να είναι τουλάχιστον %{min}", + "maxValue": "Πρέπει να είναι %{max} ή λιγότερο", + "number": "Πρέπει να είναι αριθμός", + "email": "Πρέπει να είναι ένα έγκυρο email", + "oneOf": "Πρέπει να είναι ένα από τα ακόλουθα: %{options}", + "regex": "Πρέπει να ταιριάζει με ένα συγκεκριμένο τύπο (κανονική έκφραση): %{pattern}", + "unique": "Πρέπει να είναι μοναδικό", + "url": "Πρέπει να είναι έγκυρη διεύθυνση URL" + }, + "action": { + "add_filter": "Προσθηκη φιλτρου", + "add": "Προσθήκη", + "back": "Πίσω", + "bulk_actions": "1 αντικείμενο επιλέχθηκε |||| %{smart_count} αντικείμενα επιλέχθηκαν", + "cancel": "Ακύρωση", + "clear_input_value": "Καθαρισμός τιμής", + "clone": "Κλωνοποίηση", + "confirm": "Επιβεβαίωση", + "create": "Δημιουργία", + "delete": "Διαγραφή", + "edit": "Επεξεργασία", + "export": "Εξαγωγη", + "list": "Λίστα", + "refresh": "Ανανέωση", + "remove_filter": "Αφαίρεση αυτού του φίλτρου", + "remove": "Αφαίρεση", + "save": "Αποθηκευση", + "search": "Αναζήτηση", + "show": "Προβολή", + "sort": "Ταξινόμιση", + "undo": "Αναίρεση", + "expand": "Επέκταση", + "close": "Κλείσιμο", + "open_menu": "Άνοιγμα μενού", + "close_menu": "Κλείσιμο μενού", + "unselect": "Αποεπιλογή", + "skip": "Παράβλεψη", + "bulk_actions_mobile": "1 |||| %{smart_count}", + "share": "Κοινοποίηση", + "download": "Λήψη " + }, + "boolean": { + "true": "Ναι", + "false": "Όχι" + }, + "page": { + "create": "Δημιουργία %{name}", + "dashboard": "Πίνακας Ελέγχου", + "edit": "%{name} #%{id}", + "error": "Κάτι πήγε στραβά", + "list": "%{name}", + "loading": "Φόρτωση", + "not_found": "Δεν βρέθηκε", + "show": "%{name} #%{id}", + "empty": "Δεν υπάρχει %{name} ακόμη.", + "invite": "Θέλετε να προσθέσετε ένα?" + }, + "input": { + "file": { + "upload_several": "Ρίξτε μερικά αρχεία για να τα ανεβάσετε, ή κάντε κλικ για να επιλέξετε ένα.", + "upload_single": "Ρίξτε ένα αρχείο για να τα ανεβάσετε, ή κάντε κλικ για να το επιλέξετε." + }, + "image": { + "upload_several": "Ρίξτε μερικές φωτογραφίες για να τις ανεβάσετε, ή κάντε κλικ για να επιλέξετε μια.", + "upload_single": "Ρίξτε μια φωτογραφία για να την ανεβάσετε, ή κάντε κλικ για να την επιλέξετε." + }, + "references": { + "all_missing": "Αδυναμία εύρεσης δεδομένων αναφοράς.", + "many_missing": "Τουλάχιστον μια από τις συσχετιζόμενες αναφορές φαίνεται δεν είναι διαθέσιμη.", + "single_missing": "Η συσχετιζόμενη αναφορά φαίνεται δεν είναι διαθέσιμη." + }, + "password": { + "toggle_visible": "Απόκρυψη κωδικού πρόσβασης", + "toggle_hidden": "Εμφάνιση κωδικού πρόσβασης" + } + }, + "message": { + "about": "Σχετικά", + "are_you_sure": "Είστε σίγουροι;", + "bulk_delete_content": "Είστε σίγουροι πως θέλετε να διαγράψετε το %{name}; |||| Είστε σίγουροι πως θέλετε να διαγράψετε τα %{smart_count};", + "bulk_delete_title": "Διαγραφή του %{name} |||| Διαγραφή του %{smart_count} %{name}", + "delete_content": "Είστε σίγουροι πως θέλετε να διαγράψετε αυτό το αντικείμενο;", + "delete_title": "Διαγραφή του %{name} #%{id}", + "details": "Λεπτομέρειες", + "error": "Παρουσιάστηκε ένα πρόβλημα από τη μεριά του πελάτη και το αίτημα σας δεν μπορεί να ολοκληρωθεί.", + "invalid_form": "Η φόρμα δεν είναι έγκυρη. Ελέγξτε για σφάλματα", + "loading": "Η σελίδα φορτώνει, περιμένετε λίγο", + "no": "Όχι", + "not_found": "Είτε έχετε εισάγει λανθασμένο URL, είτε ακολουθήσατε έναν υπερσύνδεσμο που δεν ισχύει.", + "yes": "Ναι", + "unsaved_changes": "Μερικές από τις αλλαγές σας δεν έχουν αποθηκευτεί. Είστε σίγουροι πως θέλετε να τις αγνοήσετε;" + }, + "navigation": { + "no_results": "Δεν βρέθηκαν αποτελέσματα", + "no_more_results": "Η σελίδα %{page} είναι εκτός ορίων. Δοκιμάστε την προηγούμενη σελίδα.", + "page_out_of_boundaries": "Η σελίδα {page} είναι εκτός ορίων", + "page_out_from_end": "Δεν είναι δυνατή η πλοήγηση πέραν της τελευταίας σελίδας", + "page_out_from_begin": "Δεν είναι δυνατή η πλοήγηση πριν τη σελίδα 1", + "page_range_info": "%{offsetBegin}-%{offsetEnd} από %{total}", + "page_rows_per_page": "Αντικείμενα ανά σελίδα:", + "next": "Επόμενο", + "prev": "Προηγούμενο", + "skip_nav": "Παράβλεψη στο περιεχόμενο" + }, + "notification": { + "updated": "Το στοιχείο ενημερώθηκε |||| %{smart_count} στοιχεία ενημερώθηκαν", + "created": "Το στοιχείο δημιουργήθηκε", + "deleted": "Το στοιχείο διαγράφηκε |||| %{smart_count} στοιχεία διαγράφηκαν", + "bad_item": "Λανθασμένο στοιχείο", + "item_doesnt_exist": "Το παρόν στοιχείο δεν υπάρχει", + "http_error": "Σφάλμα κατά την επικοινωνία με το διακομιστή", + "data_provider_error": "Σφάλμα παρόχου δεδομένων. Παρακαλούμε συμβουλευτείτε την κονσόλα για περισσότερες πληροφορίες.", + "i18n_error": "Αδυναμία ανάκτησης των μεταφράσεων για την συγκεκριμένη γλώσσα", + "canceled": "Η συγκεκριμένη δράση ακυρώθηκε", + "logged_out": "Η συνεδρία σας έχει λήξει, παρακαλούμε ξανασυνδεθείτε.", + "new_version": "Υπάρχει νέα έκδοση διαθέσιμη! Παρακαλούμε ανανεώστε το παράθυρο." + }, + "toggleFieldsMenu": { + "columnsToDisplay": "Στήλες προς εμφάνιση", + "layout": "Διάταξη", + "grid": "Πλεγμα", + "table": "Πινακας" + } + }, + "message": { + "note": "ΣΗΜΕΙΩΣΗ", + "transcodingDisabled": "Η αλλαγή της διαμόρφωσης της διακωδικοποίησης μέσω της διεπαφής του περιηγητή ιστού είναι απενεργοποιημένη για λόγους ασφαλείας. Εαν επιθυμείτε να αλλάξετε (τροποποίηση ή δημιουργία) των επιλογών διακωδικοποίησης, επανεκκινήστε το διακομιστή με την επιλογή %{config}.", + "transcodingEnabled": "Το Navidrome λειτουργεί με %{config}, καθιστόντας δυνατή την εκτέλεση εντολών συστήματος μέσω των ρυθμίσεων διακωδικοποίησης χρησιμοποιώντας την διεπαφή ιστού. Προτείνουμε να το απενεργοποιήσετε για λόγους ασφαλείας και να το ενεργοποιήσετε μόνο όταν παραμετροποιείτε τις επιλογές διακωδικοποίησης.", + "songsAddedToPlaylist": "Προστέθηκε 1 τραγούδι στη λίστα αναπαραγωγής |||| Προστέθηκαν %{smart_count} τραγούδια στη λίστα αναπαραγωγής", + "noPlaylistsAvailable": "Κανένα διαθέσιμο", + "delete_user_title": "Διαγραφή του χρήστη '%{name}'", + "delete_user_content": "Είστε σίγουροι πως θέλετε να διαγράψετε αυτό το χρήστη και όλα τα δεδομένα του (συμπεριλαμβανομένων των λιστών αναπαραγωγής και προτιμήσεων);", + "notifications_blocked": "Έχετε μπλοκάρει τις Ειδοποιήσεις από τη σελίδα, μέσω των ρυθμίσεων του περιηγητή ιστού σας", + "notifications_not_available": "Αυτός ο περιηγητής ιστού δεν υποστηρίζει ειδοποιήσεις στην επιφάνεια εργασίας ή δεν έχετε πρόσβαση στο Navidrome μέσω https", + "lastfmLinkSuccess": "Το Last.fm έχει διασυνδεθεί επιτυχώς και η λειτουργία scrobbling ενεργοποιήθηκε", + "lastfmLinkFailure": "Δεν μπορεί να πραγματοποιηθεί διασύνδεση με το Last.fm", + "lastfmUnlinkSuccess": "Το Last.fm αποσυνδέθηκε και η λειτουργία scrobbling έχει απενεργοποιηθεί", + "lastfmUnlinkFailure": "Το Last.fm δεν μπορεί να αποσυνδεθεί", + "openIn": { + "lastfm": "Άνοιγμα στο Last.fm", + "musicbrainz": "Άνοιγμα στο MusicBrainz" + }, + "lastfmLink": "Διαβάστε περισσότερα...", + "listenBrainzLinkSuccess": "Το ListenBrainz έχει διασυνδεθεί επιτυχώς και η λειτουργία scrobbling έχει ενεργοποιηθεί για το χρήστη: %{user}", + "listenBrainzLinkFailure": "Το ListenBrainz δεν μπορεί να διασυνδεθεί: %{error}", + "listenBrainzUnlinkSuccess": "Το ListenBrainz έχει αποσυνδεθεί και το scrobbling έχει απενεργοποιηθεί", + "listenBrainzUnlinkFailure": "Το ListenBrainz δεν μπορεί να διασυνδεθεί", + "downloadOriginalFormat": "Λήψη σε αρχική μορφή", + "shareOriginalFormat": "Κοινή χρήση σε αρχική μορφή", + "shareDialogTitle": "Κοινή χρήση %{resource} '%{name}'", + "shareBatchDialogTitle": "Κοινή χρήση 1 %{resource} |||| Κοινή χρήση %{smart_count} %{resource}", + "shareSuccess": "Το URL αντιγράφτηκε στο πρόχειρο: %{url}", + "shareFailure": "Σφάλμα κατά την αντιγραφή της διεύθυνσης URL %{url} στο πρόχειρο", + "downloadDialogTitle": "Λήψη %{resource} '%{name}'(%{size})", + "shareCopyToClipboard": "Αντιγραφή στο πρόχειρο: Ctrl+C, Enter", + "remove_missing_title": "Αφαιρέστε τα αρχεία που λείπουν", + "remove_missing_content": "Είστε βέβαιοι ότι θέλετε να αφαιρέσετε τα επιλεγμένα αρχεία που λείπουν από τη βάση δεδομένων; Αυτό θα καταργήσει οριστικά τυχόν αναφορές σε αυτά, συμπεριλαμβανομένων των αριθμών παιχνιδιών και των αξιολογήσεών τους." + }, + "menu": { + "library": "Βιβλιοθήκη", + "settings": "Ρυθμίσεις", + "version": "Έκδοση", + "theme": "Θέμα", + "personal": { + "name": "Προσωπικές", + "options": { + "theme": "Θέμα", + "language": "Γλώσσα", + "defaultView": "Προκαθορισμένη προβολή", + "desktop_notifications": "Ειδοποιήσεις στην Επιφάνεια Εργασίας", + "lastfmScrobbling": "Λειτουργία Scrobble στο Last.fm", + "listenBrainzScrobbling": "Λειτουργία scrobble με το ListenBrainz", + "replaygain": "Λειτουργία ReplayGain", + "preAmp": "ReplayGain PreAmp (dB)", + "gain": { + "none": "Ανενεργό", + "album": "Χρησιμοποιήστε το Album Gain", + "track": "Χρησιμοποιήστε το Track Gain" + }, + "lastfmNotConfigured": "Το Last.fm API-Key δεν έχει ρυθμιστεί" + } + }, + "albumList": "Άλμπουμ", + "about": "Σχετικά", + "playlists": "Λίστες Αναπαραγωγής", + "sharedPlaylists": "Κοινοποιημένες Λίστες Αναπαραγωγής" + }, + "player": { + "playListsText": "Ουρά Αναπαραγωγής", + "openText": "Άνοιγμα", + "closeText": "Κλείσιμο", + "notContentText": "Δεν υπάρχει μουσική", + "clickToPlayText": "Κλίκ για αναπαραγωγή", + "clickToPauseText": "Κλίκ για παύση", + "nextTrackText": "Επόμενο κομμάτι", + "previousTrackText": "Προηγούμενο κομμάτι", + "reloadText": "Επαναφόρτωση", + "volumeText": "Ένταση", + "toggleLyricText": "Εναλλαγή στίχων", + "toggleMiniModeText": "Ελαχιστοποίηση", + "destroyText": "Κλέισιμο", + "downloadText": "Ληψη", + "removeAudioListsText": "Διαγραφή λιστών ήχου", + "clickToDeleteText": "Κάντε κλικ για να διαγράψετε %{name}", + "emptyLyricText": "Δεν υπάρχουν στίχοι", + "playModeText": { + "order": "Στη σειρά", + "orderLoop": "Επανάληψη", + "singleLoop": "Επανάληψη μια φορά", + "shufflePlay": "Ανακατεμα" + } + }, + "about": { + "links": { + "homepage": "Αρχική σελίδα", + "source": "Πηγαίος κώδικας", + "featureRequests": "Αιτήματα χαρακτηριστικών", + "lastInsightsCollection": "Τελευταία συλλογή πληροφοριών", + "insights": { + "disabled": "Απενεργοποιημένο", + "waiting": "Αναμονή" + } + } + }, + "activity": { + "title": "Δραστηριότητα", + "totalScanned": "Σαρώμένοι Φάκελοι", + "quickScan": "Γρήγορη Σάρωση", + "fullScan": "Πλήρης Σάρωση", + "serverUptime": "Λειτουργία Διακομιστή", + "serverDown": "ΕΚΤΟΣ ΣΥΝΔΕΣΗΣ" + }, + "help": { + "title": "Συντομεύσεις του Navidrome", + "hotkeys": { + "show_help": "Προβολή αυτής της Βοήθειας", + "toggle_menu": "Εναλλαγή Μπάρας Μενού", + "toggle_play": "Αναπαραγωγή / Παύση", + "prev_song": "Προηγούμενο Τραγούδι", + "next_song": "Επόμενο Τραγούδι", + "vol_up": "Αύξηση Έντασης", + "vol_down": "Μείωση Έντασης", + "toggle_love": "Προσθήκη αυτού του κομματιού στα αγαπημένα", + "current_song": "Μεταβείτε στο Τρέχον τραγούδι" + } + } +} \ No newline at end of file From 339458041389219279f2519205c25d4dcc0e0940 Mon Sep 17 00:00:00 2001 From: Deluan Date: Tue, 25 Mar 2025 17:43:25 -0400 Subject: [PATCH 27/30] feat(ui): add Norwegian translation Signed-off-by: Deluan --- resources/i18n/no.json | 514 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 514 insertions(+) create mode 100644 resources/i18n/no.json diff --git a/resources/i18n/no.json b/resources/i18n/no.json new file mode 100644 index 000000000..bd4c37d0b --- /dev/null +++ b/resources/i18n/no.json @@ -0,0 +1,514 @@ +{ + "languageName": "Engelsk", + "resources": { + "song": { + "name": "Låt |||| Låter", + "fields": { + "albumArtist": "Album Artist", + "duration": "Tid", + "trackNumber": "#", + "playCount": "Avspillinger", + "title": "Tittel", + "artist": "Artist", + "album": "Album", + "path": "Filbane", + "genre": "Sjanger", + "compilation": "Samling", + "year": "År", + "size": "Filstørrelse", + "updatedAt": "Oppdatert kl", + "bitRate": "Bithastighet", + "discSubtitle": "Diskundertekst", + "starred": "Favoritt", + "comment": "Kommentar", + "rating": "Vurdering", + "quality": "Kvalitet", + "bpm": "BPM", + "playDate": "Sist spilt", + "channels": "Kanaler", + "createdAt": "", + "grouping": "", + "mood": "", + "participants": "", + "tags": "", + "mappedTags": "", + "rawTags": "", + "bitDepth": "" + }, + "actions": { + "addToQueue": "Spill Senere", + "playNow": "Leke nå", + "addToPlaylist": "Legg til i spilleliste", + "shuffleAll": "Bland alle", + "download": "nedlasting", + "playNext": "Spill Neste", + "info": "Få informasjon" + } + }, + "album": { + "name": "Album", + "fields": { + "albumArtist": "Album Artist", + "artist": "Artist", + "duration": "Tid", + "songCount": "Sanger", + "playCount": "Avspillinger", + "name": "Navn", + "genre": "Sjanger", + "compilation": "Samling", + "year": "År", + "updatedAt": "Oppdatert kl", + "comment": "Kommentar", + "rating": "Vurdering", + "createdAt": "", + "size": "", + "originalDate": "", + "releaseDate": "", + "releases": "", + "released": "", + "recordLabel": "", + "catalogNum": "", + "releaseType": "", + "grouping": "", + "media": "", + "mood": "" + }, + "actions": { + "playAll": "Spill", + "playNext": "Spill neste", + "addToQueue": "Spille senere", + "shuffle": "Bland", + "addToPlaylist": "Legg til i spilleliste", + "download": "nedlasting", + "info": "Få informasjon", + "share": "" + }, + "lists": { + "all": "Alle", + "random": "Tilfeldig", + "recentlyAdded": "Nylig lagt til", + "recentlyPlayed": "Nylig spilt", + "mostPlayed": "Mest spilte", + "starred": "Favoritter", + "topRated": "Topp rangert" + } + }, + "artist": { + "name": "Artist |||| Artister", + "fields": { + "name": "Navn", + "albumCount": "Antall album", + "songCount": "Antall sanger", + "playCount": "Spiller", + "rating": "Vurdering", + "genre": "Sjanger", + "size": "", + "role": "" + }, + "roles": { + "albumartist": "", + "artist": "", + "composer": "", + "conductor": "", + "lyricist": "", + "arranger": "", + "producer": "", + "director": "", + "engineer": "", + "mixer": "", + "remixer": "", + "djmixer": "", + "performer": "" + } + }, + "user": { + "name": "Bruker |||| Brukere", + "fields": { + "userName": "Brukernavn", + "isAdmin": "er admin", + "lastLoginAt": "Siste pålogging kl", + "updatedAt": "Oppdatert kl", + "name": "Navn", + "password": "Passord", + "createdAt": "Opprettet kl", + "changePassword": "Bytte Passord", + "currentPassword": "Nåværende Passord", + "newPassword": "Nytt Passord", + "token": "Token", + "lastAccessAt": "" + }, + "helperTexts": { + "name": "Endringer i navnet ditt vil kun gjenspeiles ved neste pålogging" + }, + "notifications": { + "created": "Bruker opprettet", + "updated": "Bruker oppdatert", + "deleted": "Bruker fjernet" + }, + "message": { + "listenBrainzToken": "Skriv inn ListenBrainz-brukertokenet ditt.", + "clickHereForToken": "Klikk her for å få tokenet ditt" + } + }, + "player": { + "name": "Avspiller |||| Avspillere", + "fields": { + "name": "Navn", + "transcodingId": "Omkoding", + "maxBitRate": "Maks. Bithastighet", + "client": "Klient", + "userName": "Brukernavn", + "lastSeen": "Sist sett kl", + "reportRealPath": "Rapporter ekte sti", + "scrobbleEnabled": "Send Scrobbles til eksterne tjenester" + } + }, + "transcoding": { + "name": "Omkoding |||| Omkodinger", + "fields": { + "name": "Navn", + "targetFormat": "Målformat", + "defaultBitRate": "Standard bithastighet", + "command": "Kommando" + } + }, + "playlist": { + "name": "Spilleliste |||| Spillelister", + "fields": { + "name": "Navn", + "duration": "Varighet", + "ownerName": "Eieren", + "public": "Offentlig", + "updatedAt": "Oppdatert kl", + "createdAt": "Opprettet kl", + "songCount": "Sanger", + "comment": "Kommentar", + "sync": "Autoimport", + "path": "Import fra" + }, + "actions": { + "selectPlaylist": "Velg en spilleliste:", + "addNewPlaylist": "Opprett \"%{name}\"", + "export": "Eksport", + "makePublic": "Gjør offentlig", + "makePrivate": "Gjør privat" + }, + "message": { + "duplicate_song": "Legg til dupliserte sanger", + "song_exist": "Det legges til duplikater i spillelisten. Vil du legge til duplikatene eller hoppe over dem?" + } + }, + "radio": { + "name": "", + "fields": { + "name": "", + "streamUrl": "", + "homePageUrl": "", + "updatedAt": "", + "createdAt": "" + }, + "actions": { + "playNow": "" + } + }, + "share": { + "name": "", + "fields": { + "username": "", + "url": "", + "description": "", + "contents": "", + "expiresAt": "", + "lastVisitedAt": "", + "visitCount": "", + "format": "", + "maxBitRate": "", + "updatedAt": "", + "createdAt": "", + "downloadable": "" + } + }, + "missing": { + "name": "", + "fields": { + "path": "", + "size": "", + "updatedAt": "" + }, + "actions": { + "remove": "" + }, + "notifications": { + "removed": "" + }, + "empty": "" + } + }, + "ra": { + "auth": { + "welcome1": "Takk for at du installerte Navidrome!", + "welcome2": "Opprett en admin -bruker for å starte", + "confirmPassword": "Bekreft Passord", + "buttonCreateAdmin": "Opprett Admin", + "auth_check_error": "Vennligst Logg inn for å fortsette", + "user_menu": "Profil", + "username": "Brukernavn", + "password": "Passord", + "sign_in": "Logg inn", + "sign_in_error": "Autentisering mislyktes. Prøv på nytt", + "logout": "Logg ut", + "insightsCollectionNote": "" + }, + "validation": { + "invalidChars": "Bruk bare bokstaver og tall", + "passwordDoesNotMatch": "Passordet er ikke like", + "required": "Obligatorisk", + "minLength": "Må være minst %{min} tegn", + "maxLength": "Må være %{max} tegn eller færre", + "minValue": "Må være minst %{min}", + "maxValue": "Må være %{max} eller mindre", + "number": "Må være et tall", + "email": "Må være en gyldig e-post", + "oneOf": "Må være en av: %{options}", + "regex": "Må samsvare med et spesifikt format (regexp): %{pattern}", + "unique": "Må være unik", + "url": "" + }, + "action": { + "add_filter": "Legg til filter", + "add": "Legge til", + "back": "Gå tilbake", + "bulk_actions": "1 element valgt |||| %{smart_count} elementer er valgt", + "cancel": "Avbryt", + "clear_input_value": "Klar verdi", + "clone": "Klone", + "confirm": "Bekrefte", + "create": "Skape", + "delete": "Slett", + "edit": "Redigere", + "export": "Eksport", + "list": "Liste", + "refresh": "oppdater", + "remove_filter": "Fjern dette filteret", + "remove": "Fjerne", + "save": "Lagre", + "search": "Søk", + "show": "Vis", + "sort": "Sortere", + "undo": "Angre", + "expand": "Utvide", + "close": "Lukk", + "open_menu": "Åpne menyen", + "close_menu": "Lukk menyen", + "unselect": "Fjern valget", + "skip": "Hopp over", + "bulk_actions_mobile": "", + "share": "", + "download": "" + }, + "boolean": { + "true": "Ja", + "false": "Nei" + }, + "page": { + "create": "Opprett %{name}", + "dashboard": "Dashbord", + "edit": "%{name} #%{id}", + "error": "Noe gikk galt", + "list": "%{Navn}", + "loading": "Laster", + "not_found": "Ikke funnet", + "show": "%{name} #%{id}", + "empty": "Ingen %{name} ennå.", + "invite": "Vil du legge til en?" + }, + "input": { + "file": { + "upload_several": "Slipp noen filer for å laste opp, eller klikk for å velge en.", + "upload_single": "Slipp en fil for å laste opp, eller klikk for å velge den." + }, + "image": { + "upload_several": "Slipp noen bilder for å laste opp, eller klikk for å velge ett.", + "upload_single": "Slipp et bilde for å laste opp, eller klikk for å velge det." + }, + "references": { + "all_missing": "Kan ikke finne referansedata.", + "many_missing": "Minst én av de tilknyttede referansene ser ikke ut til å være tilgjengelig lenger.", + "single_missing": "Tilknyttet referanse ser ikke lenger ut til å være tilgjengelig." + }, + "password": { + "toggle_visible": "Skjul passord", + "toggle_hidden": "Vis passord" + } + }, + "message": { + "about": "Om", + "are_you_sure": "Er du sikker?", + "bulk_delete_content": "Er du sikker på at du vil slette denne %{name}? |||| Er du sikker på at du vil slette disse %{smart_count} elementene?", + "bulk_delete_title": "Slett %{name} |||| Slett %{smart_count} %{name}", + "delete_content": "Er du sikker på at du vil slette dette elementet?", + "delete_title": "Slett %{name} #%{id}", + "details": "Detaljer", + "error": "Det oppstod en klientfeil og forespørselen din kunne ikke fullføres.", + "invalid_form": "Skjemaet er ikke gyldig. Vennligst se etter feil", + "loading": "Siden lastes, bare et øyeblikk", + "no": "Nei", + "not_found": "Enten skrev du inn feil URL, eller så fulgte du en dårlig lenke.", + "yes": "Ja", + "unsaved_changes": "Noen av endringene dine ble ikke lagret. Er du sikker på at du vil ignorere dem?" + }, + "navigation": { + "no_results": "Ingen resultater", + "no_more_results": "Sidetallet %{page} er utenfor grensene. Prøv forrige side.", + "page_out_of_boundaries": "Sidetall %{page} utenfor grensene", + "page_out_from_end": "Kan ikke gå etter siste side", + "page_out_from_begin": "Kan ikke gå før side 1", + "page_range_info": "%{offsetBegin}-%{offsetEnd} av %{total}", + "page_rows_per_page": "Elementer per side:", + "next": "Neste", + "prev": "Forrige", + "skip_nav": "Hopp til innholdet" + }, + "notification": { + "updated": "Element oppdatert |||| %{smart_count} elementer er oppdatert", + "created": "Element opprettet", + "deleted": "Element slettet |||| %{smart_count} elementer slettet", + "bad_item": "Feil element", + "item_doesnt_exist": "Elementet eksisterer ikke", + "http_error": "Serverkommunikasjonsfeil", + "data_provider_error": "dataleverandørfeil. Sjekk konsollen for detaljer.", + "i18n_error": "Kan ikke laste oversettelsene for det angitte språket", + "canceled": "Handlingen avbrutt", + "logged_out": "Økten din er avsluttet. Koble til på nytt.", + "new_version": "Ny versjon tilgjengelig! Trykk Oppdater " + }, + "toggleFieldsMenu": { + "columnsToDisplay": "Kolonner som skal vises", + "layout": "Oppsett", + "grid": "Nett", + "table": "Bord" + } + }, + "message": { + "note": "Info", + "transcodingDisabled": "Endring av transkodingskonfigurasjonen gjennom webgrensesnittet er deaktivert av sikkerhetsgrunner. Hvis du ønsker å endre (redigere eller legge til) transkodingsalternativer, start serveren på nytt med %{config}-konfigurasjonsalternativet.", + "transcodingEnabled": "Navidrome kjører for øyeblikket med %{config}, noe som gjør det mulig å kjøre systemkommandoer fra transkodingsinnstillingene ved å bruke nettgrensesnittet. Vi anbefaler å deaktivere den av sikkerhetsgrunner og bare aktivere den når du konfigurerer alternativer for omkoding.", + "songsAddedToPlaylist": "Lagt til 1 sang i spillelisten |||| Lagt til %{smart_count} sanger i spillelisten", + "noPlaylistsAvailable": "Ingen tilgjengelig", + "delete_user_title": "Slett bruker «%{name}»", + "delete_user_content": "Er du sikker på at du vil slette denne brukeren og alle dataene deres (inkludert spillelister og preferanser)?", + "notifications_blocked": "Du har blokkert varsler for dette nettstedet i nettleserens innstillinger", + "notifications_not_available": "Denne nettleseren støtter ikke skrivebordsvarsler, eller du har ikke tilgang til Navidrome over https", + "lastfmLinkSuccess": "Last.fm er vellykket koblet og scrobbling aktivert", + "lastfmLinkFailure": "Last.fm kunne ikke kobles til", + "lastfmUnlinkSuccess": "Last.fm koblet fra og scrobbling deaktivert", + "lastfmUnlinkFailure": "Last.fm kunne ikke kobles fra", + "openIn": { + "lastfm": "Åpne i Last.fm", + "musicbrainz": "Åpne i MusicBrainz" + }, + "lastfmLink": "Les mer...", + "listenBrainzLinkSuccess": "ListenBrainz er vellykket koblet og scrobbling aktivert som bruker: %{user}", + "listenBrainzLinkFailure": "ListenBrainz kunne ikke kobles: %{error}", + "listenBrainzUnlinkSuccess": "ListenBrainz koblet fra og scrobbling deaktivert", + "listenBrainzUnlinkFailure": "ListenBrainz kunne ikke fjernes", + "downloadOriginalFormat": "", + "shareOriginalFormat": "", + "shareDialogTitle": "", + "shareBatchDialogTitle": "", + "shareSuccess": "", + "shareFailure": "", + "downloadDialogTitle": "", + "shareCopyToClipboard": "", + "remove_missing_title": "", + "remove_missing_content": "" + }, + "menu": { + "library": "Bibliotek", + "settings": "Innstillinger", + "version": "Versjon", + "theme": "Tema", + "personal": { + "name": "Personlig", + "options": { + "theme": "Tema", + "language": "Språk", + "defaultView": "Standardvisning", + "desktop_notifications": "Skrivebordsvarsler", + "lastfmScrobbling": "Scrobble til Last.fm", + "listenBrainzScrobbling": "Scrobble til ListenBrainz", + "replaygain": "", + "preAmp": "", + "gain": { + "none": "", + "album": "", + "track": "" + }, + "lastfmNotConfigured": "" + } + }, + "albumList": "Album", + "about": "Om", + "playlists": "Spilleliste", + "sharedPlaylists": "Delte spillelister" + }, + "player": { + "playListsText": "Spillekø", + "openText": "Åpne", + "closeText": "Lukk", + "notContentText": "Ingen musikk", + "clickToPlayText": "Klikk for å spille", + "clickToPauseText": "Klikk for å sette på pause", + "nextTrackText": "Neste spor", + "previousTrackText": "Forrige spor", + "reloadText": "Last inn på nytt", + "volumeText": "Volum", + "toggleLyricText": "Veksle mellom tekster", + "toggleMiniModeText": "Minimer", + "destroyText": "Ødelegge", + "downloadText": "nedlasting", + "removeAudioListsText": "Slett lydlister", + "clickToDeleteText": "Klikk for å slette %{name}", + "emptyLyricText": "Ingen sangtekster", + "playModeText": { + "order": "I rekkefølge", + "orderLoop": "Gjenta", + "singleLoop": "Gjenta engang", + "shufflePlay": "Tilfeldig rekkefølge" + } + }, + "about": { + "links": { + "homepage": "Hjemmeside", + "source": "Kildekode", + "featureRequests": "Funksjonsforespørsler", + "lastInsightsCollection": "", + "insights": { + "disabled": "", + "waiting": "" + } + } + }, + "activity": { + "title": "Aktivitet", + "totalScanned": "Totalt skannede mapper", + "quickScan": "Rask skanning", + "fullScan": "Full skanning", + "serverUptime": "Serveroppetid", + "serverDown": "OFFLINE" + }, + "help": { + "title": "Navidrome hurtigtaster", + "hotkeys": { + "show_help": "Vis denne hjelpen", + "toggle_menu": "Bytt menysidelinje", + "toggle_play": "Spill / Pause", + "prev_song": "Forrige sang", + "next_song": "Neste sang", + "vol_up": "Volum opp", + "vol_down": "Volum ned", + "toggle_love": "Legg til dette sporet i favoritter", + "current_song": "" + } + } +} \ No newline at end of file From 46a2ec0ba195183419e3836de7d2f41f96061d92 Mon Sep 17 00:00:00 2001 From: Deluan Date: Tue, 25 Mar 2025 20:05:24 -0400 Subject: [PATCH 28/30] feat(ui): hide absolute paths from regular users Signed-off-by: Deluan --- ui/src/common/PathField.jsx | 24 ++++----- ui/src/common/PathField.test.jsx | 86 ++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 13 deletions(-) create mode 100644 ui/src/common/PathField.test.jsx diff --git a/ui/src/common/PathField.jsx b/ui/src/common/PathField.jsx index 115a2ee49..21822878a 100644 --- a/ui/src/common/PathField.jsx +++ b/ui/src/common/PathField.jsx @@ -1,24 +1,22 @@ import PropTypes from 'prop-types' import React from 'react' -import { useRecordContext } from 'react-admin' +import { usePermissions, useRecordContext } from 'react-admin' import config from '../config' export const PathField = (props) => { const record = useRecordContext(props) - return ( - - {record.libraryPath} - {config.separator} - {record.path} - - ) + const { permissions } = usePermissions() + let path = permissions === 'admin' ? record.libraryPath : '' + + if (path && path.endsWith(config.separator)) { + path = `${path}${record.path}` + } else { + path = path ? `${path}${config.separator}${record.path}` : record.path + } + + return {path} } PathField.propTypes = { - label: PropTypes.string, record: PropTypes.object, } - -PathField.defaultProps = { - addLabel: true, -} diff --git a/ui/src/common/PathField.test.jsx b/ui/src/common/PathField.test.jsx new file mode 100644 index 000000000..de8b90899 --- /dev/null +++ b/ui/src/common/PathField.test.jsx @@ -0,0 +1,86 @@ +import React from 'react' +import { render } from '@testing-library/react' +import { PathField } from './PathField' +import { usePermissions, useRecordContext } from 'react-admin' +import config from '../config' + +// Mock react-admin hooks +vi.mock('react-admin', () => ({ + usePermissions: vi.fn(), + useRecordContext: vi.fn(), +})) + +// Mock config +vi.mock('../config', () => ({ + default: { + separator: '/', + }, +})) + +describe('PathField', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('renders path without libraryPath for non-admin users', () => { + // Setup + usePermissions.mockReturnValue({ permissions: 'user' }) + useRecordContext.mockReturnValue({ + path: 'music/song.mp3', + libraryPath: '/data/media', + }) + + // Act + const { container } = render() + + // Assert + expect(container.textContent).toBe('music/song.mp3') + expect(container.textContent).not.toContain('/data/media') + }) + + it('renders combined path for admin users when libraryPath does not end with separator', () => { + // Setup + usePermissions.mockReturnValue({ permissions: 'admin' }) + useRecordContext.mockReturnValue({ + path: 'music/song.mp3', + libraryPath: '/data/media', + }) + + // Act + const { container } = render() + + // Assert + expect(container.textContent).toBe('/data/media/music/song.mp3') + }) + + it('renders combined path for admin users when libraryPath ends with separator', () => { + // Setup + usePermissions.mockReturnValue({ permissions: 'admin' }) + useRecordContext.mockReturnValue({ + path: 'music/song.mp3', + libraryPath: '/data/media/', + }) + + // Act + const { container } = render() + + // Assert + expect(container.textContent).toBe('/data/media/music/song.mp3') + }) + + it('works with a different separator from config', () => { + // Setup + config.separator = '\\' + usePermissions.mockReturnValue({ permissions: 'admin' }) + useRecordContext.mockReturnValue({ + path: 'music\\song.mp3', + libraryPath: 'C:\\data', + }) + + // Act + const { container } = render() + + // Assert + expect(container.textContent).toBe('C:\\data\\music\\song.mp3') + }) +}) From 5ab345c83ed8af92f95e4131299d879b18a1f37f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deluan=20Quint=C3=A3o?= Date: Thu, 27 Mar 2025 18:57:06 -0400 Subject: [PATCH 29/30] chore(server): add more info to scrobble errors logs (#3889) * chore(server): add more info to scrobble errors Signed-off-by: Deluan * chore(server): add more info to scrobble errors Signed-off-by: Deluan * chore(server): add more info to scrobble errors Signed-off-by: Deluan --------- Signed-off-by: Deluan --- core/agents/lastfm/agent.go | 10 +++++----- core/agents/listenbrainz/agent.go | 12 ++++++------ utils/cache/cached_http_client.go | 4 +++- utils/cache/simple_cache.go | 9 ++++++++- 4 files changed, 22 insertions(+), 13 deletions(-) diff --git a/core/agents/lastfm/agent.go b/core/agents/lastfm/agent.go index 01ffa677e..0c8d290d4 100644 --- a/core/agents/lastfm/agent.go +++ b/core/agents/lastfm/agent.go @@ -296,7 +296,7 @@ func (l *lastfmAgent) NowPlaying(ctx context.Context, userId string, track *mode }) if err != nil { log.Warn(ctx, "Last.fm client.updateNowPlaying returned error", "track", track.Title, err) - return scrobbler.ErrUnrecoverable + return errors.Join(err, scrobbler.ErrUnrecoverable) } return nil } @@ -304,7 +304,7 @@ func (l *lastfmAgent) NowPlaying(ctx context.Context, userId string, track *mode func (l *lastfmAgent) Scrobble(ctx context.Context, userId string, s scrobbler.Scrobble) error { sk, err := l.sessionKeys.Get(ctx, userId) if err != nil || sk == "" { - return scrobbler.ErrNotAuthorized + return errors.Join(err, scrobbler.ErrNotAuthorized) } if s.Duration <= 30 { @@ -328,12 +328,12 @@ func (l *lastfmAgent) Scrobble(ctx context.Context, userId string, s scrobbler.S isLastFMError := errors.As(err, &lfErr) if !isLastFMError { log.Warn(ctx, "Last.fm client.scrobble returned error", "track", s.Title, err) - return scrobbler.ErrRetryLater + return errors.Join(err, scrobbler.ErrRetryLater) } if lfErr.Code == 11 || lfErr.Code == 16 { - return scrobbler.ErrRetryLater + return errors.Join(err, scrobbler.ErrRetryLater) } - return scrobbler.ErrUnrecoverable + return errors.Join(err, scrobbler.ErrUnrecoverable) } func (l *lastfmAgent) IsAuthorized(ctx context.Context, userId string) bool { diff --git a/core/agents/listenbrainz/agent.go b/core/agents/listenbrainz/agent.go index e808f025e..200e9f63c 100644 --- a/core/agents/listenbrainz/agent.go +++ b/core/agents/listenbrainz/agent.go @@ -76,14 +76,14 @@ func (l *listenBrainzAgent) formatListen(track *model.MediaFile) listenInfo { func (l *listenBrainzAgent) NowPlaying(ctx context.Context, userId string, track *model.MediaFile) error { sk, err := l.sessionKeys.Get(ctx, userId) if err != nil || sk == "" { - return scrobbler.ErrNotAuthorized + return errors.Join(err, scrobbler.ErrNotAuthorized) } li := l.formatListen(track) err = l.client.updateNowPlaying(ctx, sk, li) if err != nil { log.Warn(ctx, "ListenBrainz updateNowPlaying returned error", "track", track.Title, err) - return scrobbler.ErrUnrecoverable + return errors.Join(err, scrobbler.ErrUnrecoverable) } return nil } @@ -91,7 +91,7 @@ func (l *listenBrainzAgent) NowPlaying(ctx context.Context, userId string, track func (l *listenBrainzAgent) Scrobble(ctx context.Context, userId string, s scrobbler.Scrobble) error { sk, err := l.sessionKeys.Get(ctx, userId) if err != nil || sk == "" { - return scrobbler.ErrNotAuthorized + return errors.Join(err, scrobbler.ErrNotAuthorized) } li := l.formatListen(&s.MediaFile) @@ -105,12 +105,12 @@ func (l *listenBrainzAgent) Scrobble(ctx context.Context, userId string, s scrob isListenBrainzError := errors.As(err, &lbErr) if !isListenBrainzError { log.Warn(ctx, "ListenBrainz Scrobble returned HTTP error", "track", s.Title, err) - return scrobbler.ErrRetryLater + return errors.Join(err, scrobbler.ErrRetryLater) } if lbErr.Code == 500 || lbErr.Code == 503 { - return scrobbler.ErrRetryLater + return errors.Join(err, scrobbler.ErrRetryLater) } - return scrobbler.ErrUnrecoverable + return errors.Join(err, scrobbler.ErrUnrecoverable) } func (l *listenBrainzAgent) IsAuthorized(ctx context.Context, userId string) bool { diff --git a/utils/cache/cached_http_client.go b/utils/cache/cached_http_client.go index d570cb062..94d33100b 100644 --- a/utils/cache/cached_http_client.go +++ b/utils/cache/cached_http_client.go @@ -49,16 +49,18 @@ func (c *HTTPClient) Do(req *http.Request) (*http.Response, error) { cached = false req, err := c.deserializeReq(key) if err != nil { + log.Trace(req.Context(), "CachedHTTPClient.Do", "key", key, err) return "", 0, err } resp, err := c.hc.Do(req) if err != nil { + log.Trace(req.Context(), "CachedHTTPClient.Do", "req", req, err) return "", 0, err } defer resp.Body.Close() return c.serializeResponse(resp), c.ttl, nil }) - log.Trace(req.Context(), "CachedHTTPClient.Do", "key", key, "cached", cached, "elapsed", time.Since(start)) + log.Trace(req.Context(), "CachedHTTPClient.Do", "key", key, "cached", cached, "elapsed", time.Since(start), err) if err != nil { return nil, err } diff --git a/utils/cache/simple_cache.go b/utils/cache/simple_cache.go index 595a26637..182d1d12a 100644 --- a/utils/cache/simple_cache.go +++ b/utils/cache/simple_cache.go @@ -2,6 +2,7 @@ package cache import ( "errors" + "fmt" "sync/atomic" "time" @@ -74,10 +75,13 @@ func (c *simpleCache[K, V]) Get(key K) (V, error) { } func (c *simpleCache[K, V]) GetWithLoader(key K, loader func(key K) (V, time.Duration, error)) (V, error) { + var err error loaderWrapper := ttlcache.LoaderFunc[K, V]( func(t *ttlcache.Cache[K, V], key K) *ttlcache.Item[K, V] { c.evictExpired() - value, ttl, err := loader(key) + var value V + var ttl time.Duration + value, ttl, err = loader(key) if err != nil { return nil } @@ -87,6 +91,9 @@ func (c *simpleCache[K, V]) GetWithLoader(key K, loader func(key K) (V, time.Dur item := c.data.Get(key, ttlcache.WithLoader[K, V](loaderWrapper)) if item == nil { var zero V + if err != nil { + return zero, fmt.Errorf("cache error: loader returned %w", err) + } return zero, errors.New("item not found") } return item.Value(), nil From cf100c4eb422c63aca3121d541263c811bced527 Mon Sep 17 00:00:00 2001 From: Deluan Date: Thu, 27 Mar 2025 22:50:22 -0400 Subject: [PATCH 30/30] chore(subsonic): update snapshot tests to use version 1.16.1 --- .../Responses AlbumInfo with data should match .JSON | 4 ++-- .../Responses AlbumInfo with data should match .XML | 2 +- .../Responses AlbumInfo without data should match .JSON | 4 ++-- .../Responses AlbumInfo without data should match .XML | 2 +- .../Responses AlbumList with OS data should match .JSON | 4 ++-- .../Responses AlbumList with OS data should match .XML | 2 +- .../Responses AlbumList with data should match .JSON | 4 ++-- .../Responses AlbumList with data should match .XML | 2 +- .../Responses AlbumList without data should match .JSON | 4 ++-- .../Responses AlbumList without data should match .XML | 2 +- .../Responses AlbumWithSongsID3 with data should match .JSON | 4 ++-- .../Responses AlbumWithSongsID3 with data should match .XML | 2 +- ...esponses AlbumWithSongsID3 without data should match .JSON | 4 ++-- ...Responses AlbumWithSongsID3 without data should match .XML | 2 +- ...mWithSongsID3 without data should match OpenSubsonic .JSON | 4 ++-- ...umWithSongsID3 without data should match OpenSubsonic .XML | 2 +- ...Responses Artist with OpenSubsonic data should match .JSON | 4 ++-- .../Responses Artist with OpenSubsonic data should match .XML | 2 +- .../.snapshots/Responses Artist with data should match .JSON | 4 ++-- .../.snapshots/Responses Artist with data should match .XML | 2 +- .../Responses Artist without data should match .JSON | 4 ++-- .../Responses Artist without data should match .XML | 2 +- .../Responses ArtistInfo with data should match .JSON | 4 ++-- .../Responses ArtistInfo with data should match .XML | 2 +- .../Responses ArtistInfo without data should match .JSON | 4 ++-- .../Responses ArtistInfo without data should match .XML | 2 +- .../Responses Bookmarks with data should match .JSON | 4 ++-- .../Responses Bookmarks with data should match .XML | 2 +- .../Responses Bookmarks without data should match .JSON | 4 ++-- .../Responses Bookmarks without data should match .XML | 2 +- .../.snapshots/Responses Child with data should match .JSON | 4 ++-- .../.snapshots/Responses Child with data should match .XML | 2 +- .../Responses Child without data should match .JSON | 4 ++-- .../.snapshots/Responses Child without data should match .XML | 2 +- ...sponses Child without data should match OpenSubsonic .JSON | 4 ++-- ...esponses Child without data should match OpenSubsonic .XML | 2 +- .../Responses Directory with data should match .JSON | 4 ++-- .../Responses Directory with data should match .XML | 2 +- .../Responses Directory without data should match .JSON | 4 ++-- .../Responses Directory without data should match .XML | 2 +- .../.snapshots/Responses EmptyResponse should match .JSON | 4 ++-- .../.snapshots/Responses EmptyResponse should match .XML | 2 +- .../.snapshots/Responses Genres with data should match .JSON | 4 ++-- .../.snapshots/Responses Genres with data should match .XML | 2 +- .../Responses Genres without data should match .JSON | 4 ++-- .../Responses Genres without data should match .XML | 2 +- .../.snapshots/Responses Indexes with data should match .JSON | 4 ++-- .../.snapshots/Responses Indexes with data should match .XML | 2 +- .../Responses Indexes without data should match .JSON | 4 ++-- .../Responses Indexes without data should match .XML | 2 +- ...sponses InternetRadioStations with data should match .JSON | 4 ++-- ...esponses InternetRadioStations with data should match .XML | 2 +- ...nses InternetRadioStations without data should match .JSON | 4 ++-- ...onses InternetRadioStations without data should match .XML | 2 +- .../responses/.snapshots/Responses License should match .JSON | 4 ++-- .../responses/.snapshots/Responses License should match .XML | 2 +- .../.snapshots/Responses Lyrics with data should match .JSON | 4 ++-- .../.snapshots/Responses Lyrics with data should match .XML | 2 +- .../Responses Lyrics without data should match .JSON | 4 ++-- .../Responses Lyrics without data should match .XML | 2 +- .../Responses LyricsList with data should match .JSON | 4 ++-- .../Responses LyricsList with data should match .XML | 2 +- .../Responses LyricsList without data should match .JSON | 4 ++-- .../Responses LyricsList without data should match .XML | 2 +- .../Responses MusicFolders with data should match .JSON | 4 ++-- .../Responses MusicFolders with data should match .XML | 2 +- .../Responses MusicFolders without data should match .JSON | 4 ++-- .../Responses MusicFolders without data should match .XML | 2 +- ...ponses OpenSubsonicExtensions with data should match .JSON | 4 ++-- ...sponses OpenSubsonicExtensions with data should match .XML | 2 +- ...ses OpenSubsonicExtensions without data should match .JSON | 4 ++-- ...nses OpenSubsonicExtensions without data should match .XML | 2 +- .../Responses PlayQueue with data should match .JSON | 4 ++-- .../Responses PlayQueue with data should match .XML | 2 +- .../Responses PlayQueue without data should match .JSON | 4 ++-- .../Responses PlayQueue without data should match .XML | 2 +- .../Responses Playlists with data should match .JSON | 4 ++-- .../Responses Playlists with data should match .XML | 2 +- .../Responses Playlists without data should match .JSON | 4 ++-- .../Responses Playlists without data should match .XML | 2 +- .../Responses ScanStatus with data should match .JSON | 4 ++-- .../Responses ScanStatus with data should match .XML | 2 +- .../Responses ScanStatus without data should match .JSON | 4 ++-- .../Responses ScanStatus without data should match .XML | 2 +- .../.snapshots/Responses Shares with data should match .JSON | 4 ++-- .../.snapshots/Responses Shares with data should match .XML | 2 +- ...ponses Shares with only required fields should match .JSON | 4 ++-- ...sponses Shares with only required fields should match .XML | 2 +- .../Responses Shares without data should match .JSON | 4 ++-- .../Responses Shares without data should match .XML | 2 +- .../Responses SimilarSongs with data should match .JSON | 4 ++-- .../Responses SimilarSongs with data should match .XML | 2 +- .../Responses SimilarSongs without data should match .JSON | 4 ++-- .../Responses SimilarSongs without data should match .XML | 2 +- .../Responses SimilarSongs2 with data should match .JSON | 4 ++-- .../Responses SimilarSongs2 with data should match .XML | 2 +- .../Responses SimilarSongs2 without data should match .JSON | 4 ++-- .../Responses SimilarSongs2 without data should match .XML | 2 +- .../Responses TopSongs with data should match .JSON | 4 ++-- .../.snapshots/Responses TopSongs with data should match .XML | 2 +- .../Responses TopSongs without data should match .JSON | 4 ++-- .../Responses TopSongs without data should match .XML | 2 +- .../.snapshots/Responses User with data should match .JSON | 4 ++-- .../.snapshots/Responses User with data should match .XML | 2 +- .../.snapshots/Responses User without data should match .JSON | 4 ++-- .../.snapshots/Responses User without data should match .XML | 2 +- .../.snapshots/Responses Users with data should match .JSON | 4 ++-- .../.snapshots/Responses Users with data should match .XML | 2 +- .../Responses Users without data should match .JSON | 4 ++-- .../.snapshots/Responses Users without data should match .XML | 2 +- server/subsonic/responses/responses_test.go | 4 ++-- 111 files changed, 167 insertions(+), 167 deletions(-) diff --git a/server/subsonic/responses/.snapshots/Responses AlbumInfo with data should match .JSON b/server/subsonic/responses/.snapshots/Responses AlbumInfo with data should match .JSON index 329f03ee9..597737fde 100644 --- a/server/subsonic/responses/.snapshots/Responses AlbumInfo with data should match .JSON +++ b/server/subsonic/responses/.snapshots/Responses AlbumInfo with data should match .JSON @@ -1,8 +1,8 @@ { "status": "ok", - "version": "1.8.0", + "version": "1.16.1", "type": "navidrome", - "serverVersion": "v0.0.0", + "serverVersion": "v0.55.0", "openSubsonic": true, "albumInfo": { "notes": "Believe is the twenty-third studio album by American singer-actress Cher...", diff --git a/server/subsonic/responses/.snapshots/Responses AlbumInfo with data should match .XML b/server/subsonic/responses/.snapshots/Responses AlbumInfo with data should match .XML index e06da821f..be7651c14 100644 --- a/server/subsonic/responses/.snapshots/Responses AlbumInfo with data should match .XML +++ b/server/subsonic/responses/.snapshots/Responses AlbumInfo with data should match .XML @@ -1,4 +1,4 @@ - + Believe is the twenty-third studio album by American singer-actress Cher... 03c91c40-49a6-44a7-90e7-a700edf97a62 diff --git a/server/subsonic/responses/.snapshots/Responses AlbumInfo without data should match .JSON b/server/subsonic/responses/.snapshots/Responses AlbumInfo without data should match .JSON index b67514b7e..27f0b26fa 100644 --- a/server/subsonic/responses/.snapshots/Responses AlbumInfo without data should match .JSON +++ b/server/subsonic/responses/.snapshots/Responses AlbumInfo without data should match .JSON @@ -1,8 +1,8 @@ { "status": "ok", - "version": "1.8.0", + "version": "1.16.1", "type": "navidrome", - "serverVersion": "v0.0.0", + "serverVersion": "v0.55.0", "openSubsonic": true, "albumInfo": {} } diff --git a/server/subsonic/responses/.snapshots/Responses AlbumInfo without data should match .XML b/server/subsonic/responses/.snapshots/Responses AlbumInfo without data should match .XML index fa8d0cedd..80aff1358 100644 --- a/server/subsonic/responses/.snapshots/Responses AlbumInfo without data should match .XML +++ b/server/subsonic/responses/.snapshots/Responses AlbumInfo without data should match .XML @@ -1,3 +1,3 @@ - + diff --git a/server/subsonic/responses/.snapshots/Responses AlbumList with OS data should match .JSON b/server/subsonic/responses/.snapshots/Responses AlbumList with OS data should match .JSON index c7bddc312..0db35c37c 100644 --- a/server/subsonic/responses/.snapshots/Responses AlbumList with OS data should match .JSON +++ b/server/subsonic/responses/.snapshots/Responses AlbumList with OS data should match .JSON @@ -1,8 +1,8 @@ { "status": "ok", - "version": "1.8.0", + "version": "1.16.1", "type": "navidrome", - "serverVersion": "v0.0.0", + "serverVersion": "v0.55.0", "openSubsonic": true, "albumList": { "album": [ diff --git a/server/subsonic/responses/.snapshots/Responses AlbumList with OS data should match .XML b/server/subsonic/responses/.snapshots/Responses AlbumList with OS data should match .XML index 33aef53be..07200c0c5 100644 --- a/server/subsonic/responses/.snapshots/Responses AlbumList with OS data should match .XML +++ b/server/subsonic/responses/.snapshots/Responses AlbumList with OS data should match .XML @@ -1,4 +1,4 @@ - + diff --git a/server/subsonic/responses/.snapshots/Responses AlbumList with data should match .JSON b/server/subsonic/responses/.snapshots/Responses AlbumList with data should match .JSON index 80a709997..946378755 100644 --- a/server/subsonic/responses/.snapshots/Responses AlbumList with data should match .JSON +++ b/server/subsonic/responses/.snapshots/Responses AlbumList with data should match .JSON @@ -1,8 +1,8 @@ { "status": "ok", - "version": "1.8.0", + "version": "1.16.1", "type": "navidrome", - "serverVersion": "v0.0.0", + "serverVersion": "v0.55.0", "openSubsonic": true, "albumList": { "album": [ diff --git a/server/subsonic/responses/.snapshots/Responses AlbumList with data should match .XML b/server/subsonic/responses/.snapshots/Responses AlbumList with data should match .XML index 5f171e72a..000b8c00c 100644 --- a/server/subsonic/responses/.snapshots/Responses AlbumList with data should match .XML +++ b/server/subsonic/responses/.snapshots/Responses AlbumList with data should match .XML @@ -1,4 +1,4 @@ - + diff --git a/server/subsonic/responses/.snapshots/Responses AlbumList without data should match .JSON b/server/subsonic/responses/.snapshots/Responses AlbumList without data should match .JSON index 4a668e5a1..706eefc08 100644 --- a/server/subsonic/responses/.snapshots/Responses AlbumList without data should match .JSON +++ b/server/subsonic/responses/.snapshots/Responses AlbumList without data should match .JSON @@ -1,8 +1,8 @@ { "status": "ok", - "version": "1.8.0", + "version": "1.16.1", "type": "navidrome", - "serverVersion": "v0.0.0", + "serverVersion": "v0.55.0", "openSubsonic": true, "albumList": {} } diff --git a/server/subsonic/responses/.snapshots/Responses AlbumList without data should match .XML b/server/subsonic/responses/.snapshots/Responses AlbumList without data should match .XML index 54a9a774e..d3012157e 100644 --- a/server/subsonic/responses/.snapshots/Responses AlbumList without data should match .XML +++ b/server/subsonic/responses/.snapshots/Responses AlbumList without data should match .XML @@ -1,3 +1,3 @@ - + diff --git a/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 with data should match .JSON b/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 with data should match .JSON index 9f7d8c6b8..c3ae3ee20 100644 --- a/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 with data should match .JSON +++ b/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 with data should match .JSON @@ -1,8 +1,8 @@ { "status": "ok", - "version": "1.8.0", + "version": "1.16.1", "type": "navidrome", - "serverVersion": "v0.0.0", + "serverVersion": "v0.55.0", "openSubsonic": true, "album": { "id": "1", diff --git a/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 with data should match .XML b/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 with data should match .XML index 98545905a..a02c0feee 100644 --- a/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 with data should match .XML +++ b/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 with data should match .XML @@ -1,4 +1,4 @@ - + diff --git a/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 without data should match .JSON b/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 without data should match .JSON index a9e38c9be..fbeded48a 100644 --- a/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 without data should match .JSON +++ b/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 without data should match .JSON @@ -1,8 +1,8 @@ { "status": "ok", - "version": "1.8.0", + "version": "1.16.1", "type": "navidrome", - "serverVersion": "v0.0.0", + "serverVersion": "v0.55.0", "openSubsonic": true, "album": { "id": "", diff --git a/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 without data should match .XML b/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 without data should match .XML index 43189f2a3..159967c1d 100644 --- a/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 without data should match .XML +++ b/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 without data should match .XML @@ -1,3 +1,3 @@ - + diff --git a/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 without data should match OpenSubsonic .JSON b/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 without data should match OpenSubsonic .JSON index d179e628a..758aef0cb 100644 --- a/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 without data should match OpenSubsonic .JSON +++ b/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 without data should match OpenSubsonic .JSON @@ -1,8 +1,8 @@ { "status": "ok", - "version": "1.8.0", + "version": "1.16.1", "type": "navidrome", - "serverVersion": "v0.0.0", + "serverVersion": "v0.55.0", "openSubsonic": true, "album": { "id": "", diff --git a/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 without data should match OpenSubsonic .XML b/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 without data should match OpenSubsonic .XML index 43189f2a3..159967c1d 100644 --- a/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 without data should match OpenSubsonic .XML +++ b/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 without data should match OpenSubsonic .XML @@ -1,3 +1,3 @@ - + diff --git a/server/subsonic/responses/.snapshots/Responses Artist with OpenSubsonic data should match .JSON b/server/subsonic/responses/.snapshots/Responses Artist with OpenSubsonic data should match .JSON index f7d701d03..71d365dda 100644 --- a/server/subsonic/responses/.snapshots/Responses Artist with OpenSubsonic data should match .JSON +++ b/server/subsonic/responses/.snapshots/Responses Artist with OpenSubsonic data should match .JSON @@ -1,8 +1,8 @@ { "status": "ok", - "version": "1.8.0", + "version": "1.16.1", "type": "navidrome", - "serverVersion": "v0.0.0", + "serverVersion": "v0.55.0", "openSubsonic": true, "artists": { "index": [ diff --git a/server/subsonic/responses/.snapshots/Responses Artist with OpenSubsonic data should match .XML b/server/subsonic/responses/.snapshots/Responses Artist with OpenSubsonic data should match .XML index 630ef919b..799d21054 100644 --- a/server/subsonic/responses/.snapshots/Responses Artist with OpenSubsonic data should match .XML +++ b/server/subsonic/responses/.snapshots/Responses Artist with OpenSubsonic data should match .XML @@ -1,4 +1,4 @@ - + diff --git a/server/subsonic/responses/.snapshots/Responses Artist with data should match .JSON b/server/subsonic/responses/.snapshots/Responses Artist with data should match .JSON index e6c74332c..f60df3ebf 100644 --- a/server/subsonic/responses/.snapshots/Responses Artist with data should match .JSON +++ b/server/subsonic/responses/.snapshots/Responses Artist with data should match .JSON @@ -1,8 +1,8 @@ { "status": "ok", - "version": "1.8.0", + "version": "1.16.1", "type": "navidrome", - "serverVersion": "v0.0.0", + "serverVersion": "v0.55.0", "openSubsonic": true, "artists": { "index": [ diff --git a/server/subsonic/responses/.snapshots/Responses Artist with data should match .XML b/server/subsonic/responses/.snapshots/Responses Artist with data should match .XML index 1e3aaba16..21bea828c 100644 --- a/server/subsonic/responses/.snapshots/Responses Artist with data should match .XML +++ b/server/subsonic/responses/.snapshots/Responses Artist with data should match .XML @@ -1,4 +1,4 @@ - + diff --git a/server/subsonic/responses/.snapshots/Responses Artist without data should match .JSON b/server/subsonic/responses/.snapshots/Responses Artist without data should match .JSON index b4b504f6e..74bb5683b 100644 --- a/server/subsonic/responses/.snapshots/Responses Artist without data should match .JSON +++ b/server/subsonic/responses/.snapshots/Responses Artist without data should match .JSON @@ -1,8 +1,8 @@ { "status": "ok", - "version": "1.8.0", + "version": "1.16.1", "type": "navidrome", - "serverVersion": "v0.0.0", + "serverVersion": "v0.55.0", "openSubsonic": true, "artists": { "lastModified": 1, diff --git a/server/subsonic/responses/.snapshots/Responses Artist without data should match .XML b/server/subsonic/responses/.snapshots/Responses Artist without data should match .XML index 01fda5620..781599731 100644 --- a/server/subsonic/responses/.snapshots/Responses Artist without data should match .XML +++ b/server/subsonic/responses/.snapshots/Responses Artist without data should match .XML @@ -1,3 +1,3 @@ - + diff --git a/server/subsonic/responses/.snapshots/Responses ArtistInfo with data should match .JSON b/server/subsonic/responses/.snapshots/Responses ArtistInfo with data should match .JSON index d062e9c20..2edaa7edc 100644 --- a/server/subsonic/responses/.snapshots/Responses ArtistInfo with data should match .JSON +++ b/server/subsonic/responses/.snapshots/Responses ArtistInfo with data should match .JSON @@ -1,8 +1,8 @@ { "status": "ok", - "version": "1.8.0", + "version": "1.16.1", "type": "navidrome", - "serverVersion": "v0.0.0", + "serverVersion": "v0.55.0", "openSubsonic": true, "artistInfo": { "biography": "Black Sabbath is an English \u003ca target='_blank' href=\"https://www.last.fm/tag/heavy%20metal\" class=\"bbcode_tag\" rel=\"tag\"\u003eheavy metal\u003c/a\u003e band", diff --git a/server/subsonic/responses/.snapshots/Responses ArtistInfo with data should match .XML b/server/subsonic/responses/.snapshots/Responses ArtistInfo with data should match .XML index ce0dda0d8..16c6c5fe0 100644 --- a/server/subsonic/responses/.snapshots/Responses ArtistInfo with data should match .XML +++ b/server/subsonic/responses/.snapshots/Responses ArtistInfo with data should match .XML @@ -1,4 +1,4 @@ - + Black Sabbath is an English <a target='_blank' href="https://www.last.fm/tag/heavy%20metal" class="bbcode_tag" rel="tag">heavy metal</a> band 5182c1d9-c7d2-4dad-afa0-ccfeada921a8 diff --git a/server/subsonic/responses/.snapshots/Responses ArtistInfo without data should match .JSON b/server/subsonic/responses/.snapshots/Responses ArtistInfo without data should match .JSON index 215bd61b5..8e2807982 100644 --- a/server/subsonic/responses/.snapshots/Responses ArtistInfo without data should match .JSON +++ b/server/subsonic/responses/.snapshots/Responses ArtistInfo without data should match .JSON @@ -1,8 +1,8 @@ { "status": "ok", - "version": "1.8.0", + "version": "1.16.1", "type": "navidrome", - "serverVersion": "v0.0.0", + "serverVersion": "v0.55.0", "openSubsonic": true, "artistInfo": {} } diff --git a/server/subsonic/responses/.snapshots/Responses ArtistInfo without data should match .XML b/server/subsonic/responses/.snapshots/Responses ArtistInfo without data should match .XML index cc4fe25be..16f0ad2c5 100644 --- a/server/subsonic/responses/.snapshots/Responses ArtistInfo without data should match .XML +++ b/server/subsonic/responses/.snapshots/Responses ArtistInfo without data should match .XML @@ -1,3 +1,3 @@ - + diff --git a/server/subsonic/responses/.snapshots/Responses Bookmarks with data should match .JSON b/server/subsonic/responses/.snapshots/Responses Bookmarks with data should match .JSON index 0cf51c8d5..7ca38d4db 100644 --- a/server/subsonic/responses/.snapshots/Responses Bookmarks with data should match .JSON +++ b/server/subsonic/responses/.snapshots/Responses Bookmarks with data should match .JSON @@ -1,8 +1,8 @@ { "status": "ok", - "version": "1.8.0", + "version": "1.16.1", "type": "navidrome", - "serverVersion": "v0.0.0", + "serverVersion": "v0.55.0", "openSubsonic": true, "bookmarks": { "bookmark": [ diff --git a/server/subsonic/responses/.snapshots/Responses Bookmarks with data should match .XML b/server/subsonic/responses/.snapshots/Responses Bookmarks with data should match .XML index ef2443428..66c57820e 100644 --- a/server/subsonic/responses/.snapshots/Responses Bookmarks with data should match .XML +++ b/server/subsonic/responses/.snapshots/Responses Bookmarks with data should match .XML @@ -1,4 +1,4 @@ - + diff --git a/server/subsonic/responses/.snapshots/Responses Bookmarks without data should match .JSON b/server/subsonic/responses/.snapshots/Responses Bookmarks without data should match .JSON index 693beb1bc..267b06eea 100644 --- a/server/subsonic/responses/.snapshots/Responses Bookmarks without data should match .JSON +++ b/server/subsonic/responses/.snapshots/Responses Bookmarks without data should match .JSON @@ -1,8 +1,8 @@ { "status": "ok", - "version": "1.8.0", + "version": "1.16.1", "type": "navidrome", - "serverVersion": "v0.0.0", + "serverVersion": "v0.55.0", "openSubsonic": true, "bookmarks": {} } diff --git a/server/subsonic/responses/.snapshots/Responses Bookmarks without data should match .XML b/server/subsonic/responses/.snapshots/Responses Bookmarks without data should match .XML index f1365599c..c0f16179a 100644 --- a/server/subsonic/responses/.snapshots/Responses Bookmarks without data should match .XML +++ b/server/subsonic/responses/.snapshots/Responses Bookmarks without data should match .XML @@ -1,3 +1,3 @@ - + diff --git a/server/subsonic/responses/.snapshots/Responses Child with data should match .JSON b/server/subsonic/responses/.snapshots/Responses Child with data should match .JSON index c3290868b..13aa1f187 100644 --- a/server/subsonic/responses/.snapshots/Responses Child with data should match .JSON +++ b/server/subsonic/responses/.snapshots/Responses Child with data should match .JSON @@ -1,8 +1,8 @@ { "status": "ok", - "version": "1.8.0", + "version": "1.16.1", "type": "navidrome", - "serverVersion": "v0.0.0", + "serverVersion": "v0.55.0", "openSubsonic": true, "directory": { "child": [ diff --git a/server/subsonic/responses/.snapshots/Responses Child with data should match .XML b/server/subsonic/responses/.snapshots/Responses Child with data should match .XML index a565f279c..477892ac7 100644 --- a/server/subsonic/responses/.snapshots/Responses Child with data should match .XML +++ b/server/subsonic/responses/.snapshots/Responses Child with data should match .XML @@ -1,4 +1,4 @@ - + diff --git a/server/subsonic/responses/.snapshots/Responses Child without data should match .JSON b/server/subsonic/responses/.snapshots/Responses Child without data should match .JSON index ddcc45bd8..66b49830f 100644 --- a/server/subsonic/responses/.snapshots/Responses Child without data should match .JSON +++ b/server/subsonic/responses/.snapshots/Responses Child without data should match .JSON @@ -1,8 +1,8 @@ { "status": "ok", - "version": "1.8.0", + "version": "1.16.1", "type": "navidrome", - "serverVersion": "v0.0.0", + "serverVersion": "v0.55.0", "openSubsonic": true, "directory": { "child": [ diff --git a/server/subsonic/responses/.snapshots/Responses Child without data should match .XML b/server/subsonic/responses/.snapshots/Responses Child without data should match .XML index fc33a139c..d43b9d3ef 100644 --- a/server/subsonic/responses/.snapshots/Responses Child without data should match .XML +++ b/server/subsonic/responses/.snapshots/Responses Child without data should match .XML @@ -1,4 +1,4 @@ - + diff --git a/server/subsonic/responses/.snapshots/Responses Child without data should match OpenSubsonic .JSON b/server/subsonic/responses/.snapshots/Responses Child without data should match OpenSubsonic .JSON index 4b8ac19ba..5dc0e8eb8 100644 --- a/server/subsonic/responses/.snapshots/Responses Child without data should match OpenSubsonic .JSON +++ b/server/subsonic/responses/.snapshots/Responses Child without data should match OpenSubsonic .JSON @@ -1,8 +1,8 @@ { "status": "ok", - "version": "1.8.0", + "version": "1.16.1", "type": "navidrome", - "serverVersion": "v0.0.0", + "serverVersion": "v0.55.0", "openSubsonic": true, "directory": { "child": [ diff --git a/server/subsonic/responses/.snapshots/Responses Child without data should match OpenSubsonic .XML b/server/subsonic/responses/.snapshots/Responses Child without data should match OpenSubsonic .XML index fc33a139c..d43b9d3ef 100644 --- a/server/subsonic/responses/.snapshots/Responses Child without data should match OpenSubsonic .XML +++ b/server/subsonic/responses/.snapshots/Responses Child without data should match OpenSubsonic .XML @@ -1,4 +1,4 @@ - + diff --git a/server/subsonic/responses/.snapshots/Responses Directory with data should match .JSON b/server/subsonic/responses/.snapshots/Responses Directory with data should match .JSON index 6138cbb00..daa7b9c7e 100644 --- a/server/subsonic/responses/.snapshots/Responses Directory with data should match .JSON +++ b/server/subsonic/responses/.snapshots/Responses Directory with data should match .JSON @@ -1,8 +1,8 @@ { "status": "ok", - "version": "1.8.0", + "version": "1.16.1", "type": "navidrome", - "serverVersion": "v0.0.0", + "serverVersion": "v0.55.0", "openSubsonic": true, "directory": { "child": [ diff --git a/server/subsonic/responses/.snapshots/Responses Directory with data should match .XML b/server/subsonic/responses/.snapshots/Responses Directory with data should match .XML index 8b256a111..2ac4f9529 100644 --- a/server/subsonic/responses/.snapshots/Responses Directory with data should match .XML +++ b/server/subsonic/responses/.snapshots/Responses Directory with data should match .XML @@ -1,4 +1,4 @@ - + diff --git a/server/subsonic/responses/.snapshots/Responses Directory without data should match .JSON b/server/subsonic/responses/.snapshots/Responses Directory without data should match .JSON index 9636d1b7a..c76abb908 100644 --- a/server/subsonic/responses/.snapshots/Responses Directory without data should match .JSON +++ b/server/subsonic/responses/.snapshots/Responses Directory without data should match .JSON @@ -1,8 +1,8 @@ { "status": "ok", - "version": "1.8.0", + "version": "1.16.1", "type": "navidrome", - "serverVersion": "v0.0.0", + "serverVersion": "v0.55.0", "openSubsonic": true, "directory": { "id": "1", diff --git a/server/subsonic/responses/.snapshots/Responses Directory without data should match .XML b/server/subsonic/responses/.snapshots/Responses Directory without data should match .XML index 44b989908..1c1f1d2ad 100644 --- a/server/subsonic/responses/.snapshots/Responses Directory without data should match .XML +++ b/server/subsonic/responses/.snapshots/Responses Directory without data should match .XML @@ -1,3 +1,3 @@ - + diff --git a/server/subsonic/responses/.snapshots/Responses EmptyResponse should match .JSON b/server/subsonic/responses/.snapshots/Responses EmptyResponse should match .JSON index 0972d329e..d53ba841f 100644 --- a/server/subsonic/responses/.snapshots/Responses EmptyResponse should match .JSON +++ b/server/subsonic/responses/.snapshots/Responses EmptyResponse should match .JSON @@ -1,7 +1,7 @@ { "status": "ok", - "version": "1.8.0", + "version": "1.16.1", "type": "navidrome", - "serverVersion": "v0.0.0", + "serverVersion": "v0.55.0", "openSubsonic": true } diff --git a/server/subsonic/responses/.snapshots/Responses EmptyResponse should match .XML b/server/subsonic/responses/.snapshots/Responses EmptyResponse should match .XML index 651d6df0d..184228a0e 100644 --- a/server/subsonic/responses/.snapshots/Responses EmptyResponse should match .XML +++ b/server/subsonic/responses/.snapshots/Responses EmptyResponse should match .XML @@ -1 +1 @@ - + diff --git a/server/subsonic/responses/.snapshots/Responses Genres with data should match .JSON b/server/subsonic/responses/.snapshots/Responses Genres with data should match .JSON index b38c97361..90d86535a 100644 --- a/server/subsonic/responses/.snapshots/Responses Genres with data should match .JSON +++ b/server/subsonic/responses/.snapshots/Responses Genres with data should match .JSON @@ -1,8 +1,8 @@ { "status": "ok", - "version": "1.8.0", + "version": "1.16.1", "type": "navidrome", - "serverVersion": "v0.0.0", + "serverVersion": "v0.55.0", "openSubsonic": true, "genres": { "genre": [ diff --git a/server/subsonic/responses/.snapshots/Responses Genres with data should match .XML b/server/subsonic/responses/.snapshots/Responses Genres with data should match .XML index 02034e7af..75497c403 100644 --- a/server/subsonic/responses/.snapshots/Responses Genres with data should match .XML +++ b/server/subsonic/responses/.snapshots/Responses Genres with data should match .XML @@ -1,4 +1,4 @@ - + Rock Reggae diff --git a/server/subsonic/responses/.snapshots/Responses Genres without data should match .JSON b/server/subsonic/responses/.snapshots/Responses Genres without data should match .JSON index 45c5a7bca..0e473a617 100644 --- a/server/subsonic/responses/.snapshots/Responses Genres without data should match .JSON +++ b/server/subsonic/responses/.snapshots/Responses Genres without data should match .JSON @@ -1,8 +1,8 @@ { "status": "ok", - "version": "1.8.0", + "version": "1.16.1", "type": "navidrome", - "serverVersion": "v0.0.0", + "serverVersion": "v0.55.0", "openSubsonic": true, "genres": {} } diff --git a/server/subsonic/responses/.snapshots/Responses Genres without data should match .XML b/server/subsonic/responses/.snapshots/Responses Genres without data should match .XML index d0a66c3e0..4f4217d43 100644 --- a/server/subsonic/responses/.snapshots/Responses Genres without data should match .XML +++ b/server/subsonic/responses/.snapshots/Responses Genres without data should match .XML @@ -1,3 +1,3 @@ - + diff --git a/server/subsonic/responses/.snapshots/Responses Indexes with data should match .JSON b/server/subsonic/responses/.snapshots/Responses Indexes with data should match .JSON index 9f835da1a..9704eab58 100644 --- a/server/subsonic/responses/.snapshots/Responses Indexes with data should match .JSON +++ b/server/subsonic/responses/.snapshots/Responses Indexes with data should match .JSON @@ -1,8 +1,8 @@ { "status": "ok", - "version": "1.8.0", + "version": "1.16.1", "type": "navidrome", - "serverVersion": "v0.0.0", + "serverVersion": "v0.55.0", "openSubsonic": true, "indexes": { "index": [ diff --git a/server/subsonic/responses/.snapshots/Responses Indexes with data should match .XML b/server/subsonic/responses/.snapshots/Responses Indexes with data should match .XML index 595f2ff03..6fc70b498 100644 --- a/server/subsonic/responses/.snapshots/Responses Indexes with data should match .XML +++ b/server/subsonic/responses/.snapshots/Responses Indexes with data should match .XML @@ -1,4 +1,4 @@ - + diff --git a/server/subsonic/responses/.snapshots/Responses Indexes without data should match .JSON b/server/subsonic/responses/.snapshots/Responses Indexes without data should match .JSON index 4dbdc3617..e267fcc01 100644 --- a/server/subsonic/responses/.snapshots/Responses Indexes without data should match .JSON +++ b/server/subsonic/responses/.snapshots/Responses Indexes without data should match .JSON @@ -1,8 +1,8 @@ { "status": "ok", - "version": "1.8.0", + "version": "1.16.1", "type": "navidrome", - "serverVersion": "v0.0.0", + "serverVersion": "v0.55.0", "openSubsonic": true, "indexes": { "lastModified": 1, diff --git a/server/subsonic/responses/.snapshots/Responses Indexes without data should match .XML b/server/subsonic/responses/.snapshots/Responses Indexes without data should match .XML index fad3a53e4..f433b62bc 100644 --- a/server/subsonic/responses/.snapshots/Responses Indexes without data should match .XML +++ b/server/subsonic/responses/.snapshots/Responses Indexes without data should match .XML @@ -1,3 +1,3 @@ - + diff --git a/server/subsonic/responses/.snapshots/Responses InternetRadioStations with data should match .JSON b/server/subsonic/responses/.snapshots/Responses InternetRadioStations with data should match .JSON index 355523605..5762011ae 100644 --- a/server/subsonic/responses/.snapshots/Responses InternetRadioStations with data should match .JSON +++ b/server/subsonic/responses/.snapshots/Responses InternetRadioStations with data should match .JSON @@ -1,8 +1,8 @@ { "status": "ok", - "version": "1.8.0", + "version": "1.16.1", "type": "navidrome", - "serverVersion": "v0.0.0", + "serverVersion": "v0.55.0", "openSubsonic": true, "internetRadioStations": { "internetRadioStation": [ diff --git a/server/subsonic/responses/.snapshots/Responses InternetRadioStations with data should match .XML b/server/subsonic/responses/.snapshots/Responses InternetRadioStations with data should match .XML index bf65d41d2..24cd687c5 100644 --- a/server/subsonic/responses/.snapshots/Responses InternetRadioStations with data should match .XML +++ b/server/subsonic/responses/.snapshots/Responses InternetRadioStations with data should match .XML @@ -1,4 +1,4 @@ - + diff --git a/server/subsonic/responses/.snapshots/Responses InternetRadioStations without data should match .JSON b/server/subsonic/responses/.snapshots/Responses InternetRadioStations without data should match .JSON index f4cee5c84..30d81f29d 100644 --- a/server/subsonic/responses/.snapshots/Responses InternetRadioStations without data should match .JSON +++ b/server/subsonic/responses/.snapshots/Responses InternetRadioStations without data should match .JSON @@ -1,8 +1,8 @@ { "status": "ok", - "version": "1.8.0", + "version": "1.16.1", "type": "navidrome", - "serverVersion": "v0.0.0", + "serverVersion": "v0.55.0", "openSubsonic": true, "internetRadioStations": {} } diff --git a/server/subsonic/responses/.snapshots/Responses InternetRadioStations without data should match .XML b/server/subsonic/responses/.snapshots/Responses InternetRadioStations without data should match .XML index 1c5ae82a9..ba81e4215 100644 --- a/server/subsonic/responses/.snapshots/Responses InternetRadioStations without data should match .XML +++ b/server/subsonic/responses/.snapshots/Responses InternetRadioStations without data should match .XML @@ -1,3 +1,3 @@ - + diff --git a/server/subsonic/responses/.snapshots/Responses License should match .JSON b/server/subsonic/responses/.snapshots/Responses License should match .JSON index 4052c5491..00f3ab7cb 100644 --- a/server/subsonic/responses/.snapshots/Responses License should match .JSON +++ b/server/subsonic/responses/.snapshots/Responses License should match .JSON @@ -1,8 +1,8 @@ { "status": "ok", - "version": "1.8.0", + "version": "1.16.1", "type": "navidrome", - "serverVersion": "v0.0.0", + "serverVersion": "v0.55.0", "openSubsonic": true, "license": { "valid": true diff --git a/server/subsonic/responses/.snapshots/Responses License should match .XML b/server/subsonic/responses/.snapshots/Responses License should match .XML index dc56efabc..f892e6f95 100644 --- a/server/subsonic/responses/.snapshots/Responses License should match .XML +++ b/server/subsonic/responses/.snapshots/Responses License should match .XML @@ -1,3 +1,3 @@ - + diff --git a/server/subsonic/responses/.snapshots/Responses Lyrics with data should match .JSON b/server/subsonic/responses/.snapshots/Responses Lyrics with data should match .JSON index 35833e00a..e2c2b4dbf 100644 --- a/server/subsonic/responses/.snapshots/Responses Lyrics with data should match .JSON +++ b/server/subsonic/responses/.snapshots/Responses Lyrics with data should match .JSON @@ -1,8 +1,8 @@ { "status": "ok", - "version": "1.8.0", + "version": "1.16.1", "type": "navidrome", - "serverVersion": "v0.0.0", + "serverVersion": "v0.55.0", "openSubsonic": true, "lyrics": { "artist": "Rick Astley", diff --git a/server/subsonic/responses/.snapshots/Responses Lyrics with data should match .XML b/server/subsonic/responses/.snapshots/Responses Lyrics with data should match .XML index 51f0032d4..52c0ff39b 100644 --- a/server/subsonic/responses/.snapshots/Responses Lyrics with data should match .XML +++ b/server/subsonic/responses/.snapshots/Responses Lyrics with data should match .XML @@ -1,3 +1,3 @@ - + Never gonna give you up Never gonna let you down Never gonna run around and desert you Never gonna say goodbye diff --git a/server/subsonic/responses/.snapshots/Responses Lyrics without data should match .JSON b/server/subsonic/responses/.snapshots/Responses Lyrics without data should match .JSON index 1094e9e1f..d6d40298a 100644 --- a/server/subsonic/responses/.snapshots/Responses Lyrics without data should match .JSON +++ b/server/subsonic/responses/.snapshots/Responses Lyrics without data should match .JSON @@ -1,8 +1,8 @@ { "status": "ok", - "version": "1.8.0", + "version": "1.16.1", "type": "navidrome", - "serverVersion": "v0.0.0", + "serverVersion": "v0.55.0", "openSubsonic": true, "lyrics": { "value": "" diff --git a/server/subsonic/responses/.snapshots/Responses Lyrics without data should match .XML b/server/subsonic/responses/.snapshots/Responses Lyrics without data should match .XML index cc1821d78..d7fcb284e 100644 --- a/server/subsonic/responses/.snapshots/Responses Lyrics without data should match .XML +++ b/server/subsonic/responses/.snapshots/Responses Lyrics without data should match .XML @@ -1,3 +1,3 @@ - + diff --git a/server/subsonic/responses/.snapshots/Responses LyricsList with data should match .JSON b/server/subsonic/responses/.snapshots/Responses LyricsList with data should match .JSON index c855a660e..e027d62e6 100644 --- a/server/subsonic/responses/.snapshots/Responses LyricsList with data should match .JSON +++ b/server/subsonic/responses/.snapshots/Responses LyricsList with data should match .JSON @@ -1,8 +1,8 @@ { "status": "ok", - "version": "1.8.0", + "version": "1.16.1", "type": "navidrome", - "serverVersion": "v0.0.0", + "serverVersion": "v0.55.0", "openSubsonic": true, "lyricsList": { "structuredLyrics": [ diff --git a/server/subsonic/responses/.snapshots/Responses LyricsList with data should match .XML b/server/subsonic/responses/.snapshots/Responses LyricsList with data should match .XML index 262b1d390..0f1c6c565 100644 --- a/server/subsonic/responses/.snapshots/Responses LyricsList with data should match .XML +++ b/server/subsonic/responses/.snapshots/Responses LyricsList with data should match .XML @@ -1,4 +1,4 @@ - + We're no strangers to love diff --git a/server/subsonic/responses/.snapshots/Responses LyricsList without data should match .JSON b/server/subsonic/responses/.snapshots/Responses LyricsList without data should match .JSON index 876cc71ce..c552df1b0 100644 --- a/server/subsonic/responses/.snapshots/Responses LyricsList without data should match .JSON +++ b/server/subsonic/responses/.snapshots/Responses LyricsList without data should match .JSON @@ -1,8 +1,8 @@ { "status": "ok", - "version": "1.8.0", + "version": "1.16.1", "type": "navidrome", - "serverVersion": "v0.0.0", + "serverVersion": "v0.55.0", "openSubsonic": true, "lyricsList": {} } diff --git a/server/subsonic/responses/.snapshots/Responses LyricsList without data should match .XML b/server/subsonic/responses/.snapshots/Responses LyricsList without data should match .XML index 040cf6b9e..3cc86c32a 100644 --- a/server/subsonic/responses/.snapshots/Responses LyricsList without data should match .XML +++ b/server/subsonic/responses/.snapshots/Responses LyricsList without data should match .XML @@ -1,3 +1,3 @@ - + diff --git a/server/subsonic/responses/.snapshots/Responses MusicFolders with data should match .JSON b/server/subsonic/responses/.snapshots/Responses MusicFolders with data should match .JSON index 016310833..84555b7a2 100644 --- a/server/subsonic/responses/.snapshots/Responses MusicFolders with data should match .JSON +++ b/server/subsonic/responses/.snapshots/Responses MusicFolders with data should match .JSON @@ -1,8 +1,8 @@ { "status": "ok", - "version": "1.8.0", + "version": "1.16.1", "type": "navidrome", - "serverVersion": "v0.0.0", + "serverVersion": "v0.55.0", "openSubsonic": true, "musicFolders": { "musicFolder": [ diff --git a/server/subsonic/responses/.snapshots/Responses MusicFolders with data should match .XML b/server/subsonic/responses/.snapshots/Responses MusicFolders with data should match .XML index 3171c6f23..a9517ea2f 100644 --- a/server/subsonic/responses/.snapshots/Responses MusicFolders with data should match .XML +++ b/server/subsonic/responses/.snapshots/Responses MusicFolders with data should match .XML @@ -1,4 +1,4 @@ - + diff --git a/server/subsonic/responses/.snapshots/Responses MusicFolders without data should match .JSON b/server/subsonic/responses/.snapshots/Responses MusicFolders without data should match .JSON index b2fdd22a1..5c0fb8be8 100644 --- a/server/subsonic/responses/.snapshots/Responses MusicFolders without data should match .JSON +++ b/server/subsonic/responses/.snapshots/Responses MusicFolders without data should match .JSON @@ -1,8 +1,8 @@ { "status": "ok", - "version": "1.8.0", + "version": "1.16.1", "type": "navidrome", - "serverVersion": "v0.0.0", + "serverVersion": "v0.55.0", "openSubsonic": true, "musicFolders": {} } diff --git a/server/subsonic/responses/.snapshots/Responses MusicFolders without data should match .XML b/server/subsonic/responses/.snapshots/Responses MusicFolders without data should match .XML index 12b4ff9ce..5237139a6 100644 --- a/server/subsonic/responses/.snapshots/Responses MusicFolders without data should match .XML +++ b/server/subsonic/responses/.snapshots/Responses MusicFolders without data should match .XML @@ -1,3 +1,3 @@ - + diff --git a/server/subsonic/responses/.snapshots/Responses OpenSubsonicExtensions with data should match .JSON b/server/subsonic/responses/.snapshots/Responses OpenSubsonicExtensions with data should match .JSON index 5e8b33ae3..d3972e7ba 100644 --- a/server/subsonic/responses/.snapshots/Responses OpenSubsonicExtensions with data should match .JSON +++ b/server/subsonic/responses/.snapshots/Responses OpenSubsonicExtensions with data should match .JSON @@ -1,8 +1,8 @@ { "status": "ok", - "version": "1.8.0", + "version": "1.16.1", "type": "navidrome", - "serverVersion": "v0.0.0", + "serverVersion": "v0.55.0", "openSubsonic": true, "openSubsonicExtensions": [ { diff --git a/server/subsonic/responses/.snapshots/Responses OpenSubsonicExtensions with data should match .XML b/server/subsonic/responses/.snapshots/Responses OpenSubsonicExtensions with data should match .XML index 587eda70d..adcb0086b 100644 --- a/server/subsonic/responses/.snapshots/Responses OpenSubsonicExtensions with data should match .XML +++ b/server/subsonic/responses/.snapshots/Responses OpenSubsonicExtensions with data should match .XML @@ -1,4 +1,4 @@ - + 1 2 diff --git a/server/subsonic/responses/.snapshots/Responses OpenSubsonicExtensions without data should match .JSON b/server/subsonic/responses/.snapshots/Responses OpenSubsonicExtensions without data should match .JSON index 143bd1f80..b81ecd039 100644 --- a/server/subsonic/responses/.snapshots/Responses OpenSubsonicExtensions without data should match .JSON +++ b/server/subsonic/responses/.snapshots/Responses OpenSubsonicExtensions without data should match .JSON @@ -1,8 +1,8 @@ { "status": "ok", - "version": "1.8.0", + "version": "1.16.1", "type": "navidrome", - "serverVersion": "v0.0.0", + "serverVersion": "v0.55.0", "openSubsonic": true, "openSubsonicExtensions": [] } diff --git a/server/subsonic/responses/.snapshots/Responses OpenSubsonicExtensions without data should match .XML b/server/subsonic/responses/.snapshots/Responses OpenSubsonicExtensions without data should match .XML index 651d6df0d..184228a0e 100644 --- a/server/subsonic/responses/.snapshots/Responses OpenSubsonicExtensions without data should match .XML +++ b/server/subsonic/responses/.snapshots/Responses OpenSubsonicExtensions without data should match .XML @@ -1 +1 @@ - + diff --git a/server/subsonic/responses/.snapshots/Responses PlayQueue with data should match .JSON b/server/subsonic/responses/.snapshots/Responses PlayQueue with data should match .JSON index 0af76f118..eb771692b 100644 --- a/server/subsonic/responses/.snapshots/Responses PlayQueue with data should match .JSON +++ b/server/subsonic/responses/.snapshots/Responses PlayQueue with data should match .JSON @@ -1,8 +1,8 @@ { "status": "ok", - "version": "1.8.0", + "version": "1.16.1", "type": "navidrome", - "serverVersion": "v0.0.0", + "serverVersion": "v0.55.0", "openSubsonic": true, "playQueue": { "entry": [ diff --git a/server/subsonic/responses/.snapshots/Responses PlayQueue with data should match .XML b/server/subsonic/responses/.snapshots/Responses PlayQueue with data should match .XML index bd9f84979..1156af0a8 100644 --- a/server/subsonic/responses/.snapshots/Responses PlayQueue with data should match .XML +++ b/server/subsonic/responses/.snapshots/Responses PlayQueue with data should match .XML @@ -1,4 +1,4 @@ - + diff --git a/server/subsonic/responses/.snapshots/Responses PlayQueue without data should match .JSON b/server/subsonic/responses/.snapshots/Responses PlayQueue without data should match .JSON index 7af12aeed..88eebb276 100644 --- a/server/subsonic/responses/.snapshots/Responses PlayQueue without data should match .JSON +++ b/server/subsonic/responses/.snapshots/Responses PlayQueue without data should match .JSON @@ -1,8 +1,8 @@ { "status": "ok", - "version": "1.8.0", + "version": "1.16.1", "type": "navidrome", - "serverVersion": "v0.0.0", + "serverVersion": "v0.55.0", "openSubsonic": true, "playQueue": { "username": "", diff --git a/server/subsonic/responses/.snapshots/Responses PlayQueue without data should match .XML b/server/subsonic/responses/.snapshots/Responses PlayQueue without data should match .XML index 1a3e0b527..5af3d9157 100644 --- a/server/subsonic/responses/.snapshots/Responses PlayQueue without data should match .XML +++ b/server/subsonic/responses/.snapshots/Responses PlayQueue without data should match .XML @@ -1,3 +1,3 @@ - + diff --git a/server/subsonic/responses/.snapshots/Responses Playlists with data should match .JSON b/server/subsonic/responses/.snapshots/Responses Playlists with data should match .JSON index 3c87c80bf..b6e996d6e 100644 --- a/server/subsonic/responses/.snapshots/Responses Playlists with data should match .JSON +++ b/server/subsonic/responses/.snapshots/Responses Playlists with data should match .JSON @@ -1,8 +1,8 @@ { "status": "ok", - "version": "1.8.0", + "version": "1.16.1", "type": "navidrome", - "serverVersion": "v0.0.0", + "serverVersion": "v0.55.0", "openSubsonic": true, "playlists": { "playlist": [ diff --git a/server/subsonic/responses/.snapshots/Responses Playlists with data should match .XML b/server/subsonic/responses/.snapshots/Responses Playlists with data should match .XML index 91a71d281..100301afe 100644 --- a/server/subsonic/responses/.snapshots/Responses Playlists with data should match .XML +++ b/server/subsonic/responses/.snapshots/Responses Playlists with data should match .XML @@ -1,4 +1,4 @@ - + diff --git a/server/subsonic/responses/.snapshots/Responses Playlists without data should match .JSON b/server/subsonic/responses/.snapshots/Responses Playlists without data should match .JSON index 4a55658d8..c4510a7eb 100644 --- a/server/subsonic/responses/.snapshots/Responses Playlists without data should match .JSON +++ b/server/subsonic/responses/.snapshots/Responses Playlists without data should match .JSON @@ -1,8 +1,8 @@ { "status": "ok", - "version": "1.8.0", + "version": "1.16.1", "type": "navidrome", - "serverVersion": "v0.0.0", + "serverVersion": "v0.55.0", "openSubsonic": true, "playlists": {} } diff --git a/server/subsonic/responses/.snapshots/Responses Playlists without data should match .XML b/server/subsonic/responses/.snapshots/Responses Playlists without data should match .XML index 0c091fe9f..acdb6732e 100644 --- a/server/subsonic/responses/.snapshots/Responses Playlists without data should match .XML +++ b/server/subsonic/responses/.snapshots/Responses Playlists without data should match .XML @@ -1,3 +1,3 @@ - + diff --git a/server/subsonic/responses/.snapshots/Responses ScanStatus with data should match .JSON b/server/subsonic/responses/.snapshots/Responses ScanStatus with data should match .JSON index 576c59051..af26f09e6 100644 --- a/server/subsonic/responses/.snapshots/Responses ScanStatus with data should match .JSON +++ b/server/subsonic/responses/.snapshots/Responses ScanStatus with data should match .JSON @@ -1,8 +1,8 @@ { "status": "ok", - "version": "1.8.0", + "version": "1.16.1", "type": "navidrome", - "serverVersion": "v0.0.0", + "serverVersion": "v0.55.0", "openSubsonic": true, "scanStatus": { "scanning": true, diff --git a/server/subsonic/responses/.snapshots/Responses ScanStatus with data should match .XML b/server/subsonic/responses/.snapshots/Responses ScanStatus with data should match .XML index fb6432bb8..6ce0dac7b 100644 --- a/server/subsonic/responses/.snapshots/Responses ScanStatus with data should match .XML +++ b/server/subsonic/responses/.snapshots/Responses ScanStatus with data should match .XML @@ -1,3 +1,3 @@ - + diff --git a/server/subsonic/responses/.snapshots/Responses ScanStatus without data should match .JSON b/server/subsonic/responses/.snapshots/Responses ScanStatus without data should match .JSON index d880a2dea..fed45c51c 100644 --- a/server/subsonic/responses/.snapshots/Responses ScanStatus without data should match .JSON +++ b/server/subsonic/responses/.snapshots/Responses ScanStatus without data should match .JSON @@ -1,8 +1,8 @@ { "status": "ok", - "version": "1.8.0", + "version": "1.16.1", "type": "navidrome", - "serverVersion": "v0.0.0", + "serverVersion": "v0.55.0", "openSubsonic": true, "scanStatus": { "scanning": false, diff --git a/server/subsonic/responses/.snapshots/Responses ScanStatus without data should match .XML b/server/subsonic/responses/.snapshots/Responses ScanStatus without data should match .XML index 6e9156eab..8e622d813 100644 --- a/server/subsonic/responses/.snapshots/Responses ScanStatus without data should match .XML +++ b/server/subsonic/responses/.snapshots/Responses ScanStatus without data should match .XML @@ -1,3 +1,3 @@ - + diff --git a/server/subsonic/responses/.snapshots/Responses Shares with data should match .JSON b/server/subsonic/responses/.snapshots/Responses Shares with data should match .JSON index d6103f59e..0c08be37a 100644 --- a/server/subsonic/responses/.snapshots/Responses Shares with data should match .JSON +++ b/server/subsonic/responses/.snapshots/Responses Shares with data should match .JSON @@ -1,8 +1,8 @@ { "status": "ok", - "version": "1.8.0", + "version": "1.16.1", "type": "navidrome", - "serverVersion": "v0.0.0", + "serverVersion": "v0.55.0", "openSubsonic": true, "shares": { "share": [ diff --git a/server/subsonic/responses/.snapshots/Responses Shares with data should match .XML b/server/subsonic/responses/.snapshots/Responses Shares with data should match .XML index d1770496e..36cfc25fe 100644 --- a/server/subsonic/responses/.snapshots/Responses Shares with data should match .XML +++ b/server/subsonic/responses/.snapshots/Responses Shares with data should match .XML @@ -1,4 +1,4 @@ - + diff --git a/server/subsonic/responses/.snapshots/Responses Shares with only required fields should match .JSON b/server/subsonic/responses/.snapshots/Responses Shares with only required fields should match .JSON index cc1e48667..2856ac7f6 100644 --- a/server/subsonic/responses/.snapshots/Responses Shares with only required fields should match .JSON +++ b/server/subsonic/responses/.snapshots/Responses Shares with only required fields should match .JSON @@ -1,8 +1,8 @@ { "status": "ok", - "version": "1.8.0", + "version": "1.16.1", "type": "navidrome", - "serverVersion": "v0.0.0", + "serverVersion": "v0.55.0", "openSubsonic": true, "shares": { "share": [ diff --git a/server/subsonic/responses/.snapshots/Responses Shares with only required fields should match .XML b/server/subsonic/responses/.snapshots/Responses Shares with only required fields should match .XML index e59372b26..12e8f6bea 100644 --- a/server/subsonic/responses/.snapshots/Responses Shares with only required fields should match .XML +++ b/server/subsonic/responses/.snapshots/Responses Shares with only required fields should match .XML @@ -1,4 +1,4 @@ - + diff --git a/server/subsonic/responses/.snapshots/Responses Shares without data should match .JSON b/server/subsonic/responses/.snapshots/Responses Shares without data should match .JSON index 393e1ab32..d05e1407e 100644 --- a/server/subsonic/responses/.snapshots/Responses Shares without data should match .JSON +++ b/server/subsonic/responses/.snapshots/Responses Shares without data should match .JSON @@ -1,8 +1,8 @@ { "status": "ok", - "version": "1.8.0", + "version": "1.16.1", "type": "navidrome", - "serverVersion": "v0.0.0", + "serverVersion": "v0.55.0", "openSubsonic": true, "shares": {} } diff --git a/server/subsonic/responses/.snapshots/Responses Shares without data should match .XML b/server/subsonic/responses/.snapshots/Responses Shares without data should match .XML index 4b9dde4e6..9217c7850 100644 --- a/server/subsonic/responses/.snapshots/Responses Shares without data should match .XML +++ b/server/subsonic/responses/.snapshots/Responses Shares without data should match .XML @@ -1,3 +1,3 @@ - + diff --git a/server/subsonic/responses/.snapshots/Responses SimilarSongs with data should match .JSON b/server/subsonic/responses/.snapshots/Responses SimilarSongs with data should match .JSON index 2fad6fe29..7df08ded1 100644 --- a/server/subsonic/responses/.snapshots/Responses SimilarSongs with data should match .JSON +++ b/server/subsonic/responses/.snapshots/Responses SimilarSongs with data should match .JSON @@ -1,8 +1,8 @@ { "status": "ok", - "version": "1.8.0", + "version": "1.16.1", "type": "navidrome", - "serverVersion": "v0.0.0", + "serverVersion": "v0.55.0", "openSubsonic": true, "similarSongs": { "song": [ diff --git a/server/subsonic/responses/.snapshots/Responses SimilarSongs with data should match .XML b/server/subsonic/responses/.snapshots/Responses SimilarSongs with data should match .XML index 7119e899d..b05443a91 100644 --- a/server/subsonic/responses/.snapshots/Responses SimilarSongs with data should match .XML +++ b/server/subsonic/responses/.snapshots/Responses SimilarSongs with data should match .XML @@ -1,4 +1,4 @@ - + diff --git a/server/subsonic/responses/.snapshots/Responses SimilarSongs without data should match .JSON b/server/subsonic/responses/.snapshots/Responses SimilarSongs without data should match .JSON index 37092e67b..2436e38cf 100644 --- a/server/subsonic/responses/.snapshots/Responses SimilarSongs without data should match .JSON +++ b/server/subsonic/responses/.snapshots/Responses SimilarSongs without data should match .JSON @@ -1,8 +1,8 @@ { "status": "ok", - "version": "1.8.0", + "version": "1.16.1", "type": "navidrome", - "serverVersion": "v0.0.0", + "serverVersion": "v0.55.0", "openSubsonic": true, "similarSongs": {} } diff --git a/server/subsonic/responses/.snapshots/Responses SimilarSongs without data should match .XML b/server/subsonic/responses/.snapshots/Responses SimilarSongs without data should match .XML index 49ffa3ebd..c3e020af0 100644 --- a/server/subsonic/responses/.snapshots/Responses SimilarSongs without data should match .XML +++ b/server/subsonic/responses/.snapshots/Responses SimilarSongs without data should match .XML @@ -1,3 +1,3 @@ - + diff --git a/server/subsonic/responses/.snapshots/Responses SimilarSongs2 with data should match .JSON b/server/subsonic/responses/.snapshots/Responses SimilarSongs2 with data should match .JSON index 9340bb5ee..73eda015e 100644 --- a/server/subsonic/responses/.snapshots/Responses SimilarSongs2 with data should match .JSON +++ b/server/subsonic/responses/.snapshots/Responses SimilarSongs2 with data should match .JSON @@ -1,8 +1,8 @@ { "status": "ok", - "version": "1.8.0", + "version": "1.16.1", "type": "navidrome", - "serverVersion": "v0.0.0", + "serverVersion": "v0.55.0", "openSubsonic": true, "similarSongs2": { "song": [ diff --git a/server/subsonic/responses/.snapshots/Responses SimilarSongs2 with data should match .XML b/server/subsonic/responses/.snapshots/Responses SimilarSongs2 with data should match .XML index c895a03f7..0402f031e 100644 --- a/server/subsonic/responses/.snapshots/Responses SimilarSongs2 with data should match .XML +++ b/server/subsonic/responses/.snapshots/Responses SimilarSongs2 with data should match .XML @@ -1,4 +1,4 @@ - + diff --git a/server/subsonic/responses/.snapshots/Responses SimilarSongs2 without data should match .JSON b/server/subsonic/responses/.snapshots/Responses SimilarSongs2 without data should match .JSON index 24d873e84..1d86c944a 100644 --- a/server/subsonic/responses/.snapshots/Responses SimilarSongs2 without data should match .JSON +++ b/server/subsonic/responses/.snapshots/Responses SimilarSongs2 without data should match .JSON @@ -1,8 +1,8 @@ { "status": "ok", - "version": "1.8.0", + "version": "1.16.1", "type": "navidrome", - "serverVersion": "v0.0.0", + "serverVersion": "v0.55.0", "openSubsonic": true, "similarSongs2": {} } diff --git a/server/subsonic/responses/.snapshots/Responses SimilarSongs2 without data should match .XML b/server/subsonic/responses/.snapshots/Responses SimilarSongs2 without data should match .XML index ef8535e1a..aa301249e 100644 --- a/server/subsonic/responses/.snapshots/Responses SimilarSongs2 without data should match .XML +++ b/server/subsonic/responses/.snapshots/Responses SimilarSongs2 without data should match .XML @@ -1,3 +1,3 @@ - + diff --git a/server/subsonic/responses/.snapshots/Responses TopSongs with data should match .JSON b/server/subsonic/responses/.snapshots/Responses TopSongs with data should match .JSON index 62cf30226..575c9b7fd 100644 --- a/server/subsonic/responses/.snapshots/Responses TopSongs with data should match .JSON +++ b/server/subsonic/responses/.snapshots/Responses TopSongs with data should match .JSON @@ -1,8 +1,8 @@ { "status": "ok", - "version": "1.8.0", + "version": "1.16.1", "type": "navidrome", - "serverVersion": "v0.0.0", + "serverVersion": "v0.55.0", "openSubsonic": true, "topSongs": { "song": [ diff --git a/server/subsonic/responses/.snapshots/Responses TopSongs with data should match .XML b/server/subsonic/responses/.snapshots/Responses TopSongs with data should match .XML index 284de9a2e..35a77cb6c 100644 --- a/server/subsonic/responses/.snapshots/Responses TopSongs with data should match .XML +++ b/server/subsonic/responses/.snapshots/Responses TopSongs with data should match .XML @@ -1,4 +1,4 @@ - + diff --git a/server/subsonic/responses/.snapshots/Responses TopSongs without data should match .JSON b/server/subsonic/responses/.snapshots/Responses TopSongs without data should match .JSON index 1dc04ae36..68ef26569 100644 --- a/server/subsonic/responses/.snapshots/Responses TopSongs without data should match .JSON +++ b/server/subsonic/responses/.snapshots/Responses TopSongs without data should match .JSON @@ -1,8 +1,8 @@ { "status": "ok", - "version": "1.8.0", + "version": "1.16.1", "type": "navidrome", - "serverVersion": "v0.0.0", + "serverVersion": "v0.55.0", "openSubsonic": true, "topSongs": {} } diff --git a/server/subsonic/responses/.snapshots/Responses TopSongs without data should match .XML b/server/subsonic/responses/.snapshots/Responses TopSongs without data should match .XML index 28429110c..74f5d1cb1 100644 --- a/server/subsonic/responses/.snapshots/Responses TopSongs without data should match .XML +++ b/server/subsonic/responses/.snapshots/Responses TopSongs without data should match .XML @@ -1,3 +1,3 @@ - + diff --git a/server/subsonic/responses/.snapshots/Responses User with data should match .JSON b/server/subsonic/responses/.snapshots/Responses User with data should match .JSON index 9581a7f11..94ca289a2 100644 --- a/server/subsonic/responses/.snapshots/Responses User with data should match .JSON +++ b/server/subsonic/responses/.snapshots/Responses User with data should match .JSON @@ -1,8 +1,8 @@ { "status": "ok", - "version": "1.8.0", + "version": "1.16.1", "type": "navidrome", - "serverVersion": "v0.0.0", + "serverVersion": "v0.55.0", "openSubsonic": true, "user": { "username": "deluan", diff --git a/server/subsonic/responses/.snapshots/Responses User with data should match .XML b/server/subsonic/responses/.snapshots/Responses User with data should match .XML index e3dafa529..18fae22f3 100644 --- a/server/subsonic/responses/.snapshots/Responses User with data should match .XML +++ b/server/subsonic/responses/.snapshots/Responses User with data should match .XML @@ -1,4 +1,4 @@ - + 1 diff --git a/server/subsonic/responses/.snapshots/Responses User without data should match .JSON b/server/subsonic/responses/.snapshots/Responses User without data should match .JSON index 8da9efca8..fb7881974 100644 --- a/server/subsonic/responses/.snapshots/Responses User without data should match .JSON +++ b/server/subsonic/responses/.snapshots/Responses User without data should match .JSON @@ -1,8 +1,8 @@ { "status": "ok", - "version": "1.8.0", + "version": "1.16.1", "type": "navidrome", - "serverVersion": "v0.0.0", + "serverVersion": "v0.55.0", "openSubsonic": true, "user": { "username": "deluan", diff --git a/server/subsonic/responses/.snapshots/Responses User without data should match .XML b/server/subsonic/responses/.snapshots/Responses User without data should match .XML index 3ad33d7ed..16ebce7ba 100644 --- a/server/subsonic/responses/.snapshots/Responses User without data should match .XML +++ b/server/subsonic/responses/.snapshots/Responses User without data should match .XML @@ -1,3 +1,3 @@ - + diff --git a/server/subsonic/responses/.snapshots/Responses Users with data should match .JSON b/server/subsonic/responses/.snapshots/Responses Users with data should match .JSON index ba29ba2ef..4688feb9e 100644 --- a/server/subsonic/responses/.snapshots/Responses Users with data should match .JSON +++ b/server/subsonic/responses/.snapshots/Responses Users with data should match .JSON @@ -1,8 +1,8 @@ { "status": "ok", - "version": "1.8.0", + "version": "1.16.1", "type": "navidrome", - "serverVersion": "v0.0.0", + "serverVersion": "v0.55.0", "openSubsonic": true, "users": { "user": [ diff --git a/server/subsonic/responses/.snapshots/Responses Users with data should match .XML b/server/subsonic/responses/.snapshots/Responses Users with data should match .XML index d31105924..f40d32379 100644 --- a/server/subsonic/responses/.snapshots/Responses Users with data should match .XML +++ b/server/subsonic/responses/.snapshots/Responses Users with data should match .XML @@ -1,4 +1,4 @@ - + 1 diff --git a/server/subsonic/responses/.snapshots/Responses Users without data should match .JSON b/server/subsonic/responses/.snapshots/Responses Users without data should match .JSON index 41ecdd67a..96b697300 100644 --- a/server/subsonic/responses/.snapshots/Responses Users without data should match .JSON +++ b/server/subsonic/responses/.snapshots/Responses Users without data should match .JSON @@ -1,8 +1,8 @@ { "status": "ok", - "version": "1.8.0", + "version": "1.16.1", "type": "navidrome", - "serverVersion": "v0.0.0", + "serverVersion": "v0.55.0", "openSubsonic": true, "users": { "user": [ diff --git a/server/subsonic/responses/.snapshots/Responses Users without data should match .XML b/server/subsonic/responses/.snapshots/Responses Users without data should match .XML index fad50ed40..3033ad9bc 100644 --- a/server/subsonic/responses/.snapshots/Responses Users without data should match .XML +++ b/server/subsonic/responses/.snapshots/Responses Users without data should match .XML @@ -1,4 +1,4 @@ - + diff --git a/server/subsonic/responses/responses_test.go b/server/subsonic/responses/responses_test.go index f3796f10a..e484ab2c2 100644 --- a/server/subsonic/responses/responses_test.go +++ b/server/subsonic/responses/responses_test.go @@ -21,9 +21,9 @@ var _ = Describe("Responses", func() { BeforeEach(func() { response = &Subsonic{ Status: StatusOK, - Version: "1.8.0", + Version: "1.16.1", Type: consts.AppName, - ServerVersion: "v0.0.0", + ServerVersion: "v0.55.0", OpenSubsonic: true, } })