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
+15 -7
View File
@@ -9,7 +9,6 @@ import (
"sort"
"strings"
"github.com/arnef/toni-sync/internal/config"
"github.com/arnef/toni-sync/internal/tonieapi"
)
@@ -21,6 +20,15 @@ type TonieClient interface {
SortChaptersOfTonie(tonie tonieapi.CreativeTonie, chapters []tonieapi.Chapter) error
}
// Target describes what to sync: a local folder and the Kreativ-Tonie it maps to.
// Mappings come from the on-disk library folder structure (see internal/library),
// not from a separate config file.
type Target struct {
TonieID string
Folder string
Prune bool // remove chapters that no longer have a matching local file
}
const maxTitleLength = 100
var audioExtensions = map[string]bool{
@@ -110,13 +118,13 @@ func (p *Plan) NeedsChanges() bool {
}
// BuildPlan fetches the current state of the tonie and computes the diff against the local folder.
func BuildPlan(client TonieClient, m config.Mapping) (*Plan, error) {
tonie, err := client.GetCreativeTonie(m.TonieID)
func BuildPlan(client TonieClient, target Target) (*Plan, error) {
tonie, err := client.GetCreativeTonie(target.TonieID)
if err != nil {
return nil, err
}
tracks, err := ListLocalTracks(m.Folder)
tracks, err := ListLocalTracks(target.Folder)
if err != nil {
return nil, err
}
@@ -142,7 +150,7 @@ func BuildPlan(client TonieClient, m config.Mapping) (*Plan, error) {
var toRemove []tonieapi.Chapter
finalOrder := append([]string{}, orderedTitles...)
if m.Prune {
if target.Prune {
for _, c := range tonie.Chapters {
if !localTitles[c.Title] {
toRemove = append(toRemove, c)
@@ -166,14 +174,14 @@ func BuildPlan(client TonieClient, m config.Mapping) (*Plan, error) {
}
// ApplyPlan uploads missing tracks, then reorders/prunes chapters to match the local folder.
func ApplyPlan(client TonieClient, m config.Mapping, plan *Plan) error {
func ApplyPlan(client TonieClient, target Target, plan *Plan) error {
for _, track := range plan.ToUpload {
if err := client.UploadFileToTonie(plan.Tonie, track.Path, track.Title); err != nil {
return fmt.Errorf("uploading %s: %w", track.Path, err)
}
}
refreshed, err := client.GetCreativeTonie(m.TonieID)
refreshed, err := client.GetCreativeTonie(target.TonieID)
if err != nil {
return fmt.Errorf("refetching tonie after upload: %w", err)
}
+5 -6
View File
@@ -5,7 +5,6 @@ import (
"path/filepath"
"testing"
"github.com/arnef/toni-sync/internal/config"
"github.com/arnef/toni-sync/internal/tonieapi"
)
@@ -79,7 +78,7 @@ func TestBuildPlanDetectsUploadNoRemoval(t *testing.T) {
tonie := makeTonie([]tonieapi.Chapter{{ID: "c1", Title: "01 - First"}})
client := &fakeClient{tonieSequence: []tonieapi.CreativeTonie{tonie}}
m := config.Mapping{Name: "m", HouseholdID: "h1", TonieID: "t1", Folder: dir, Prune: true}
m := Target{TonieID: "t1", Folder: dir, Prune: true}
plan, err := BuildPlan(client, m)
if err != nil {
t.Fatal(err)
@@ -101,7 +100,7 @@ func TestBuildPlanPrunesRemovedChapters(t *testing.T) {
tonie := makeTonie([]tonieapi.Chapter{{ID: "c1", Title: "01 - First"}, {ID: "c2", Title: "stale"}})
client := &fakeClient{tonieSequence: []tonieapi.CreativeTonie{tonie}}
m := config.Mapping{Name: "m", HouseholdID: "h1", TonieID: "t1", Folder: dir, Prune: true}
m := Target{TonieID: "t1", Folder: dir, Prune: true}
plan, err := BuildPlan(client, m)
if err != nil {
t.Fatal(err)
@@ -116,7 +115,7 @@ func TestBuildPlanNoPruneKeepsStaleChapters(t *testing.T) {
tonie := makeTonie([]tonieapi.Chapter{{ID: "c1", Title: "01 - First"}, {ID: "c2", Title: "stale"}})
client := &fakeClient{tonieSequence: []tonieapi.CreativeTonie{tonie}}
m := config.Mapping{Name: "m", HouseholdID: "h1", TonieID: "t1", Folder: dir, Prune: false}
m := Target{TonieID: "t1", Folder: dir, Prune: false}
plan, err := BuildPlan(client, m)
if err != nil {
t.Fatal(err)
@@ -136,7 +135,7 @@ func TestApplyPlanUploadsAndSkipsReorderWhenAlreadyMatching(t *testing.T) {
after := makeTonie([]tonieapi.Chapter{{ID: "c1", Title: "01 - First"}, {ID: "c2", Title: "02 - Second"}})
client := &fakeClient{tonieSequence: []tonieapi.CreativeTonie{before, after}}
m := config.Mapping{Name: "m", HouseholdID: "h1", TonieID: "t1", Folder: dir, Prune: true}
m := Target{TonieID: "t1", Folder: dir, Prune: true}
plan, err := BuildPlan(client, m)
if err != nil {
t.Fatal(err)
@@ -158,7 +157,7 @@ func TestApplyPlanReordersWhenOrderDiffers(t *testing.T) {
reversed := makeTonie([]tonieapi.Chapter{{ID: "c2", Title: "02 - Second"}, {ID: "c1", Title: "01 - First"}})
client := &fakeClient{tonieSequence: []tonieapi.CreativeTonie{reversed, reversed}}
m := config.Mapping{Name: "m", HouseholdID: "h1", TonieID: "t1", Folder: dir, Prune: true}
m := Target{TonieID: "t1", Folder: dir, Prune: true}
plan, err := BuildPlan(client, m)
if err != nil {
t.Fatal(err)