gostations/internal/config/config.go

193 lines
4.3 KiB
Go
Raw Permalink Normal View History

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")
}