package config import ( "errors" "fmt" "log" "os" "path/filepath" "runtime" "strconv" "github.com/alyu/configparser" ) // Config holds cached configuration values loaded once at startup. type Config struct { path string section *configparser.Section loaded bool } var ( defaultConfig *Config loadErr error ) // Init loads (or creates) the configuration once. Call early from main. // It is safe to call multiple times (subsequent calls are no-ops if successful). func Init() error { if defaultConfig != nil && defaultConfig.loaded { return loadErr } c := &Config{} c.path = configStat("radiostations.ini") if err := c.load(); err != nil { loadErr = err return err } defaultConfig = c defaultConfig.loaded = true return nil } func configStat(configFile string) string { xdgConfigPath := os.Getenv("XDG_CONFIG_HOME") if xdgConfigPath == "" { if runtime.GOOS == "windows" { xdgConfigPath = os.Getenv("USERPROFILE") if xdgConfigPath == "" { xdgConfigPath = "." } xdgConfigPath = filepath.Join(xdgConfigPath, ".config") } else { xdgConfigPath = os.Getenv("HOME") if xdgConfigPath == "" { xdgConfigPath = "." } xdgConfigPath = filepath.Join(xdgConfigPath, ".config") } } configFile = filepath.Join(xdgConfigPath, "gostations", configFile) if _, err := os.Stat(configFile); errors.Is(err, os.ErrNotExist) { log.Printf("Your stations config file seems to be missing. A default will be generated at %s.", configFile) if errs := createIniFile(configFile); len(errs) > 0 { for _, e := range errs { if e != nil { log.Printf("Error creating default config: %s", e.Error()) } } log.Fatal("Cannot continue without a config file.") } } return configFile } func createIniFile(fpath string) []error { log.Printf("Creating config file: %s\n", fpath) var errorlist []error if err := os.MkdirAll(filepath.Dir(fpath), 0770); err != nil { errorlist = append(errorlist, err) } file, err := os.Create(fpath) if err != nil { errorlist = append(errorlist, err) } else { // only write if file was created writes := []string{ "[DEFAULT]\n", "radio_browser.api=all.api.radio-browser.info\n", "player.command=mpv\n", "player.options=--no-video\n", "menu_items.max=9999\n", } for _, w := range writes { if _, err := file.Write([]byte(w)); err != nil { errorlist = append(errorlist, err) } } if cerr := file.Close(); cerr != nil { errorlist = append(errorlist, cerr) } } return errorlist } func (c *Config) load() error { configparser.Delimiter = "=" // set once cfg, err := configparser.Read(c.path) if err != nil { return fmt.Errorf("read config %s: %w", c.path, err) } sec, err := cfg.Section("DEFAULT") if err != nil { return fmt.Errorf("find DEFAULT section in %s: %w", c.path, err) } c.section = sec return nil } // Get returns the raw string value for key, or error if missing/empty. func Get(key string) (string, error) { if err := Init(); err != nil { return "", err } if defaultConfig.section == nil { return "", errors.New("config not loaded") } val := defaultConfig.section.Options()[key] if val == "" { return "", fmt.Errorf("no value for option %q in config", key) } return val, nil } // MustGet is like Get but fatals on error (for precheck paths only). func MustGet(key string) string { v, err := Get(key) if err != nil { log.Fatalf("config error for %s: %v", key, err) } return v } func str2int(s string) int { i, err := strconv.Atoi(s) if err != nil { return 9999 } return i } // API returns the radio browser API host (or default on error for compat). func API() string { if v, err := Get("radio_browser.api"); err == nil && v != "" { return v } return "all.api.radio-browser.info" } // Player returns the player command. func Player() string { if v, err := Get("player.command"); err == nil && v != "" { return v } return "mpv" } // Options returns extra player options string. func Options() string { v, _ := Get("player.options") return v } // MaxItems returns the menu limit. func MaxItems() int { if v, err := Get("menu_items.max"); err == nil && v != "" { return str2int(v) } return 9999 } // Path returns the resolved config file path (for diagnostics). func Path() string { if defaultConfig != nil { return defaultConfig.path } return configStat("radiostations.ini") }