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)
}