193 lines
4.3 KiB
Go
193 lines
4.3 KiB
Go
|
|
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")
|
||
|
|
}
|