diff --git a/Dockerfile b/Dockerfile index 9dd379c..3538dff 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/README.md b/README.md index 76becaa..f6c8207 100644 --- a/README.md +++ b/README.md @@ -60,13 +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) +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 can change this directory with the `--state` flag. + +The state directory contains: - `state.json`: The player state and credentials -- `lockfile`: A lockfile to prevent running multiple instances on the same configuration +- `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. @@ -232,4 +237,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 048cbcc..9229836 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,11 +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"` @@ -440,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) @@ -450,8 +462,15 @@ 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(userConfigDir, "go-librespot", "config.yaml") + f.StringVar(&cfg.ConfigPath, "config", defaultConfigPath, "the configuration file") + + userStateDir, err := UserStateDir() // local implementation + 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)") @@ -461,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) @@ -505,10 +524,11 @@ 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) { - configPath = filepath.Join(cfg.ConfigDir, "config.yml") + if _, err := os.Stat(cfg.ConfigPath); os.IsNotExist(err) { + // postel: allow .yml in place of .yaml + configPath = strings.TrimSuffix(cfg.ConfigPath, filepath.Ext(cfg.ConfigPath)) + ".yml" } else { - configPath = filepath.Join(cfg.ConfigDir, "config.yaml") + configPath = cfg.ConfigPath } if err := k.Load(file.Provider(configPath), yaml.Parser()); err != nil { diff --git a/cmd/daemon/os.go b/cmd/daemon/os.go new file mode 100644 index 0000000..83963de --- /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 +} diff --git a/docker-compose.pulse.yml b/docker-compose.pulse.yml index 24ad4be..5ee1a89 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"