From ca828623462400df8e59f16da58edef758a4538c Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Sun, 3 May 2026 18:40:59 -0400 Subject: [PATCH] chore(config): rename SMARTMUSIC_ env prefix to MINSTREL_; default admin user MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The legacy SMARTMUSIC_ prefix dates from when the project was specced as "smart-music" before being renamed to Minstrel. Hard cutover to MINSTREL_* across config.go, tests, config.example.yaml, README, and docker-compose. Also renames SMARTMUSIC_CONFIG in cmd/minstrel/main.go which was missed in the original audit. Also seeds Auth.AdminBootstrap.Username = "admin" in Default() so MINSTREL_DATABASE_URL is the only env var required for a fresh prod deployment — the bootstrap auto-generates a password and prints it to stderr. Drops the now-redundant MINSTREL_AUTH_ADMIN_USERNAME=admin line from the README quickstart and docker-compose. M1 follow-up from the #363 final review: guards MINSTREL_BRANDING_APP_NAME and _DESCRIPTION against empty-string overrides — empty would have shipped to share-preview crawlers. Co-Authored-By: Claude Sonnet 4.6 --- README.md | 15 +++++++-------- cmd/minstrel/main.go | 2 +- config.example.yaml | 21 +++++++++++---------- docker-compose.yml | 11 +++++------ internal/config/config.go | 29 +++++++++++++++++------------ internal/config/config_test.go | 22 +++++++++++----------- 6 files changed, 52 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index b33f5275..a798b81b 100644 --- a/README.md +++ b/README.md @@ -27,9 +27,8 @@ services: - ./music:/music:ro - minstrel-data:/data environment: - SMARTMUSIC_DATABASE_URL: postgres://minstrel:minstrel@db:5432/minstrel?sslmode=disable - SMARTMUSIC_LIBRARY_SCAN_PATHS: /music - SMARTMUSIC_AUTH_ADMIN_USERNAME: admin + MINSTREL_DATABASE_URL: postgres://minstrel:minstrel@db:5432/minstrel?sslmode=disable + MINSTREL_LIBRARY_SCAN_PATHS: /music depends_on: [db] db: @@ -57,14 +56,14 @@ For the full configuration surface, see [`config.example.yaml`](./config.example Most operators only need the env vars in the quickstart above. A few extras worth knowing: -- `SMARTMUSIC_BRANDING_APP_NAME` — rename the instance ("Family Jukebox", "Office Music"). Surfaces in the header, browser tab, and OG share previews. -- `SMARTMUSIC_STORAGE_DATA_DIR` — defaults to `./data`. Holds playlist cover collages and other generated artefacts. -- `SMARTMUSIC_LIBRARY_SCAN_PATHS` — colon-separated list of music library roots to scan. Supports multiple roots (`/music:/podcasts`). -- `SMARTMUSIC_AUTH_ADMIN_USERNAME` / `SMARTMUSIC_AUTH_ADMIN_PASSWORD` — bootstrap admin credentials. Leave the password unset to have the server generate one and print it to stderr on first start. +- `MINSTREL_BRANDING_APP_NAME` — rename the instance ("Family Jukebox", "Office Music"). Surfaces in the header, browser tab, and OG share previews. +- `MINSTREL_STORAGE_DATA_DIR` — defaults to `./data`. Holds playlist cover collages and other generated artefacts. +- `MINSTREL_LIBRARY_SCAN_PATHS` — colon-separated list of music library roots to scan. Supports multiple roots (`/music:/podcasts`). +- `MINSTREL_AUTH_ADMIN_USERNAME` / `MINSTREL_AUTH_ADMIN_PASSWORD` — bootstrap admin credentials. Username defaults to `admin`; leave the password unset to have the server generate one and print it to stderr on first start. ListenBrainz integration (per-user scrobble + similarity tokens) and Lidarr integration (URL + API key) are configured through the admin Settings UI rather than env vars or yaml — per Minstrel's "config in UI" rule, integration settings live where operators can edit them without restarting. -Most operational keys have a `SMARTMUSIC_
_` env override. Recommendation and events tuning are yaml-only. See [`config.example.yaml`](./config.example.yaml) for the authoritative surface. +Most operational keys have a `MINSTREL_
_` env override. Recommendation and events tuning are yaml-only. See [`config.example.yaml`](./config.example.yaml) for the authoritative surface. ## Updating diff --git a/cmd/minstrel/main.go b/cmd/minstrel/main.go index 6b2a4536..48d73218 100644 --- a/cmd/minstrel/main.go +++ b/cmd/minstrel/main.go @@ -33,7 +33,7 @@ func main() { } func run() error { - configPath := flag.String("config", os.Getenv("SMARTMUSIC_CONFIG"), "path to YAML config file") + configPath := flag.String("config", os.Getenv("MINSTREL_CONFIG"), "path to YAML config file") flag.Parse() cfg, err := config.Load(*configPath) diff --git a/config.example.yaml b/config.example.yaml index 509945fc..42568fae 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -1,6 +1,6 @@ # Minstrel example configuration. # Every field is optional; defaults shown below match Default() in internal/config. -# Each setting can be overridden at runtime via SMARTMUSIC_* environment variables +# Each setting can be overridden at runtime via MINSTREL_* environment variables # (see internal/config/config.go for the full list). server: @@ -11,7 +11,7 @@ storage: # collages under /playlist_covers/). Default: "./data" relative # to the working directory. Production deployments should set an absolute # path on persistent storage. - # Env: SMARTMUSIC_STORAGE_DATA_DIR + # Env: MINSTREL_STORAGE_DATA_DIR data_dir: "./data" database: @@ -24,25 +24,26 @@ log: auth: # One-shot admin bootstrap. Applied only when the users table is empty. - # Set username to enable; leave password blank to have the server generate - # one and log it to stderr on first start. - # Env: SMARTMUSIC_AUTH_ADMIN_USERNAME, SMARTMUSIC_AUTH_ADMIN_PASSWORD + # Username defaults to "admin" when unset — you can omit this section + # entirely for a working bootstrap. Leave password blank to have the server + # generate one and log it to stderr on first start. + # Env: MINSTREL_AUTH_ADMIN_USERNAME, MINSTREL_AUTH_ADMIN_PASSWORD admin_bootstrap: - username: "" + username: "admin" password: "" library: # Filesystem roots to scan for music. Each path is walked recursively. - # Env: SMARTMUSIC_LIBRARY_SCAN_PATHS (colon-separated, PATH-style) + # Env: MINSTREL_LIBRARY_SCAN_PATHS (colon-separated, PATH-style) scan_paths: [] - # Kick off a scan on server startup. Env: SMARTMUSIC_LIBRARY_SCAN_ON_STARTUP + # Kick off a scan on server startup. Env: MINSTREL_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 +# Env: MINSTREL_BRANDING_APP_NAME, MINSTREL_BRANDING_DESCRIPTION branding: app_name: "Minstrel" description: "Self-hosted music server with Subsonic compatibility, smart shuffle, and Lidarr integration." @@ -51,7 +52,7 @@ subsonic: # Gate for the legacy `p=` plaintext password parameter on /rest/*. # Leave disabled unless a specific client requires it — Minstrel prefers # apiKey (OpenSubsonic) and falls back to u+t+s token auth. - # Env: SMARTMUSIC_SUBSONIC_ALLOW_PLAINTEXT_PASSWORD + # Env: MINSTREL_SUBSONIC_ALLOW_PLAINTEXT_PASSWORD allow_plaintext_password: false events: diff --git a/docker-compose.yml b/docker-compose.yml index 997de037..2358fada 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -34,12 +34,11 @@ services: postgres: condition: service_healthy environment: - SMARTMUSIC_DATABASE_URL: postgres://minstrel:minstrel@postgres:5432/minstrel?sslmode=disable - SMARTMUSIC_LOG_FORMAT: text - SMARTMUSIC_LIBRARY_SCAN_PATHS: /music - SMARTMUSIC_LIBRARY_SCAN_ON_STARTUP: "true" - SMARTMUSIC_AUTH_ADMIN_USERNAME: admin - # SMARTMUSIC_AUTH_ADMIN_PASSWORD left unset on purpose — bootstrap + MINSTREL_DATABASE_URL: postgres://minstrel:minstrel@postgres:5432/minstrel?sslmode=disable + MINSTREL_LOG_FORMAT: text + MINSTREL_LIBRARY_SCAN_PATHS: /music + MINSTREL_LIBRARY_SCAN_ON_STARTUP: "true" + # MINSTREL_AUTH_ADMIN_PASSWORD left unset on purpose — bootstrap # generates one and prints it to stderr on first boot. Watch the logs # of the minstrel service the first time you bring the stack up. networks: diff --git a/internal/config/config.go b/internal/config/config.go index 01f80f10..5deb6971 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -111,6 +111,11 @@ func Default() Config { AppName: "Minstrel", Description: "Self-hosted music server with Subsonic compatibility, smart shuffle, and Lidarr integration.", }, + Auth: AuthConfig{ + AdminBootstrap: AdminBootstrapConfig{ + Username: "admin", + }, + }, Events: EventsConfig{ SessionTimeoutMinutes: 30, SkipMaxCompletionRatio: 0.5, @@ -148,42 +153,42 @@ func Load(path string) (Config, error) { } func applyEnv(cfg *Config) { - if v, ok := os.LookupEnv("SMARTMUSIC_SERVER_ADDRESS"); ok { + if v, ok := os.LookupEnv("MINSTREL_SERVER_ADDRESS"); ok { cfg.Server.Address = v } - if v, ok := os.LookupEnv("SMARTMUSIC_STORAGE_DATA_DIR"); ok { + if v, ok := os.LookupEnv("MINSTREL_STORAGE_DATA_DIR"); ok { cfg.Storage.DataDir = v } - if v, ok := os.LookupEnv("SMARTMUSIC_DATABASE_URL"); ok { + if v, ok := os.LookupEnv("MINSTREL_DATABASE_URL"); ok { cfg.Database.URL = v } - if v, ok := os.LookupEnv("SMARTMUSIC_LOG_LEVEL"); ok { + if v, ok := os.LookupEnv("MINSTREL_LOG_LEVEL"); ok { cfg.Log.Level = strings.ToLower(v) } - if v, ok := os.LookupEnv("SMARTMUSIC_LOG_FORMAT"); ok { + if v, ok := os.LookupEnv("MINSTREL_LOG_FORMAT"); ok { cfg.Log.Format = strings.ToLower(v) } - if v, ok := os.LookupEnv("SMARTMUSIC_BRANDING_APP_NAME"); ok { + if v, ok := os.LookupEnv("MINSTREL_BRANDING_APP_NAME"); ok && v != "" { cfg.Branding.AppName = v } - if v, ok := os.LookupEnv("SMARTMUSIC_BRANDING_DESCRIPTION"); ok { + if v, ok := os.LookupEnv("MINSTREL_BRANDING_DESCRIPTION"); ok && v != "" { cfg.Branding.Description = v } - if v, ok := os.LookupEnv("SMARTMUSIC_AUTH_ADMIN_USERNAME"); ok { + if v, ok := os.LookupEnv("MINSTREL_AUTH_ADMIN_USERNAME"); ok { cfg.Auth.AdminBootstrap.Username = v } - if v, ok := os.LookupEnv("SMARTMUSIC_AUTH_ADMIN_PASSWORD"); ok { + if v, ok := os.LookupEnv("MINSTREL_AUTH_ADMIN_PASSWORD"); ok { cfg.Auth.AdminBootstrap.Password = v } - if v, ok := os.LookupEnv("SMARTMUSIC_LIBRARY_SCAN_PATHS"); ok { + if v, ok := os.LookupEnv("MINSTREL_LIBRARY_SCAN_PATHS"); ok { cfg.Library.ScanPaths = splitPaths(v) } - if v, ok := os.LookupEnv("SMARTMUSIC_LIBRARY_SCAN_ON_STARTUP"); ok { + if v, ok := os.LookupEnv("MINSTREL_LIBRARY_SCAN_ON_STARTUP"); ok { if b, err := strconv.ParseBool(v); err == nil { cfg.Library.ScanOnStartup = b } } - if v, ok := os.LookupEnv("SMARTMUSIC_SUBSONIC_ALLOW_PLAINTEXT_PASSWORD"); ok { + if v, ok := os.LookupEnv("MINSTREL_SUBSONIC_ALLOW_PLAINTEXT_PASSWORD"); ok { if b, err := strconv.ParseBool(v); err == nil { cfg.Subsonic.AllowPlaintextPassword = b } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 6acaedd3..3bfd8741 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -56,10 +56,10 @@ func TestLoadMissingFileReturnsDefaults(t *testing.T) { } func TestEnvOverrides(t *testing.T) { - t.Setenv("SMARTMUSIC_SERVER_ADDRESS", ":8080") - t.Setenv("SMARTMUSIC_DATABASE_URL", "postgres://env") - t.Setenv("SMARTMUSIC_LOG_LEVEL", "WARN") - t.Setenv("SMARTMUSIC_LOG_FORMAT", "JSON") + t.Setenv("MINSTREL_SERVER_ADDRESS", ":8080") + t.Setenv("MINSTREL_DATABASE_URL", "postgres://env") + t.Setenv("MINSTREL_LOG_LEVEL", "WARN") + t.Setenv("MINSTREL_LOG_FORMAT", "JSON") cfg, err := Load("") if err != nil { @@ -80,8 +80,8 @@ func TestEnvOverrides(t *testing.T) { } func TestAdminBootstrapEnvOverrides(t *testing.T) { - t.Setenv("SMARTMUSIC_AUTH_ADMIN_USERNAME", "root") - t.Setenv("SMARTMUSIC_AUTH_ADMIN_PASSWORD", "sekret") + t.Setenv("MINSTREL_AUTH_ADMIN_USERNAME", "root") + t.Setenv("MINSTREL_AUTH_ADMIN_PASSWORD", "sekret") cfg, err := Load("") if err != nil { @@ -96,8 +96,8 @@ func TestAdminBootstrapEnvOverrides(t *testing.T) { } func TestLibraryEnvOverrides(t *testing.T) { - t.Setenv("SMARTMUSIC_LIBRARY_SCAN_PATHS", "/music:/other::/third") - t.Setenv("SMARTMUSIC_LIBRARY_SCAN_ON_STARTUP", "true") + t.Setenv("MINSTREL_LIBRARY_SCAN_PATHS", "/music:/other::/third") + t.Setenv("MINSTREL_LIBRARY_SCAN_ON_STARTUP", "true") cfg, err := Load("") if err != nil { @@ -118,7 +118,7 @@ func TestLibraryEnvOverrides(t *testing.T) { } func TestSubsonicEnvOverride(t *testing.T) { - t.Setenv("SMARTMUSIC_SUBSONIC_ALLOW_PLAINTEXT_PASSWORD", "true") + t.Setenv("MINSTREL_SUBSONIC_ALLOW_PLAINTEXT_PASSWORD", "true") cfg, err := Load("") if err != nil { t.Fatalf("Load: %v", err) @@ -185,8 +185,8 @@ func TestBrandingYAMLOverride(t *testing.T) { } func TestBrandingEnvOverride(t *testing.T) { - t.Setenv("SMARTMUSIC_BRANDING_APP_NAME", "Office Music") - t.Setenv("SMARTMUSIC_BRANDING_DESCRIPTION", "Office tunes.") + t.Setenv("MINSTREL_BRANDING_APP_NAME", "Office Music") + t.Setenv("MINSTREL_BRANDING_DESCRIPTION", "Office tunes.") cfg, err := Load("") if err != nil { t.Fatalf("Load: %v", err)