Skip to content

bernardo-cs/tonys-cli

Repository files navigation

tonys

CI Release Go Reference Go Report Card License: MIT

An agent-friendly command-line wrapper around the TonieCloud API for creative tonies. Send audio — local files, stdin, or YouTube links — to your kids' tonies from scripts and bots, with automatic format conversion and loudness normalization.

It's a Go port of the Python library alexhartm/tonie_api, extended with a real token-caching auth layer, structured errors, full chapter editing, audio conversion/normalization, a YouTube importer, and a machine-readable command schema.

Note

Unofficial project. Not affiliated with, endorsed by, or supported by Boxine GmbH / tonies. Use your own account, at your own risk. See Disclaimer.

Why "agent-friendly"?

  • Non-interactive auth via env vars, with a cached & auto-refreshed token.
  • --json everywhere — stable JSON on stdout, errors as JSON on stderr.
  • Meaningful exit codes (0 ok, 1 error, 2 usage, 3 auth, 4 not-found).
  • tonys schema emits a JSON description of every command/flag for tools to introspect — the authoritative, always-current command reference.
  • tonys raw can hit any endpoint, so nothing in the API is out of reach.
  • Tonies and households can be referenced by name or id.

Install

Homebrew (macOS / Linux)

brew install bernardo-cs/tap/tonys

Linux packages (.deb / .rpm / .apk)

Each release attaches native packages for amd64 and arm64. Download the one for your distro from the latest release and install it:

# Debian / Ubuntu
sudo apt install ./tonys_0.1.0_linux_amd64.deb

# Fedora / RHEL / openSUSE
sudo dnf install ./tonys_0.1.0_linux_amd64.rpm     # or: sudo rpm -i ...

# Alpine
sudo apk add --allow-untrusted ./tonys_0.1.0_linux_amd64.apk

The binary installs to /usr/bin/tonys. ffmpeg/yt-dlp are listed as optional (recommended/suggested) dependencies, so installation never pulls them in by force.

Prebuilt binary

Download the archive for your OS/arch from the latest release, verify it against checksums.txt, then put tonys on your PATH:

# macOS arm64 example — adjust the version/OS/arch to taste
VER=0.1.0
curl -fsSLO https://github.com/bernardo-cs/tonys-cli/releases/download/v$VER/tonys_${VER}_darwin_arm64.tar.gz
curl -fsSLO https://github.com/bernardo-cs/tonys-cli/releases/download/v$VER/checksums.txt
shasum -a 256 -c checksums.txt --ignore-missing      # verify
tar xzf tonys_${VER}_darwin_arm64.tar.gz
sudo mv tonys /usr/local/bin/

With Go

go install github.com/bernardo-cs/tonys-cli/cmd/tonys@latest

From source

git clone https://github.com/bernardo-cs/tonys-cli
cd tonys-cli
make build        # produces ./tonys
# or: make install   # installs to $GOBIN

Optional external tools

The binary is self-contained Go, but two optional external tools are NOT bundled — they must be on PATH for the features that use them:

  • ffmpeg — audio conversion & loudness normalization (brew install ffmpeg, apt-get install ffmpeg, …)
  • yt-dlp — the yt YouTube importer (brew install yt-dlp, pipx install yt-dlp, …)

Plain uploads of already-accepted formats and every API command work without either. Run tonys doctor to check what's present (it prints OS-specific install hints); for provisioning a box, gate on tonys doctor --strict:

tonys doctor            # human/JSON report of deps, creds, writable paths
tonys doctor --online   # also verify login + API reachability
tonys doctor --strict   # exit non-zero unless every check is ok

Override the discovered binaries with $TONYS_FFMPEG / $TONYS_YTDLP.

Authentication

The recommended path for bots is environment variables; the token is exchanged once and cached (mode 0600) under ~/.cache/tonys/token.json, then auto-refreshed.

export TONIE_USERNAME="you@example.com"
export TONIE_PASSWORD="secret"

tonys auth login      # optional: pre-warm & verify the cache
tonys auth status     # show cached token validity
tonys auth logout     # clear the cache (add --all to clear every account)

Credential precedence: --username/--password flags → env → optional ~/.config/tonys/config.json. Override endpoints with --api-url / --token-url or $TONIE_API_URL / $TONIE_TOKEN_URL. Passwords are never logged or printed, and only ever sent to the OpenID token endpoint over HTTPS — see SECURITY.md.

Quick start

# Tables by default; add --json for machine output.
tonys household list
tonys tonie list
tonys tonie get "Erna-Tonie"

# Upload an audio file as a new chapter (the main command):
tonys upload "Erna-Tonie" bedtime.mp3
tonys upload "Erna-Tonie" story.m4a --title "Bedtime story" --wait
tonys upload "Erna-Tonie" intro.mp3 --position beginning

# Pipe from stdin:
cat song.mp3 | tonys upload "Erna-Tonie" - --title "Song"

Commands

tonys schema is the authoritative, machine-readable reference for every command and flag. A human summary:

