From 2f4f31df94d1432dc5076c232c4b40f5180182d2 Mon Sep 17 00:00:00 2001 From: Mark Ferry Date: Thu, 11 Sep 2025 16:18:05 +0100 Subject: [PATCH 1/4] fix #201: add --config flag --- README.md | 6 ++++-- cmd/daemon/main.go | 8 ++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 76becaa4..d233fa4e 100644 --- a/README.md +++ b/README.md @@ -62,12 +62,14 @@ Details about cross-compiling go-librespot are described [here](/CROSS_COMPILE.m The default directory for configuration files is `~/.config/go-librespot`. On macOS devices, this is `~/Library/Application Support/go-librespot`. You can change this directory with the -`-config_dir` flag. The configuration directory contains: +`--config_dir` flag. The configuration directory contains: - `config.yml`: The main configuration (does not exist by default) - `state.json`: The player state and credentials - `lockfile`: A lockfile to prevent running multiple instances on the same configuration +You may also specify a custom config file with the `--config` flag. + The full configuration schema is available [here](/config_schema.json), only the main options are detailed below. ### Zeroconf Mode and mDNS Backend Selection @@ -232,4 +234,4 @@ or using Go: ```shell go generate ./... -``` \ No newline at end of file +``` diff --git a/cmd/daemon/main.go b/cmd/daemon/main.go index 048cbccb..6c9155a3 100644 --- a/cmd/daemon/main.go +++ b/cmd/daemon/main.go @@ -381,6 +381,7 @@ func (app *App) withAppPlayer(ctx context.Context, appPlayerFunc func(context.Co type Config struct { ConfigDir string `koanf:"config_dir"` + ConfigPath string `koanf:"config"` // We need to keep this object around, otherwise it gets GC'd and the // finalizer will run, probably closing the lock. @@ -453,6 +454,9 @@ func loadConfig(cfg *Config) error { defaultConfigDir := filepath.Join(userConfigDir, "go-librespot") f.StringVar(&cfg.ConfigDir, "config_dir", defaultConfigDir, "the configuration directory") + defaultConfigPath := filepath.Join(defaultConfigDir, "config.yaml") + f.StringVar(&cfg.ConfigPath, "config", defaultConfigPath, "the configuration file") + var configOverrides []string f.StringArrayVarP(&configOverrides, "conf", "c", nil, "override config values (format: field=value, use field1.field2=value for nested fields)") @@ -505,10 +509,10 @@ func loadConfig(cfg *Config) error { // load file configuration (if available) var configPath string - if _, err := os.Stat(filepath.Join(cfg.ConfigDir, "config.yaml")); os.IsNotExist(err) { + if _, err := os.Stat(cfg.ConfigPath); os.IsNotExist(err) { configPath = filepath.Join(cfg.ConfigDir, "config.yml") } else { - configPath = filepath.Join(cfg.ConfigDir, "config.yaml") + configPath = cfg.ConfigPath } if err := k.Load(file.Provider(configPath), yaml.Parser()); err != nil { From 31e3e62ab18a62eb51e9628532a6b42f71aec830 Mon Sep 17 00:00:00 2001 From: Mark Ferry Date: Thu, 11 Sep 2025 17:14:27 +0100 Subject: [PATCH 2/4] add --state flag --- cmd/daemon/main.go | 48 ++++++++++++++++++++++++++++++---------------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/cmd/daemon/main.go b/cmd/daemon/main.go index 6c9155a3..a3edcf18 100644 --- a/cmd/daemon/main.go +++ b/cmd/daemon/main.go @@ -80,7 +80,7 @@ func NewApp(cfg *Config) (app *App, err error) { } app.state.SetLogger(app.log) - if err := app.state.Read(cfg.ConfigDir); err != nil { + if err := app.state.Read(cfg.StateDir); err != nil { return nil, err } @@ -380,12 +380,12 @@ func (app *App) withAppPlayer(ctx context.Context, appPlayerFunc func(context.Co } type Config struct { - ConfigDir string `koanf:"config_dir"` + StateDir string `koanf:"state"` ConfigPath string `koanf:"config"` // We need to keep this object around, otherwise it gets GC'd and the // finalizer will run, probably closing the lock. - configLock *flock.Flock + stateLock *flock.Flock LogLevel log.Level `koanf:"log_level"` LogDisableTimestamp bool `koanf:"log_disable_timestamp"` @@ -441,8 +441,19 @@ type Config struct { } `koanf:"credentials"` } +// backwards compatibility for config_dir flag +func aliasNormalizeFunc(f *flag.FlagSet, name string) flag.NormalizedName { + switch name { + case "config_dir": + name = "state" + break + } + return flag.NormalizedName(name) +} + func loadConfig(cfg *Config) error { f := flag.NewFlagSet("config", flag.ContinueOnError) + f.SetNormalizeFunc(aliasNormalizeFunc) f.Usage = func() { fmt.Println(f.FlagUsages()) os.Exit(0) @@ -451,12 +462,16 @@ func loadConfig(cfg *Config) error { if err != nil { return err } - defaultConfigDir := filepath.Join(userConfigDir, "go-librespot") - f.StringVar(&cfg.ConfigDir, "config_dir", defaultConfigDir, "the configuration directory") - - defaultConfigPath := filepath.Join(defaultConfigDir, "config.yaml") + defaultConfigPath := filepath.Join(userConfigDir, "go-librespot", "config.yaml") f.StringVar(&cfg.ConfigPath, "config", defaultConfigPath, "the configuration file") + userStateDir, err := os.UserConfigDir() + if err != nil { + return err + } + defaultStatePath := filepath.Join(userStateDir, "go-librespot") + f.StringVar(&cfg.StateDir, "state", defaultStatePath, "the state directory") + var configOverrides []string f.StringArrayVarP(&configOverrides, "conf", "c", nil, "override config values (format: field=value, use field1.field2=value for nested fields)") @@ -465,18 +480,18 @@ func loadConfig(cfg *Config) error { return err } - // Make config directory if needed. - err = os.MkdirAll(cfg.ConfigDir, 0o700) + // Make state directory if needed. + err = os.MkdirAll(cfg.StateDir, 0o700) if err != nil { - return fmt.Errorf("failed creating config directory: %w", err) + return fmt.Errorf("failed creating state directory: %w", err) } - // Lock the config directory (to ensure multiple instances won't clobber + // Lock the state directory (to ensure multiple instances won't clobber // each others state). - lockFilePath := filepath.Join(cfg.ConfigDir, "lockfile") - cfg.configLock = flock.New(lockFilePath) - if locked, err := cfg.configLock.TryLock(); err != nil { - return fmt.Errorf("could not lock config directory: %w", err) + lockFilePath := filepath.Join(cfg.StateDir, "lockfile") + cfg.stateLock = flock.New(lockFilePath) + if locked, err := cfg.stateLock.TryLock(); err != nil { + return fmt.Errorf("could not lock state directory: %w", err) } else if !locked { // Lock already taken! Looks like go-librespot is already running. return fmt.Errorf("%w (lockfile: %s)", errAlreadyRunning, lockFilePath) @@ -510,7 +525,8 @@ func loadConfig(cfg *Config) error { // load file configuration (if available) var configPath string if _, err := os.Stat(cfg.ConfigPath); os.IsNotExist(err) { - configPath = filepath.Join(cfg.ConfigDir, "config.yml") + // postel: allow .yml in place of .yaml + configPath = strings.TrimSuffix(cfg.ConfigPath, filepath.Ext(cfg.ConfigPath)) + ".yml" } else { configPath = cfg.ConfigPath } From ca428a31d1701512d6eafe626b94a2ca64474669 Mon Sep 17 00:00:00 2001 From: Mark Ferry Date: Thu, 19 Feb 2026 12:50:33 +0200 Subject: [PATCH 3/4] add UserStateDir implementation Default state dir moves from os.UserConfigDir to (local) UserStateDir --- README.md | 17 ++++++++++------- cmd/daemon/main.go | 2 +- cmd/daemon/os.go | 39 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 8 deletions(-) create mode 100644 cmd/daemon/os.go diff --git a/README.md b/README.md index d233fa4e..f6c8207c 100644 --- a/README.md +++ b/README.md @@ -60,15 +60,18 @@ Details about cross-compiling go-librespot are described [here](/CROSS_COMPILE.m ## Configuration -The default directory for configuration files is `~/.config/go-librespot`. On macOS devices, this is -`~/Library/Application Support/go-librespot`. You can change this directory with the -`--config_dir` flag. The configuration directory contains: +The main configuration files is `~/.config/go-librespot/config.yaml`. On macOS devices, this is +`~/Library/Application Support/go-librespot/config.yaml`. It does not exist by default. -- `config.yml`: The main configuration (does not exist by default) -- `state.json`: The player state and credentials -- `lockfile`: A lockfile to prevent running multiple instances on the same configuration +You can change this path with the `--config` flag. + +State files are stored in `~/.local/state/go-librespot` (`~/Library/Application Support/go-librespot` on macOS). -You may also specify a custom config file with the `--config` flag. +You can change this directory with the `--state` flag. + +The state directory contains: +- `state.json`: The player state and credentials +- `lockfile`: A lockfile to prevent multiple instances using the same state The full configuration schema is available [here](/config_schema.json), only the main options are detailed below. diff --git a/cmd/daemon/main.go b/cmd/daemon/main.go index a3edcf18..92298364 100644 --- a/cmd/daemon/main.go +++ b/cmd/daemon/main.go @@ -465,7 +465,7 @@ func loadConfig(cfg *Config) error { defaultConfigPath := filepath.Join(userConfigDir, "go-librespot", "config.yaml") f.StringVar(&cfg.ConfigPath, "config", defaultConfigPath, "the configuration file") - userStateDir, err := os.UserConfigDir() + userStateDir, err := UserStateDir() // local implementation if err != nil { return err } diff --git a/cmd/daemon/os.go b/cmd/daemon/os.go new file mode 100644 index 00000000..83963deb --- /dev/null +++ b/cmd/daemon/os.go @@ -0,0 +1,39 @@ +package main + +import ( + "errors" + "os" + "runtime" +) + +// UserStateDir is not included in the `os` package. +// It returns the default root directory to use for user-specific +// state data. Users should create their own application-specific subdirectory +// within this one and use that. +// +// On Unix systems, it returns $XDG_STATE_HOME as specified by +// https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html if +// non-empty, else $HOME/.local/state. +// On other systems it returns os.UserConfigDir +// +// If the location cannot be determined (for example, $HOME is not defined) +// then it will return an error. +func UserStateDir() (string, error) { + var dir string + + switch runtime.GOOS { + case "windows", "darwin", "ios", "plan9": + return os.UserConfigDir() + default: // Unix, as UserConfigDir + dir = os.Getenv("XDG_STATE_HOME") + if dir == "" { + dir = os.Getenv("HOME") + if dir == "" { + return "", errors.New("neither $XDG_STATE_HOME nor $HOME are defined") + } + dir += "/.local/state" + } + } + + return dir, nil +} From 2b54b746476b296b4f52dddbbd23dabb88e56527 Mon Sep 17 00:00:00 2001 From: Mark Ferry Date: Thu, 11 Sep 2025 17:51:08 +0100 Subject: [PATCH 4/4] update Docker files --- Dockerfile | 2 +- docker-compose.pulse.yml | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9dd379cd..3538dff5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,4 +16,4 @@ RUN apk update && apk -U --no-cache add libpulse avahi libgcc gcompat alsa-lib COPY --from=build /src/daemon /usr/bin/go-librespot -CMD ["/usr/bin/go-librespot", "--config_dir", "/config"] \ No newline at end of file +CMD ["/usr/bin/go-librespot", "--state", "/state", "--config", "/config/config.yaml"] diff --git a/docker-compose.pulse.yml b/docker-compose.pulse.yml index 24ad4bee..5ee1a898 100644 --- a/docker-compose.pulse.yml +++ b/docker-compose.pulse.yml @@ -5,9 +5,10 @@ services: network_mode: host userns_mode: keep-id volumes: + - ~/.local/state/go-librespot:/state - ~/.config/go-librespot:/config - ~/.config/pulse/cookie:/pulse_cookie:ro - /run/user/1000/pulse/native:/pulse_native # Replace 1000 with your UID environment: PULSE_SERVER: "unix:/pulse_native" - PULSE_COOKIE: "/pulse_cookie" \ No newline at end of file + PULSE_COOKIE: "/pulse_cookie"