- 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>
73 lines
2.4 KiB
Python
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)
|