// Package config loads VibeSonic's process configuration from // VIBESONIC_* environment variables. There is no config file: everything // needed to run the server is available via the environment so the same // binary works under systemd or Docker without a wrapper script. package config import ( "fmt" "net/netip" "os" "path/filepath" "strconv" "strings" "time" ) type Config struct { ListenAddr string DataDir string DBPath string MusicDirs []string FFmpegPath string FFprobePath string ScanInterval time.Duration SessionSecret string CookieSecure bool ExtAuth ExtAuthConfig } // ExtAuthConfig configures optional Navidrome-style externalized // authentication: a trusted reverse proxy authenticates the browser user // and passes their identity in a header. VibeSonic never talks to an // identity provider itself. type ExtAuthConfig struct { Enabled bool TrustedSources []netip.Prefix UserHeader string LogoutURL string AutoCreateUsers bool } // Load reads configuration from the environment, applying defaults for // anything unset. It returns an error only for values that are set but // invalid (e.g. an unparsable duration or CIDR). func Load() (Config, error) { cfg := Config{ ListenAddr: envOr("VIBESONIC_LISTEN_ADDR", ":8080"), DataDir: envOr("VIBESONIC_DATA_DIR", "/var/lib/vibesonic"), FFmpegPath: envOr("VIBESONIC_FFMPEG_PATH", "ffmpeg"), FFprobePath: envOr("VIBESONIC_FFPROBE_PATH", "ffprobe"), SessionSecret: os.Getenv("VIBESONIC_SESSION_SECRET"), CookieSecure: envBool("VIBESONIC_COOKIE_SECURE", true), } cfg.DBPath = envOr("VIBESONIC_DB", filepath.Join(cfg.DataDir, "vibesonic.db")) if dirs := os.Getenv("VIBESONIC_MUSIC_DIRS"); dirs != "" { for _, d := range strings.Split(dirs, ":") { d = strings.TrimSpace(d) if d != "" { cfg.MusicDirs = append(cfg.MusicDirs, d) } } } interval := envOr("VIBESONIC_SCAN_INTERVAL", "1h") d, err := time.ParseDuration(interval) if err != nil { return cfg, fmt.Errorf("VIBESONIC_SCAN_INTERVAL: %w", err) } cfg.ScanInterval = d extauth, err := loadExtAuth() if err != nil { return cfg, err } cfg.ExtAuth = extauth return cfg, nil } func loadExtAuth() (ExtAuthConfig, error) { e := ExtAuthConfig{ Enabled: envBool("VIBESONIC_EXTAUTH_ENABLED", false), UserHeader: envOr("VIBESONIC_EXTAUTH_USER_HEADER", "Remote-User"), LogoutURL: os.Getenv("VIBESONIC_EXTAUTH_LOGOUT_URL"), AutoCreateUsers: envBool("VIBESONIC_EXTAUTH_AUTO_CREATE_USERS", true), } raw := os.Getenv("VIBESONIC_EXTAUTH_TRUSTED_SOURCES") if raw == "" { // No default trusted network: external auth must be explicitly // scoped to a real network before it can trust anything. return e, nil } for _, part := range strings.Split(raw, ",") { part = strings.TrimSpace(part) if part == "" { continue } prefix, err := parseCIDROrIP(part) if err != nil { return e, fmt.Errorf("VIBESONIC_EXTAUTH_TRUSTED_SOURCES: %w", err) } e.TrustedSources = append(e.TrustedSources, prefix) } return e, nil } func parseCIDROrIP(s string) (netip.Prefix, error) { if p, err := netip.ParsePrefix(s); err == nil { return p, nil } addr, err := netip.ParseAddr(s) if err != nil { return netip.Prefix{}, fmt.Errorf("invalid CIDR or IP %q: %w", s, err) } return netip.PrefixFrom(addr, addr.BitLen()), nil } func envOr(key, def string) string { if v := os.Getenv(key); v != "" { return v } return def } func envBool(key string, def bool) bool { v := os.Getenv(key) if v == "" { return def } b, err := strconv.ParseBool(v) if err != nil { return def } return b }