- Replace the Python implementation with a Go module compiling to a single static binary (no Python/pip runtime dependency) - internal/tonieapi: minimal, dependency-light HTTP client for the TonieCloud REST API (login, households, creative tonies, file upload via presigned S3 request, chapter add/sort/clear) - internal/config: YAML-based mapping of local playlist folders to Kreativ-Tonies, credentials never stored - internal/syncer: diff/apply logic (upload new tracks, prune removed chapters, reorder to match local file order), built against a TonieClient interface for testability - cmd/toni-sync: Cobra CLI with `tonies list`, `config add/list/remove`, `sync [NAME|--all] [--dry-run]` - Unit tests for config persistence and syncer plan/apply logic Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
103 lines
2.6 KiB
Go
103 lines
2.6 KiB
Go
// Package config manages the local mapping between playlist folders and
|
|
// Kreativ-Tonies. Credentials are never stored here.
|
|
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// Mapping maps a local folder (e.g. an exported Deezer playlist) to a Kreativ-Tonie.
|
|
type Mapping struct {
|
|
Name string `yaml:"name"`
|
|
HouseholdID string `yaml:"household_id"`
|
|
TonieID string `yaml:"tonie_id"`
|
|
TonieName string `yaml:"tonie_name,omitempty"`
|
|
Folder string `yaml:"folder"`
|
|
PlaylistRef string `yaml:"playlist_ref,omitempty"`
|
|
Prune bool `yaml:"prune"`
|
|
}
|
|
|
|
// Config is the full toni-sync configuration.
|
|
type Config struct {
|
|
Mappings []Mapping `yaml:"mappings"`
|
|
}
|
|
|
|
// Get returns the mapping with the given name, if present.
|
|
func (c *Config) Get(name string) *Mapping {
|
|
for i := range c.Mappings {
|
|
if c.Mappings[i].Name == name {
|
|
return &c.Mappings[i]
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Upsert adds a new mapping or replaces an existing one with the same name.
|
|
func (c *Config) Upsert(m Mapping) {
|
|
for i := range c.Mappings {
|
|
if c.Mappings[i].Name == m.Name {
|
|
c.Mappings[i] = m
|
|
return
|
|
}
|
|
}
|
|
c.Mappings = append(c.Mappings, m)
|
|
}
|
|
|
|
// Remove deletes the mapping with the given name. Returns false if it was not found.
|
|
func (c *Config) Remove(name string) bool {
|
|
for i := range c.Mappings {
|
|
if c.Mappings[i].Name == name {
|
|
c.Mappings = append(c.Mappings[:i], c.Mappings[i+1:]...)
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// DefaultPath returns the default config file location, honoring TONI_SYNC_CONFIG.
|
|
func DefaultPath() string {
|
|
if p := os.Getenv("TONI_SYNC_CONFIG"); p != "" {
|
|
return p
|
|
}
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
home = "."
|
|
}
|
|
return filepath.Join(home, ".config", "toni-sync", "config.yaml")
|
|
}
|
|
|
|
// Load reads the config from path, or returns an empty Config if it doesn't exist yet.
|
|
func Load(path string) (*Config, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return &Config{}, nil
|
|
}
|
|
return nil, fmt.Errorf("reading config %s: %w", path, err)
|
|
}
|
|
var cfg Config
|
|
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
|
return nil, fmt.Errorf("parsing config %s: %w", path, err)
|
|
}
|
|
return &cfg, nil
|
|
}
|
|
|
|
// Save writes the config to path, creating parent directories as needed.
|
|
func Save(cfg *Config, path string) error {
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
return fmt.Errorf("creating config dir: %w", err)
|
|
}
|
|
data, err := yaml.Marshal(cfg)
|
|
if err != nil {
|
|
return fmt.Errorf("encoding config: %w", err)
|
|
}
|
|
if err := os.WriteFile(path, data, 0o644); err != nil {
|
|
return fmt.Errorf("writing config %s: %w", path, err)
|
|
}
|
|
return nil
|
|
}
|