From 03640ca93d32ef885b8c684889584376717a6eaa Mon Sep 17 00:00:00 2001
From: Deluan <deluan@navidrome.org>
Date: Tue, 29 Nov 2022 14:40:44 -0500
Subject: [PATCH] Fix background images when BaseURL is specified

---
 cmd/root.go                   |   3 +-
 conf/configtest/configtest.go |  10 ++++
 go.mod                        |   2 +-
 server/backgrounds/handler.go |  13 ++---
 server/serve_index.go         |   4 ++
 server/serve_index_test.go    | 103 +++++++++++++++++++++++++++++-----
 tests/fixtures/index.html     |   5 +-
 ui/public/index.html          |   2 +-
 ui/src/config.js              |   1 -
 9 files changed, 115 insertions(+), 28 deletions(-)
 create mode 100644 conf/configtest/configtest.go

diff --git a/cmd/root.go b/cmd/root.go
index 7b0299a54..0963c7e59 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -5,6 +5,7 @@ import (
 	"errors"
 	"fmt"
 	"os"
+	"strings"
 	"time"
 
 	"github.com/navidrome/navidrome/conf"
@@ -91,7 +92,7 @@ func startServer(ctx context.Context) func() error {
 		if conf.Server.Prometheus.Enabled {
 			a.MountRouter("Prometheus metrics", conf.Server.Prometheus.MetricsPath, promhttp.Handler())
 		}
-		if conf.Server.UILoginBackgroundURL == consts.DefaultUILoginBackgroundURL {
+		if strings.HasPrefix(conf.Server.UILoginBackgroundURL, "/") {
 			a.MountRouter("Background images", consts.DefaultUILoginBackgroundURL, backgrounds.NewHandler())
 		}
 		return a.Run(ctx, fmt.Sprintf("%s:%d", conf.Server.Address, conf.Server.Port))
diff --git a/conf/configtest/configtest.go b/conf/configtest/configtest.go
new file mode 100644
index 000000000..b947e6263
--- /dev/null
+++ b/conf/configtest/configtest.go
@@ -0,0 +1,10 @@
+package configtest
+
+import "github.com/navidrome/navidrome/conf"
+
+func SetupConfig() func() {
+	oldValues := *conf.Server
+	return func() {
+		conf.Server = &oldValues
+	}
+}
diff --git a/go.mod b/go.mod
index 7ca9bcde8..747fb02dd 100644
--- a/go.mod
+++ b/go.mod
@@ -47,6 +47,7 @@ require (
 	golang.org/x/sync v0.1.0
 	golang.org/x/text v0.4.0
 	golang.org/x/tools v0.3.0
+	gopkg.in/yaml.v3 v3.0.1
 )
 
 require (
@@ -236,7 +237,6 @@ require (
 	google.golang.org/protobuf v1.28.1 // indirect
 	gopkg.in/ini.v1 v1.67.0 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
-	gopkg.in/yaml.v3 v3.0.1 // indirect
 	honnef.co/go/tools v0.3.3 // indirect
 	mvdan.cc/gofumpt v0.4.0 // indirect
 	mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed // indirect
diff --git a/server/backgrounds/handler.go b/server/backgrounds/handler.go
index a8f8d6ad2..d081a6128 100644
--- a/server/backgrounds/handler.go
+++ b/server/backgrounds/handler.go
@@ -5,17 +5,16 @@ import (
 	"crypto/rand"
 	"encoding/base64"
 	"errors"
-	"io"
 	"math/big"
 	"net/http"
 	"net/url"
 	"path"
-	"strings"
 	"sync"
 	"time"
 
 	"github.com/navidrome/navidrome/consts"
 	"github.com/navidrome/navidrome/log"
+	"gopkg.in/yaml.v3"
 )
 
 type Handler struct {
@@ -31,7 +30,7 @@ func NewHandler() *Handler {
 	return h
 }
 
-const ndImageServiceURL = "https://www.navidrome.org"
+const ndImageServiceURL = "https://www.navidrome.org/images"
 
 func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	image, err := h.getRandomImage(r.Context())
@@ -42,7 +41,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	http.Redirect(w, r, buildPath(ndImageServiceURL, "backgrounds", image+".jpg"), http.StatusFound)
+	http.Redirect(w, r, buildPath(ndImageServiceURL, image), http.StatusFound)
 }
 
 func (h *Handler) getRandomImage(ctx context.Context) (string, error) {
@@ -73,15 +72,15 @@ func (h *Handler) getImageList(ctx context.Context) ([]string, error) {
 		Timeout: 5 * time.Second,
 	}
 
-	req, _ := http.NewRequestWithContext(ctx, http.MethodGet, buildPath(ndImageServiceURL, "images"), nil)
+	req, _ := http.NewRequestWithContext(ctx, http.MethodGet, buildPath(ndImageServiceURL, "index.yml"), nil)
 	resp, err := c.Do(req)
 	if err != nil {
 		log.Warn(ctx, "Could not get list from image service", err)
 		return nil, err
 	}
 	defer resp.Body.Close()
-	body, err := io.ReadAll(resp.Body)
-	h.list = strings.Split(string(body), "\n")
+	dec := yaml.NewDecoder(resp.Body)
+	err = dec.Decode(&h.list)
 	log.Debug(ctx, "Loaded images from image service", "total", len(h.list), "elapsed", time.Since(start))
 	return h.list, err
 }
diff --git a/server/serve_index.go b/server/serve_index.go
index fe3df021e..01bc4adf7 100644
--- a/server/serve_index.go
+++ b/server/serve_index.go
@@ -6,6 +6,7 @@ import (
 	"io"
 	"io/fs"
 	"net/http"
+	"path"
 	"strings"
 
 	"github.com/navidrome/navidrome/conf"
@@ -53,6 +54,9 @@ func serveIndex(ds model.DataStore, fs fs.FS) http.HandlerFunc {
 			"devShowArtistPage":       conf.Server.DevShowArtistPage,
 			"listenBrainzEnabled":     conf.Server.ListenBrainz.Enabled,
 		}
+		if strings.HasPrefix(conf.Server.UILoginBackgroundURL, "/") {
+			appConfig["loginBackgroundURL"] = path.Join(conf.Server.BaseURL, conf.Server.UILoginBackgroundURL)
+		}
 		auth := handleLoginFromHeaders(ds, r)
 		if auth != nil {
 			appConfig["auth"] = auth
diff --git a/server/serve_index_test.go b/server/serve_index_test.go
index 154ec5c72..7defea659 100644
--- a/server/serve_index_test.go
+++ b/server/serve_index_test.go
@@ -10,6 +10,7 @@ import (
 	"strings"
 
 	"github.com/navidrome/navidrome/conf"
+	"github.com/navidrome/navidrome/conf/configtest"
 	"github.com/navidrome/navidrome/consts"
 	"github.com/navidrome/navidrome/model"
 	"github.com/navidrome/navidrome/tests"
@@ -24,7 +25,7 @@ var _ = Describe("serveIndex", func() {
 
 	BeforeEach(func() {
 		ds = &tests.MockDataStore{MockedUser: mockUser}
-		conf.Server.UILoginBackgroundURL = ""
+		DeferCleanup(configtest.SetupConfig())
 	})
 
 	It("adds app_config to index.html", func() {
@@ -82,17 +83,6 @@ var _ = Describe("serveIndex", func() {
 		Expect(config).To(HaveKeyWithValue("baseURL", "base_url_test"))
 	})
 
-	It("sets the uiLoginBackgroundURL", func() {
-		conf.Server.UILoginBackgroundURL = "my_background_url"
-		r := httptest.NewRequest("GET", "/index.html", nil)
-		w := httptest.NewRecorder()
-
-		serveIndex(ds, fs)(w, r)
-
-		config := extractAppConfig(w.Body.String())
-		Expect(config).To(HaveKeyWithValue("loginBackgroundURL", "my_background_url"))
-	})
-
 	It("sets the welcomeMessage", func() {
 		conf.Server.UIWelcomeMessage = "Hello"
 		r := httptest.NewRequest("GET", "/index.html", nil)
@@ -298,9 +288,94 @@ var _ = Describe("serveIndex", func() {
 		config := extractAppConfig(w.Body.String())
 		Expect(config).To(HaveKeyWithValue("listenBrainzEnabled", true))
 	})
+
+	Describe("loginBackgroundURL", func() {
+		Context("empty BaseURL", func() {
+			BeforeEach(func() {
+				conf.Server.BaseURL = "/"
+			})
+			When("it is the default URL", func() {
+				It("points to the default URL", func() {
+					conf.Server.UILoginBackgroundURL = consts.DefaultUILoginBackgroundURL
+					r := httptest.NewRequest("GET", "/index.html", nil)
+					w := httptest.NewRecorder()
+
+					serveIndex(ds, fs)(w, r)
+
+					config := extractAppConfig(w.Body.String())
+					Expect(config).To(HaveKeyWithValue("loginBackgroundURL", consts.DefaultUILoginBackgroundURL))
+				})
+			})
+			When("it is the default offline URL", func() {
+				It("points to the offline URL", func() {
+					conf.Server.UILoginBackgroundURL = consts.DefaultUILoginBackgroundURLOffline
+					r := httptest.NewRequest("GET", "/index.html", nil)
+					w := httptest.NewRecorder()
+
+					serveIndex(ds, fs)(w, r)
+
+					config := extractAppConfig(w.Body.String())
+					Expect(config).To(HaveKeyWithValue("loginBackgroundURL", consts.DefaultUILoginBackgroundURLOffline))
+				})
+			})
+			When("it is a custom URL", func() {
+				It("points to the offline URL", func() {
+					conf.Server.UILoginBackgroundURL = "https://example.com/images/1.jpg"
+					r := httptest.NewRequest("GET", "/index.html", nil)
+					w := httptest.NewRecorder()
+
+					serveIndex(ds, fs)(w, r)
+
+					config := extractAppConfig(w.Body.String())
+					Expect(config).To(HaveKeyWithValue("loginBackgroundURL", "https://example.com/images/1.jpg"))
+				})
+			})
+		})
+		Context("with a BaseURL", func() {
+			BeforeEach(func() {
+				conf.Server.BaseURL = "/music"
+			})
+			When("it is the default URL", func() {
+				It("points to the default URL with BaseURL prefix", func() {
+					conf.Server.UILoginBackgroundURL = consts.DefaultUILoginBackgroundURL
+					r := httptest.NewRequest("GET", "/index.html", nil)
+					w := httptest.NewRecorder()
+
+					serveIndex(ds, fs)(w, r)
+
+					config := extractAppConfig(w.Body.String())
+					Expect(config).To(HaveKeyWithValue("loginBackgroundURL", "/music"+consts.DefaultUILoginBackgroundURL))
+				})
+			})
+			When("it is the default offline URL", func() {
+				It("points to the offline URL", func() {
+					conf.Server.UILoginBackgroundURL = consts.DefaultUILoginBackgroundURLOffline
+					r := httptest.NewRequest("GET", "/index.html", nil)
+					w := httptest.NewRecorder()
+
+					serveIndex(ds, fs)(w, r)
+
+					config := extractAppConfig(w.Body.String())
+					Expect(config).To(HaveKeyWithValue("loginBackgroundURL", consts.DefaultUILoginBackgroundURLOffline))
+				})
+			})
+			When("it is a custom URL", func() {
+				It("points to the offline URL", func() {
+					conf.Server.UILoginBackgroundURL = "https://example.com/images/1.jpg"
+					r := httptest.NewRequest("GET", "/index.html", nil)
+					w := httptest.NewRecorder()
+
+					serveIndex(ds, fs)(w, r)
+
+					config := extractAppConfig(w.Body.String())
+					Expect(config).To(HaveKeyWithValue("loginBackgroundURL", "https://example.com/images/1.jpg"))
+				})
+			})
+		})
+	})
 })
 
-var appConfigRegex = regexp.MustCompile(`(?m)window.__APP_CONFIG__="([^"]*)`)
+var appConfigRegex = regexp.MustCompile(`(?m)window.__APP_CONFIG__=(.*);</script>`)
 
 func extractAppConfig(body string) map[string]interface{} {
 	config := make(map[string]interface{})
@@ -308,7 +383,7 @@ func extractAppConfig(body string) map[string]interface{} {
 	if match == nil {
 		return config
 	}
-	str, err := strconv.Unquote("\"" + match[1] + "\"")
+	str, err := strconv.Unquote(match[1])
 	if err != nil {
 		panic(fmt.Sprintf("%s: %s", match[1], err))
 	}
diff --git a/tests/fixtures/index.html b/tests/fixtures/index.html
index f93ce189c..53915d86a 100644
--- a/tests/fixtures/index.html
+++ b/tests/fixtures/index.html
@@ -7,9 +7,8 @@
             content="Navidrome Music Server - {{.Version}}"
     />
     <title>Navidrome</title>
-    <script>
-      window.__APP_CONFIG__="{{.AppConfig}}"
-    </script>
+    <!-- The line below has to match the exact format of the equivalent line in ui/build/index.html -->
+    <script>window.__APP_CONFIG__={{ .AppConfig }};</script>
 </head>
 <body>
 </body>
diff --git a/ui/public/index.html b/ui/public/index.html
index 02675cbf2..626d43a8c 100644
--- a/ui/public/index.html
+++ b/ui/public/index.html
@@ -31,7 +31,7 @@
     -->
     <title>Navidrome</title>
     <script>
-      window.__APP_CONFIG__ = "{{.AppConfig}}"
+      window.__APP_CONFIG__ = {{ .AppConfig }}
     </script>
   </head>
   <body>
diff --git a/ui/src/config.js b/ui/src/config.js
index 6f2621106..8ce1f59e7 100644
--- a/ui/src/config.js
+++ b/ui/src/config.js
@@ -34,7 +34,6 @@ let config
 
 try {
   const appConfig = JSON.parse(window.__APP_CONFIG__)
-
   config = {
     ...defaultConfig,
     ...appConfig,