Replace config file with folder-based mapping

Mappings between local playlist folders and Kreativ-Tonies are now
expressed purely through the filesystem instead of a YAML config:

  <root>/<household_id>/<freier_name> [id=<tonie_id>]/*.mp3

- internal/library: discovers tonie folders from this structure
  (Discover/Filter), and scaffolds it automatically from the
  TonieCloud account (Scaffold), leaving already-existing folders
  (matched by tonie id) untouched
- internal/syncer: decoupled from the removed config package via a
  plain Target{TonieID, Folder, Prune} struct
- cmd/toni-sync: new `init --root PATH` command to scaffold the
  library; `sync [FILTER] --root PATH` now discovers targets from the
  folder tree instead of a config file; removed the `config` command
  group entirely
- Unit tests for folder discovery, filtering, and scaffolding

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-08-14 12:02:54 +02:00
co-authored by Copilot
parent d003c32433
commit ae46e8f5c0
15 changed files with 580 additions and 395 deletions
+91
View File
@@ -0,0 +1,91 @@
package library
import (
"os"
"path/filepath"
"testing"
)
func mkTonieDir(t *testing.T, householdPath, dirName string) string {
t.Helper()
p := filepath.Join(householdPath, dirName)
if err := os.MkdirAll(p, 0o755); err != nil {
t.Fatal(err)
}
return p
}
func TestParseTonieDir(t *testing.T) {
name, id, ok := ParseTonieDir("Peppa Wutz [id=ef01-5678]")
if !ok || name != "Peppa Wutz" || id != "ef01-5678" {
t.Fatalf("unexpected parse result: name=%q id=%q ok=%v", name, id, ok)
}
}
func TestParseTonieDirNoMatch(t *testing.T) {
if _, _, ok := ParseTonieDir("just a folder"); ok {
t.Fatal("expected no match for folder without [id=...] suffix")
}
}
func TestDirName(t *testing.T) {
if got := DirName("Peppa/Wutz", "abc"); got != "Peppa-Wutz [id=abc]" {
t.Fatalf("unexpected dir name: %q", got)
}
}
func TestDiscover(t *testing.T) {
root := t.TempDir()
household := filepath.Join(root, "household-1")
if err := os.MkdirAll(household, 0o755); err != nil {
t.Fatal(err)
}
mkTonieDir(t, household, "Peppa Wutz [id=tonie-1]")
mkTonieDir(t, household, "not-a-tonie-folder")
// A second household with one tonie.
household2 := filepath.Join(root, "household-2")
mkTonieDir(t, household2, "Gute Nacht [id=tonie-2]")
tonies, err := Discover(root)
if err != nil {
t.Fatal(err)
}
if len(tonies) != 2 {
t.Fatalf("expected 2 tonies, got %d: %+v", len(tonies), tonies)
}
byID := map[string]Tonie{}
for _, tn := range tonies {
byID[tn.TonieID] = tn
}
got, ok := byID["tonie-1"]
if !ok || got.HouseholdID != "household-1" || got.Name != "Peppa Wutz" {
t.Fatalf("unexpected tonie-1 entry: %+v", got)
}
got2, ok := byID["tonie-2"]
if !ok || got2.HouseholdID != "household-2" || got2.Name != "Gute Nacht" {
t.Fatalf("unexpected tonie-2 entry: %+v", got2)
}
}
func TestFilter(t *testing.T) {
tonies := []Tonie{
{HouseholdID: "h1", TonieID: "t1", Name: "Peppa Wutz"},
{HouseholdID: "h2", TonieID: "t2", Name: "Gute Nacht"},
}
if got := Filter(tonies, ""); len(got) != 2 {
t.Fatalf("empty filter should return all, got %d", len(got))
}
if got := Filter(tonies, "peppa"); len(got) != 1 || got[0].TonieID != "t1" {
t.Fatalf("expected 1 match for 'peppa', got %+v", got)
}
if got := Filter(tonies, "h2"); len(got) != 1 || got[0].TonieID != "t2" {
t.Fatalf("expected 1 match for 'h2', got %+v", got)
}
if got := Filter(tonies, "nomatch"); len(got) != 0 {
t.Fatalf("expected no matches, got %+v", got)
}
}