Terminal Jellyfin client, watch-side companion to TUISTREAM. - LAN auto-discovery (broadcast + unicast subnet sweep) - mpv-window video playback over JSON IPC (pause/seek/progress/resume) - headless audio playback with in-terminal now-playing screen - volume control with remembered level, mute - audio auto-advance + n/p track skip through the album queue - Omarchy-themed UI with Nerd Font (nf-md) icons - cliamp Jellyfin provider auto-config on login - pacman dependency-installing installer Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
103 lines
2.6 KiB
Go
103 lines
2.6 KiB
Go
// 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 != ""
|
|
}
|