Files
arnefandCopilot ae46e8f5c0 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>
2026-08-14 12:02:54 +02:00

99 lines
2.7 KiB
Go

package library
import (
"fmt"
"os"
"path/filepath"
"github.com/arnef/toni-sync/internal/tonieapi"
)
// ScaffoldClient is the subset of tonieapi.Client needed to scaffold a library folder tree.
type ScaffoldClient interface {
GetHouseholds() ([]tonieapi.Household, error)
GetCreativeTonies(householdID string) ([]tonieapi.CreativeTonie, error)
}
// ScaffoldResult reports what Scaffold did for a single tonie.
type ScaffoldResult struct {
HouseholdID string
TonieID string
Name string
Folder string
Created bool // false if a matching folder already existed and was left untouched
}
// Scaffold creates the "<root>/<household_id>/<name> [id=<tonie_id>]" folder
// structure for every household and Kreativ-Tonie on the account. Existing
// tonie folders (matched by id, regardless of the current display name) are
// left untouched so local audio files are never affected.
func Scaffold(client ScaffoldClient, root string) ([]ScaffoldResult, error) {
households, err := client.GetHouseholds()
if err != nil {
return nil, err
}
var results []ScaffoldResult
for _, h := range households {
householdPath := filepath.Join(root, h.ID)
if err := os.MkdirAll(householdPath, 0o755); err != nil {
return nil, fmt.Errorf("creating household folder %s: %w", householdPath, err)
}
existingByID, err := existingTonieFoldersByID(householdPath)
if err != nil {
return nil, err
}
tonies, err := client.GetCreativeTonies(h.ID)
if err != nil {
return nil, err
}
for _, t := range tonies {
if existingDir, ok := existingByID[t.ID]; ok {
results = append(results, ScaffoldResult{
HouseholdID: h.ID,
TonieID: t.ID,
Name: t.Name,
Folder: filepath.Join(householdPath, existingDir),
Created: false,
})
continue
}
dirName := DirName(t.Name, t.ID)
tonieFolder := filepath.Join(householdPath, dirName)
if err := os.MkdirAll(tonieFolder, 0o755); err != nil {
return nil, fmt.Errorf("creating tonie folder %s: %w", tonieFolder, err)
}
results = append(results, ScaffoldResult{
HouseholdID: h.ID,
TonieID: t.ID,
Name: t.Name,
Folder: tonieFolder,
Created: true,
})
}
}
return results, nil
}
// existingTonieFoldersByID maps tonie id -> existing folder name within a household folder.
func existingTonieFoldersByID(householdPath string) (map[string]string, error) {
entries, err := os.ReadDir(householdPath)
if err != nil {
return nil, fmt.Errorf("reading household folder %s: %w", householdPath, err)
}
byID := make(map[string]string)
for _, e := range entries {
if !e.IsDir() {
continue
}
if _, id, ok := ParseTonieDir(e.Name()); ok {
byID[id] = e.Name()
}
}
return byID, nil
}