Command Purpose
me the logged-in user (GET /me)
config backend config & upload limits (GET /config)
auth login|status|logout manage the token cache
household list list households
tonie list|get|rename list / inspect / rename creative tonies
upload <tonie> <file> upload a file (or -) as a new chapter
chapter list|add|add-existing|rm|rename|move|sort|clear edit chapters
yt <tonie> <url> import a YouTube video/playlist (needs yt-dlp)
intro detect <file>... find a shared (or spectral) intro in local files
outro detect <file>... find a shared (or spectral) closing in local files
loudness measure|show|measure-tonie loudness tooling (see below)
file create low-level: request a presigned upload slot
raw <METHOD> <path> call any endpoint directly
doctor check dependencies, credentials and writable paths
schema machine-readable description of all commands
version print version

Run tonys help <command> for full per-command help.

Global flags

Available on every command:

Flag Meaning
--json shorthand for --output json
-o, --output table|json|jsonl output format (default table)
--username, --password credentials (override env)
--api-url, --token-url endpoint overrides
--no-cache ignore the token cache for this call
--timeout 3m per-request timeout
--quiet suppress stderr status lines
--verbose extra stderr diagnostics

Chapters

Chapters can be referenced by id, 1-based number, or exact title:

tonys chapter list "Erna-Tonie"
tonys chapter rename "Erna-Tonie" 3 "New title"
tonys chapter move "Erna-Tonie" "Old intro" 1
tonys chapter rm "Erna-Tonie" 4 5            # remove chapters 4 and 5
tonys chapter sort "Erna-Tonie" 3 1 2        # reorder
tonys chapter clear "Erna-Tonie"

New chapters are added to the end by default. Pass --position beginning on upload, chapter add, chapter add-existing, or yt to put new audio at the front instead.

file create plus chapter add-existing is the low-level split of an upload: file create asks TonieCloud for a presigned upload slot (returning a fileId), and chapter add-existing <tonie> <fileId> attaches an already-uploaded file as a chapter. Most users just want upload, which does both.

Audio conversion

TonieCloud accepts aac aif aiff flac mp3 m4a m4b wav oga ogg opus wma. Anything else is auto-converted with ffmpeg before upload. Controls (on upload, chapter add, yt):

--convert auto|always|never          # default auto (convert only unsupported inputs)
--format mp3|opus|ogg|m4a|flac|wav   # target codec when converting (default mp3)
--bitrate 128k
--skip 30s                           # trim this many seconds from the start
--skip-end 10s                       # trim this many seconds from the end
tonys upload "Erna-Tonie" voice-memo.webm   # webm → mp3 automatically
tonys upload "Erna-Tonie" podcast.mp3 --skip 1m30s          # drop intro
tonys upload "Erna-Tonie" podcast.mp3 --skip-end 30s        # drop outro
tonys upload "Erna-Tonie" podcast.mp3 --skip 1m --skip-end 30s  # both

--format only takes effect when a conversion actually happens. With --convert auto (the default), an already-accepted input is uploaded as-is and keeps its original format; with --normalize on, a re-encode is required, so the file is encoded to --format (default mp3).

--skip and --skip-end accept any Go duration string (30s, 1m30s, 2m, …) and trim that many seconds from the start or end respectively. Both force an ffmpeg pass even for already-accepted formats. When combined with loudness normalization, the measure and encode passes both operate on the same trimmed window.

Loudness normalization

Normalize perceived volume (EBU R128 two-pass loudnorm) so chapters don't jump between quiet and loud. It runs in the same ffmpeg pass as conversion and is independent of the input bitrate/codec.

--normalize off|target|match   # default off
--target-lufs -16              # integrated loudness target
  • target brings the file to a fixed loudness (default −16 LUFS).
  • match brings the file to the average loudness of the chapters already on the tonie (as known to the local loudness ledger; see below).
tonys upload "Erna-Tonie" loud.wav  --normalize target --target-lufs -16
tonys upload "Erna-Tonie" quiet.mp3 --normalize match

The loudness ledger and match

TonieCloud does not offer a supported way to download a chapter's audio (the official API returns only metadata, and neither the website nor the iOS app expose downloads). So tonys keeps a local loudness ledger at ~/.local/share/tonys/loudness.json: every file you upload through tonys is measured and recorded, and --normalize match averages those records. When a tonie has no known chapter loudness yet, match falls back to --target-lufs. If you upload everything through tonys, your whole tonie stays at a consistent level.

tonys loudness measure song.mp3        # measure a local file's LUFS/true-peak/LRA
tonys loudness show "Erna-Tonie"       # what the ledger already knows for a tonie
tonys loudness measure-tonie "Erna-Tonie"

loudness measure-tonie additionally makes a best-effort attempt to fetch and measure the tonie's existing cloud chapters through an undocumented endpoint, caching any it can. This is not guaranteed: tonies-published content can never be downloaded, and many accounts/plans don't expose the endpoint at all — in those cases chapters are reported as not downloadable and the local ledger remains the reliable source for match.

YouTube import

tonys yt "Erna-Tonie" "https://youtu.be/XXXX"
tonys yt "Erna-Tonie" "https://youtube.com/playlist?list=YYYY" \
    --normalize target --limit 10 --title-prefix "Mix: " --wait

