Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
CMD ["/usr/bin/go-librespot", "--state", "/state", "--config", "/config/config.yaml"]
17 changes: 11 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -232,4 +237,4 @@ or using Go:

```shell
go generate ./...
```
```
52 changes: 36 additions & 16 deletions cmd/daemon/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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"`
Expand Down Expand Up @@ -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)
Expand All @@ -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)")
Expand All @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down
39 changes: 39 additions & 0 deletions cmd/daemon/os.go
Original file line number Diff line number Diff line change
@@ -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
}
3 changes: 2 additions & 1 deletion docker-compose.pulse.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
PULSE_COOKIE: "/pulse_cookie"