// Package config persists the small amount of state a Jellyfin client needs // between runs: the server URL, the access token and user id obtained at login, // a stable per-install device id, and the last-used playback volume. It lives at // ~/.config/tsplay/config.json (XDG_CONFIG_HOME honoured). package config import ( "encoding/json" "os" "path/filepath" "github.com/google/uuid" ) // Config is the on-disk state. Token/UserID are empty until a successful login; // the TUI shows the login screen whenever Token is blank. type Config struct { ServerURL string `json:"server_url"` Username string `json:"username"` Token string `json:"token"` UserID string `json:"user_id"` DeviceID string `json:"device_id"` // Volume is the last playback volume (mpv's 0-130 scale), remembered across // tracks and runs. A nil pointer means "never set" — distinct from 0, which // is a deliberate silent level. Volume *float64 `json:"volume,omitempty"` } // VolumeOr returns the saved volume, or def if none has been stored yet. func (c *Config) VolumeOr(def float64) float64 { if c.Volume == nil { return def } return *c.Volume } // SetVolume records v as the remembered volume. func (c *Config) SetVolume(v float64) { c.Volume = &v } // dir returns ~/.config/tsplay, creating it on demand. func dir() (string, error) { base, err := os.UserConfigDir() if err != nil { return "", err } d := filepath.Join(base, "tsplay") if err := os.MkdirAll(d, 0o700); err != nil { return "", err } return d, nil } func path() (string, error) { d, err := dir() if err != nil { return "", err } return filepath.Join(d, "config.json"), nil } // Load reads the saved config. A missing file is not an error: it returns a // fresh Config with a newly minted DeviceID so first-run login can proceed. func Load() (*Config, error) { p, err := path() if err != nil { return nil, err } data, err := os.ReadFile(p) if os.IsNotExist(err) { return &Config{DeviceID: uuid.NewString()}, nil } if err != nil { return nil, err } var c Config if err := json.Unmarshal(data, &c); err != nil { return nil, err } if c.DeviceID == "" { c.DeviceID = uuid.NewString() } return &c, nil } // Save writes the config back to disk with 0600 perms (it holds a token). func (c *Config) Save() error { p, err := path() if err != nil { return err } data, err := json.MarshalIndent(c, "", " ") if err != nil { return err } return os.WriteFile(p, data, 0o600) } // LoggedIn reports whether we have credentials to talk to the server. func (c *Config) LoggedIn() bool { return c.ServerURL != "" && c.Token != "" && c.UserID != "" }