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:
@@ -0,0 +1,98 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user