From 37d3cf0e2409f1cee47154a825ad1541295e76f2 Mon Sep 17 00:00:00 2001 From: lanlinux141 <196287462+lanlinux141@users.noreply.github.com> Date: Thu, 10 Sep 2026 19:06:02 -0500 Subject: [PATCH] feat(ui): add configurable instance name - #1046 Signed-off-by: lanlinux141 <196287462+lanlinux141@users.noreply.github.com> --- README.md | 4 ++ conf/configuration.go | 6 +++ conf/configuration_test.go | 27 +++++++++++ server/serve_index.go | 6 ++- server/serve_index_test.go | 20 ++++++++ ui/index.html | 2 +- ui/src/audioplayer/Player.jsx | 6 +-- ui/src/audioplayer/Player.test.jsx | 78 ++++++++++++++++++++++++++++++ ui/src/common/Title.jsx | 10 +++- ui/src/common/Title.test.jsx | 41 ++++++++++++++++ ui/src/config.js | 1 + ui/src/layout/Login.jsx | 16 ++++++ ui/src/layout/Login.test.jsx | 35 ++++++++++++++ 13 files changed, 244 insertions(+), 8 deletions(-) create mode 100644 ui/src/audioplayer/Player.test.jsx create mode 100644 ui/src/common/Title.test.jsx create mode 100644 ui/src/layout/Login.test.jsx diff --git a/README.md b/README.md index 4bc85e6a6..1f4dc1476 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,10 @@ please file a [GitHub issue](https://github.com/navidrome/navidrome/issues) or j See instructions on the [project's website](https://www.navidrome.org/docs/installation/) +Set `InstanceName = "My Music"` (or `ND_INSTANCENAME="My Music"`) to name your +server on the login screen, page header and browser tab. The default is +`Navidrome`, including for blank values. Restart and reload after changing it. + ## Cloud Hosting [PikaPods](https://www.pikapods.com) has partnered with us to offer you an diff --git a/conf/configuration.go b/conf/configuration.go index ff119417a..6d203012d 100644 --- a/conf/configuration.go +++ b/conf/configuration.go @@ -51,6 +51,7 @@ type configOptions struct { TLSKey string UILoginBackgroundURL string UIWelcomeMessage string + InstanceName string MaxSidebarPlaylists int EnableTranscodingConfig bool EnableDownloads bool @@ -379,6 +380,10 @@ func Load(noConfigDump bool) { if err != nil { logFatal("Error parsing config:", err) } + Server.InstanceName = strings.TrimSpace(Server.InstanceName) + if Server.InstanceName == "" { + Server.InstanceName = "Navidrome" + } // Validate non-root user early, before any filesystem operations if err := validateEnforceNonRootUser(); err != nil { @@ -1019,6 +1024,7 @@ func setViperDefaults() { viper.SetDefault("enablestarrating", true) viper.SetDefault("enableuserediting", true) viper.SetDefault("defaulttheme", "Dark") + viper.SetDefault("instancename", "Navidrome") viper.SetDefault("defaultlanguage", "") viper.SetDefault("defaultuivolume", consts.DefaultUIVolume) viper.SetDefault("uisearchdebouncems", consts.DefaultUISearchDebounceMs) diff --git a/conf/configuration_test.go b/conf/configuration_test.go index 2c7f8edaa..8b432e0d7 100644 --- a/conf/configuration_test.go +++ b/conf/configuration_test.go @@ -37,6 +37,33 @@ var _ = Describe("Configuration", func() { })) }) + Describe("InstanceName", func() { + It("defaults to Navidrome", func() { + conf.Load(true) + Expect(conf.Server.InstanceName).To(Equal("Navidrome")) + }) + + DescribeTable("normalizes configured names", func(value, expected string) { + viper.Set("instancename", value) + conf.Load(true) + Expect(conf.Server.InstanceName).To(Equal(expected)) + }, + Entry("custom name", "mp3-player", "mp3-player"), + Entry("surrounding whitespace", " 家の音楽 ", "家の音楽"), + Entry("empty", "", "Navidrome"), + Entry("whitespace", " \t ", "Navidrome"), + ) + + It("loads ND_INSTANCENAME over the config file", func() { + filename := filepath.Join(GinkgoT().TempDir(), "navidrome.toml") + Expect(os.WriteFile(filename, []byte(`InstanceName = "File name"`), 0600)).To(Succeed()) + GinkgoT().Setenv("ND_INSTANCENAME", "mp3-player") + conf.InitConfig(filename, true) + conf.Load(true) + Expect(conf.Server.InstanceName).To(Equal("mp3-player")) + }) + }) + Describe("ParseLanguages", func() { It("parses single language", func() { Expect(conf.ParseLanguages("en")).To(Equal([]string{"en"})) diff --git a/server/serve_index.go b/server/serve_index.go index a538daf1a..038a71292 100644 --- a/server/serve_index.go +++ b/server/serve_index.go @@ -46,6 +46,7 @@ func serveIndex(ds model.DataStore, fs fs.FS, shareInfo *model.Share) http.Handl "baseURL": str.SanitizeText(strings.TrimSuffix(conf.Server.BasePath, "/")), "loginBackgroundURL": str.SanitizeText(conf.Server.UILoginBackgroundURL), "welcomeMessage": str.SanitizeHTML(conf.Server.UIWelcomeMessage), + "instanceName": conf.Server.InstanceName, "maxSidebarPlaylists": conf.Server.MaxSidebarPlaylists, "enableTranscodingConfig": conf.Server.EnableTranscodingConfig, "enableDownloads": conf.Server.EnableDownloads, @@ -101,8 +102,9 @@ func serveIndex(ds model.DataStore, fs fs.FS, shareInfo *model.Share) http.Handl version = "v" + version } data := map[string]any{ - "AppConfig": string(appConfigJson), - "Version": version, + "InstanceName": conf.Server.InstanceName, + "AppConfig": string(appConfigJson), + "Version": version, } addShareData(r, data, shareInfo) diff --git a/server/serve_index_test.go b/server/serve_index_test.go index 78f3873b8..7181b0391 100644 --- a/server/serve_index_test.go +++ b/server/serve_index_test.go @@ -3,6 +3,7 @@ package server import ( "encoding/json" "fmt" + "html" "net/http" "net/http/httptest" "os" @@ -42,6 +43,24 @@ var _ = Describe("serveIndex", func() { Expect(config).To(BeAssignableToTypeOf(map[string]any{})) }) + DescribeTable("renders the instance name as text in the actual UI template", func(name string) { + conf.Server.InstanceName = name + r := httptest.NewRequest("GET", "/app/", nil) + w := httptest.NewRecorder() + serveIndex(ds, os.DirFS("ui"), nil)(w, r) + Expect(w.Code).To(Equal(http.StatusOK)) + Expect(w.Body.String()).To(ContainSubstring("" + html.EscapeString(name) + "")) + // The JSON configuration also preserves the name as plain text. + w = httptest.NewRecorder() + serveIndex(ds, fs, nil)(w, r) + Expect(extractAppConfig(w.Body.String())).To(HaveKeyWithValue("instanceName", name)) + }, + Entry("default name", "Navidrome"), + Entry("custom name", "mp3-player"), + Entry("Unicode and punctuation", `家の音楽 & "Friends"`), + Entry("HTML-like name", ``), + ) + It("sets firstTime = true when User table is empty", func() { mockUser.empty = true r := httptest.NewRequest("GET", "/index.html", nil) @@ -76,6 +95,7 @@ var _ = Describe("serveIndex", func() { Expect(config).To(HaveKeyWithValue(configKey, expectedValue)) }, Entry("baseURL", func() { conf.Server.BasePath = "base_url_test" }, "baseURL", "base_url_test"), + Entry("instanceName", func() { conf.Server.InstanceName = "mp3-player" }, "instanceName", "mp3-player"), Entry("welcomeMessage", func() { conf.Server.UIWelcomeMessage = "Hello" }, "welcomeMessage", "Hello"), Entry("maxSidebarPlaylists", func() { conf.Server.MaxSidebarPlaylists = 42 }, "maxSidebarPlaylists", float64(42)), Entry("enableTranscodingConfig", func() { conf.Server.EnableTranscodingConfig = true }, "enableTranscodingConfig", true), diff --git a/ui/index.html b/ui/index.html index 827751856..a59d1c597 100644 --- a/ui/index.html +++ b/ui/index.html @@ -26,7 +26,7 @@ - Navidrome + {{ .InstanceName }}