Files
toni-sync/src/toni_sync/config.py
T
arnefandCopilot 4860edf886 Initial toni-sync CLI: sync local playlist folders to Kreativ-Tonies
- Config commands (config add/list/remove) to map local audio folders
  to Kreativ-Tonies via TonieCloud household/tonie ids
- tonies list command to look up household/tonie ids
- sync command: diff-based upload of new tracks, pruning of removed
  chapters, and reordering to match local file order (via tonie-api)
- Credentials resolved via env vars/CLI flags/prompt, never stored
- Unit tests for config persistence and sync planning/apply logic

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-08-13 22:41:00 +02:00

73 lines
2.4 KiB
Python

"""Configuration handling for toni-sync.
Stores the mapping between local audio folders (e.g. exported Deezer
playlists) and Kreativ-Tonies in a simple YAML file. Credentials for the
TonieCloud account are NEVER stored here - they are provided via
environment variables or interactive prompt (see client.py).
"""
from __future__ import annotations
import os
from pathlib import Path
import yaml
from pydantic import BaseModel, Field
DEFAULT_CONFIG_PATH = Path(
os.environ.get("TONI_SYNC_CONFIG", str(Path.home() / ".config" / "toni-sync" / "config.yaml"))
)
class ToniMapping(BaseModel):
"""Mapping of a local folder to a Kreativ-Tonie."""
name: str # unique friendly identifier used on the CLI, e.g. "peppa-wutz"
household_id: str
tonie_id: str
tonie_name: str | None = None # informational, kept in sync on config add/list
folder: str # local path containing the audio files for this tonie
playlist_ref: str | None = None # informational, e.g. deezer playlist URL/ID
prune: bool = True # remove chapters on the tonie that no longer exist locally
@property
def folder_path(self) -> Path:
return Path(self.folder).expanduser()
class AppConfig(BaseModel):
"""The full toni-sync configuration."""
mappings: list[ToniMapping] = Field(default_factory=list)
def get(self, name: str) -> ToniMapping | None:
return next((m for m in self.mappings if m.name == name), None)
def upsert(self, mapping: ToniMapping) -> None:
for i, existing in enumerate(self.mappings):
if existing.name == mapping.name:
self.mappings[i] = mapping
return
self.mappings.append(mapping)
def remove(self, name: str) -> bool:
before = len(self.mappings)
self.mappings = [m for m in self.mappings if m.name != name]
return len(self.mappings) != before
def load_config(path: Path | None = None) -> AppConfig:
path = path or DEFAULT_CONFIG_PATH
if not path.exists():
return AppConfig()
with path.open("r", encoding="utf-8") as fh:
raw = yaml.safe_load(fh) or {}
return AppConfig(**raw)
def save_config(config: AppConfig, path: Path | None = None) -> None:
path = path or DEFAULT_CONFIG_PATH
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("w", encoding="utf-8") as fh:
yaml.safe_dump(config.model_dump(), fh, allow_unicode=True, sort_keys=False)