Each item is downloaded (yt-dlp), run through the same convert/normalize pipeline, and added as a chapter. Useful flags: --limit N, --reverse, --title-prefix, --position, --skip / --skip-end, --wait / --wait-timeout, --trim-intro / --trim-outro / --auto-trim (see below), and --continue-on-error (default on) which keeps going if one item fails and reports failures in the summary.

YouTube radio URLs such as watch?v=...&list=RD...&start_radio=1 can expand to large auto-generated playlists. tonys yt warns when it sees one and asks whether to import only the current video or the whole radio playlist. Choosing the current video cleans the URL before download; pass --allow-radio to skip the prompt when you intended to import the radio playlist.

Automatic intro & outro trimming

Many playlists stamp the same channel intro — and sometimes a closing jingle — on every video. tonys can find and cut those with pure signal analysis (spectral fingerprints compared across files; no AI, no network):

tonys yt "Erna-Tonie" PLAYLIST_URL --trim-intro auto     # cut the shared intro
tonys yt "Erna-Tonie" PLAYLIST_URL --trim-outro auto     # cut the shared closing
tonys yt "Erna-Tonie" PLAYLIST_URL --auto-trim           # both at once

Modes for --trim-intro / --trim-outro:

  • common — fingerprint every downloaded item and measure the exact audio prefix (or suffix) the batch shares, then cut it per item. Items that lack the jingle are left untouched, duplicated songs can't fool the consensus, and a short pause between the jingle and the content (up to ~3s) is trimmed with it. This is cross-file evidence and is what auto uses for playlists.
  • spectral — single-file heuristic: find the point where the average spectrum before differs most from the audio after, and cut only when the candidate jingle is both sharply bounded and internally uniform. Real songs can still fool it (a fade-out or a distinct first section looks similar), so it never runs implicitly: with auto on a single video, tonys only reports the suspected boundary and leaves the audio alone until you opt in with spectral or cut manually with --skip/--skip-end.

Bounds: --intro-max/--outro-max (default 1m) limit how long a jingle may be, --intro-min/--outro-min (default 1s) how short. Detected cuts are added on top of any manual --skip/--skip-end. Note that trimming downloads the whole batch before the first upload, since common mode needs all items on disk to compare them.

The same detection is available offline for local files — handy to preview what would be cut, or to feed --skip/--skip-end yourself:

tonys intro detect *.mp3            # per-file cut points + shared intro length
tonys outro detect *.mp3            # same, measured from the end of each file
tonys intro detect --mode spectral song.mp3   # single-file heuristic, with score
tonys intro detect *.webm --max 30s --json    # machine-readable

Using it from an agent / script

export TONIE_USERNAME=... TONIE_PASSWORD=...

# Discover the interface:
tonys schema | jq .

# Machine output + robust error handling:
if out=$(tonys --json tonie get "Erna-Tonie"); then
    echo "$out" | jq '.chapters | length'
else
    code=$?            # 2 = usage, 3 = auth, 4 = not found, 1 = other
fi

Successful output goes to stdout as JSON; errors go to stderr as {"error": "...", "class": "...", "status": N}. stdout stays clean for parsing.

Exit code Class Meaning
0 success
1 error generic / unexpected error
2 usage bad flags or arguments (incl. ambiguous references)
3 auth authentication failure
4 not_found tonie / household / chapter not found

Environment variables

Var Meaning
TONIE_USERNAME, TONIE_PASSWORD credentials (aliases: TONIES_*, TONIE_CLOUD_*)
TONYS_OUTPUT default output format (table/json/jsonl)
TONYS_CONFIG, TONYS_CACHE, TONYS_LOUDNESS_DB explicit state-file locations
TONIE_API_URL, TONIE_TOKEN_URL endpoint overrides
TONYS_FFMPEG, TONYS_YTDLP binary overrides (aliases: TONIE_FFMPEG, TONIE_YTDLP)
XDG_CONFIG_HOME, XDG_CACHE_HOME, XDG_DATA_HOME base dirs for config / token cache / ledger

Development

make build    # build ./tonys
make test     # go test ./...
make vet      # go vet ./...
make fmt      # gofmt -w .
make cover    # coverage summary

See CONTRIBUTING.md for conventions, and SECURITY.md for the security model and how to report issues.

Disclaimer

tonys is an unofficial tool. It is not affiliated with, authorized, endorsed, or sponsored by Boxine GmbH or tonies. "tonies", "TonieCloud", "Toniebox", and "Creative-Tonie" are trademarks of their respective owners; they are used here only to describe what this tool interoperates with.

The tool talks to TonieCloud on your behalf using your own credentials. It relies on undocumented endpoints that can change or break at any time. Use it at your own risk and in accordance with tonies' Terms of Service. The software is provided "as is", without warranty of any kind — see LICENSE.

License

MIT © 2026 Bernardo Simoes. Ported from the MIT-licensed alexhartm/tonie_api.

About

Agent-friendly Go CLI for the TonieCloud API — upload audio (files, stdin, YouTube) to creative tonies, edit chapters, with ffmpeg conversion & loudness normalization. Unofficial.

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors