diff --git a/config.example.yaml b/config.example.yaml index 8e1051d6..509945fc 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -38,6 +38,15 @@ library: # Kick off a scan on server startup. Env: SMARTMUSIC_LIBRARY_SCAN_ON_STARTUP scan_on_startup: false +# Per-instance branding. Operators can rename their instance ("Family +# Jukebox", "Office Music", etc.) and override the OG share-preview +# description. Both fields default to sensible Minstrel values when +# unset; you only need this section to override. +# Env: SMARTMUSIC_BRANDING_APP_NAME, SMARTMUSIC_BRANDING_DESCRIPTION +branding: + app_name: "Minstrel" + description: "Self-hosted music server with Subsonic compatibility, smart shuffle, and Lidarr integration." + subsonic: # Gate for the legacy `p=` plaintext password parameter on /rest/*. # Leave disabled unless a specific client requires it — Minstrel prefers diff --git a/internal/config/config.go b/internal/config/config.go index f9ddb85f..01f80f10 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -19,6 +19,7 @@ type Config struct { Subsonic SubsonicConfig `yaml:"subsonic"` Events EventsConfig `yaml:"events"` Recommendation RecommendationConfig `yaml:"recommendation"` + Branding BrandingConfig `yaml:"branding"` } type ServerConfig struct { @@ -37,6 +38,14 @@ type StorageConfig struct { DataDir string `yaml:"data_dir"` } +// BrandingConfig drives per-instance UI labelling: the app name shown +// in the header / tab title and the description used in OG share-preview +// metadata. Both default to sensible values when unset. +type BrandingConfig struct { + AppName string `yaml:"app_name"` + Description string `yaml:"description"` +} + type DatabaseConfig struct { URL string `yaml:"url"` } @@ -98,6 +107,10 @@ func Default() Config { Storage: StorageConfig{DataDir: "./data"}, Database: DatabaseConfig{URL: ""}, Log: LogConfig{Level: "info", Format: "text"}, + Branding: BrandingConfig{ + AppName: "Minstrel", + Description: "Self-hosted music server with Subsonic compatibility, smart shuffle, and Lidarr integration.", + }, Events: EventsConfig{ SessionTimeoutMinutes: 30, SkipMaxCompletionRatio: 0.5, @@ -150,6 +163,12 @@ func applyEnv(cfg *Config) { if v, ok := os.LookupEnv("SMARTMUSIC_LOG_FORMAT"); ok { cfg.Log.Format = strings.ToLower(v) } + if v, ok := os.LookupEnv("SMARTMUSIC_BRANDING_APP_NAME"); ok { + cfg.Branding.AppName = v + } + if v, ok := os.LookupEnv("SMARTMUSIC_BRANDING_DESCRIPTION"); ok { + cfg.Branding.Description = v + } if v, ok := os.LookupEnv("SMARTMUSIC_AUTH_ADMIN_USERNAME"); ok { cfg.Auth.AdminBootstrap.Username = v } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 511a5bb0..6acaedd3 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -151,3 +151,50 @@ func TestLibraryYAMLLoads(t *testing.T) { t.Error("scan_on_startup = false") } } + +func TestBrandingDefaults(t *testing.T) { + cfg := Default() + if cfg.Branding.AppName != "Minstrel" { + t.Errorf("AppName: got %q, want %q", cfg.Branding.AppName, "Minstrel") + } + if cfg.Branding.Description == "" { + t.Errorf("Description: empty after Default(); want default tagline") + } +} + +func TestBrandingYAMLOverride(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "config.yaml") + content := []byte(`branding: + app_name: "Family Jukebox" + description: "Our home library." +`) + if err := os.WriteFile(path, content, 0o644); err != nil { + t.Fatal(err) + } + cfg, err := Load(path) + if err != nil { + t.Fatalf("Load: %v", err) + } + if cfg.Branding.AppName != "Family Jukebox" { + t.Errorf("AppName: got %q", cfg.Branding.AppName) + } + if cfg.Branding.Description != "Our home library." { + t.Errorf("Description: got %q", cfg.Branding.Description) + } +} + +func TestBrandingEnvOverride(t *testing.T) { + t.Setenv("SMARTMUSIC_BRANDING_APP_NAME", "Office Music") + t.Setenv("SMARTMUSIC_BRANDING_DESCRIPTION", "Office tunes.") + cfg, err := Load("") + if err != nil { + t.Fatalf("Load: %v", err) + } + if cfg.Branding.AppName != "Office Music" { + t.Errorf("AppName: got %q", cfg.Branding.AppName) + } + if cfg.Branding.Description != "Office tunes." { + t.Errorf("Description: got %q", cfg.Branding.Description) + } +}