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>
110 lines
3.4 KiB
Go
110 lines
3.4 KiB
Go
// Package library discovers Kreativ-Tonie mappings from a folder structure,
|
|
// instead of a separate config file:
|
|
//
|
|
// <root>/<household_id>/<freier_name> [id=<tonie_id>]/*.mp3
|
|
//
|
|
// The household id is the top-level folder name, and each tonie folder
|
|
// name encodes both a free-form display name and the tonie id in brackets.
|
|
// This keeps the "configuration" fully visible and editable as plain
|
|
// folders on disk.
|
|
package library
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
// tonieDirPattern matches folder names like "Peppa Wutz [id=ef01-5678]".
|
|
var tonieDirPattern = regexp.MustCompile(`^(?P<name>.+) \[id=(?P<id>[^\]]+)\]$`)
|
|
|
|
// Tonie is a single discovered mapping between a local folder and a Kreativ-Tonie.
|
|
type Tonie struct {
|
|
HouseholdID string // taken from the top-level folder name
|
|
TonieID string // parsed from the "[id=...]" suffix of the folder name
|
|
Name string // free-form part of the folder name (before " [id=...]")
|
|
Folder string // absolute path to the folder containing the audio files
|
|
}
|
|
|
|
// ParseTonieDir parses a single tonie folder name into name and id.
|
|
// Returns ok=false if the name doesn't match the expected "<name> [id=<id>]" pattern.
|
|
func ParseTonieDir(dirName string) (name, id string, ok bool) {
|
|
m := tonieDirPattern.FindStringSubmatch(dirName)
|
|
if m == nil {
|
|
return "", "", false
|
|
}
|
|
return m[1], m[2], true
|
|
}
|
|
|
|
// DirName builds the folder name for a tonie: "<name> [id=<id>]".
|
|
func DirName(name, id string) string {
|
|
return fmt.Sprintf("%s [id=%s]", sanitizeName(name), id)
|
|
}
|
|
|
|
// sanitizeName strips path separators and other characters that are unsafe in folder names.
|
|
func sanitizeName(name string) string {
|
|
replacer := strings.NewReplacer("/", "-", "\\", "-", ":", "-")
|
|
return strings.TrimSpace(replacer.Replace(name))
|
|
}
|
|
|
|
// Discover walks root and returns all tonie mappings found in it.
|
|
// Non-matching entries (files, folders without the "[id=...]" suffix) are skipped.
|
|
func Discover(root string) ([]Tonie, error) {
|
|
householdEntries, err := os.ReadDir(root)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("reading library root %s: %w", root, err)
|
|
}
|
|
|
|
var tonies []Tonie
|
|
for _, hEntry := range householdEntries {
|
|
if !hEntry.IsDir() {
|
|
continue
|
|
}
|
|
householdID := hEntry.Name()
|
|
householdPath := filepath.Join(root, householdID)
|
|
|
|
tonieEntries, err := os.ReadDir(householdPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("reading household folder %s: %w", householdPath, err)
|
|
}
|
|
|
|
for _, tEntry := range tonieEntries {
|
|
if !tEntry.IsDir() {
|
|
continue
|
|
}
|
|
name, tonieID, ok := ParseTonieDir(tEntry.Name())
|
|
if !ok {
|
|
continue
|
|
}
|
|
tonies = append(tonies, Tonie{
|
|
HouseholdID: householdID,
|
|
TonieID: tonieID,
|
|
Name: name,
|
|
Folder: filepath.Join(householdPath, tEntry.Name()),
|
|
})
|
|
}
|
|
}
|
|
return tonies, nil
|
|
}
|
|
|
|
// Filter returns only the tonies matching the given filter string, matched
|
|
// case-insensitively as a substring against household id, tonie id, and name.
|
|
// An empty filter returns all tonies.
|
|
func Filter(tonies []Tonie, filter string) []Tonie {
|
|
if filter == "" {
|
|
return tonies
|
|
}
|
|
filter = strings.ToLower(filter)
|
|
var matched []Tonie
|
|
for _, t := range tonies {
|
|
if strings.Contains(strings.ToLower(t.HouseholdID), filter) ||
|
|
strings.Contains(strings.ToLower(t.TonieID), filter) ||
|
|
strings.Contains(strings.ToLower(t.Name), filter) {
|
|
matched = append(matched, t)
|
|
}
|
|
}
|
|
return matched
|
|
}
|