toniecloud

package
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 3, 2026 License: MIT Imports: 17 Imported by: 0

Documentation

Overview

Package toniecloud is a Go client for the TonieCloud REST API (https://api.tonie.cloud/v2). It is a faithful, feature-complete port of the Python library github.com/alexhartm/tonie_api, extended with a real token-caching auth layer, structured errors, single-tonie reads and chapter-level editing.

It is NOT associated with Boxine / tonies.de in any way.

Index

Constants

View Source
const (
	DefaultTokenURL = "https://login.tonies.com/auth/realms/tonies/protocol/openid-connect/token"
	DefaultClientID = "my-tonies"
)

Default OpenID Connect endpoint and client used by the official my tonies app.

View Source
const DefaultAPIURL = "https://api.tonie.cloud/v2"

DefaultAPIURL is the TonieCloud REST base URL.

Variables

View Source
var ErrChapterDownloadUnavailable = errors.New("chapter audio download not available")

ErrChapterDownloadUnavailable is returned when a chapter's audio cannot be downloaded (no available endpoint, or a content-token chapter).

View Source
var ErrNotAuthenticated = errors.New("not authenticated: set TONIE_USERNAME and TONIE_PASSWORD (or run `tonys auth login`)")

ErrNotAuthenticated is returned when no credentials and no usable cached token are available.

View Source
var UserAgent = "tonys-cli"

UserAgent is sent with every request; overridable by the CLI.

Functions

func IsNotFound

func IsNotFound(err error) bool

IsNotFound reports whether err is an APIError with a 404 status.

func IsUnauthorized

func IsUnauthorized(err error) bool

IsUnauthorized reports whether err is an APIError with a 401/403 status, which usually means the token expired or the credentials are wrong.

func NormalizeUser

func NormalizeUser(u string) string

NormalizeUser returns the cache-key form of a username (lowercased, trimmed). Exposed so the CLI computes identical keys.

Types

type APIError

type APIError struct {
	Method string
	URL    string
	Status int
	// Detail is the server's human-readable message when one could be parsed.
	Detail string
	// Body is the raw response body (truncated) for debugging.
	Body string
}

APIError represents a non-2xx HTTP response from TonieCloud (or S3). Unlike the upstream Python library, which silently swallows failures and returns an empty dict, we surface the status, endpoint and server-provided detail so callers (and agents) can react.

func (*APIError) Error

func (e *APIError) Error() string

type Authenticator

type Authenticator struct {
	TokenURL string
	ClientID string
	Username string
	Password string

	HTTP  *http.Client
	Cache *TokenCache // optional; nil disables persistence
	// contains filtered or unexported fields
}

Authenticator turns a username/password into a bearer token, transparently caching it and refreshing via the OpenID refresh_token grant when possible so that repeated CLI invocations (e.g. from a bot) do not hammer the SSO server.

func (*Authenticator) Login

func (a *Authenticator) Login(ctx context.Context) (TokenEntry, error)

Login forces a fresh password grant (ignoring any cached access token) and persists the result. Used by `tonys auth login`.

func (*Authenticator) Token

func (a *Authenticator) Token(ctx context.Context) (string, error)

Token returns a valid bearer token, acquiring or refreshing one as needed.

type Chapter

type Chapter struct {
	ID    string `json:"id"`
	Title string `json:"title"`
	// File is the file identifier. For new chapters this is the UUID returned
	// by POST /file. For existing chapters it is an opaque blob; for content
	// shipped by tonies it starts with "ContentToken:".
	File        string  `json:"file"`
	Seconds     float64 `json:"seconds"`
	Transcoding bool    `json:"transcoding"`
}

Chapter is a single audio track on a creative tonie.

func (Chapter) IsContentToken

func (c Chapter) IsContentToken() bool

IsContentToken reports whether the chapter is tonies-published content rather than a user upload. Such chapters cannot be re-uploaded, only reordered or removed.

type Client

type Client struct {
	APIURL string
	HTTP   *http.Client
	Auth   *Authenticator
}

Client talks to the TonieCloud REST API. Construct it with New.

func New

func New(apiURL string, auth *Authenticator, httpClient *http.Client) *Client

New builds a Client. A nil httpClient uses http.DefaultClient.

func (*Client) AddChapter

func (c *Client) AddChapter(ctx context.Context, t CreativeTonie, fileID, title string) error

AddChapter appends an already-uploaded file as a new chapter (POST /households/{hid}/creativetonies/{id}/chapters).

func (*Client) ChapterDownloadURL

func (c *Client) ChapterDownloadURL(ctx context.Context, t CreativeTonie, ch Chapter) (string, error)

ChapterDownloadURL resolves a temporary URL to a chapter's audio, if the backend exposes one. The endpoint used here is discovered empirically; if the account/plan does not expose it, ErrChapterDownloadUnavailable is returned.

func (*Client) CreateFileUpload

func (c *Client) CreateFileUpload(ctx context.Context) (FileUploadRequest, error)

CreateFileUpload requests a presigned S3 upload slot (POST /file).

func (*Client) CreativeTonie

func (c *Client) CreativeTonie(ctx context.Context, householdID, tonieID string) (CreativeTonie, error)

CreativeTonie fetches a single creative tonie (GET /households/{hid}/creativetonies/{id}).

func (*Client) CreativeTonies

func (c *Client) CreativeTonies(ctx context.Context) ([]CreativeTonie, error)

CreativeTonies lists every creative tonie across all of the user's households.

func (*Client) CreativeToniesByHousehold

func (c *Client) CreativeToniesByHousehold(ctx context.Context, householdID string) ([]CreativeTonie, error)

CreativeToniesByHousehold lists all creative tonies in one household.

func (*Client) DownloadChapter

func (c *Client) DownloadChapter(ctx context.Context, t CreativeTonie, ch Chapter, destPath string) error

DownloadChapter writes a chapter's audio to destPath.

func (*Client) GetConfig

func (c *Client) GetConfig(ctx context.Context) (Config, error)

GetConfig returns the backend configuration (GET /config).

func (*Client) Households

func (c *Client) Households(ctx context.Context) ([]Household, error)

Households returns all households of the logged-in user (GET /households).

func (*Client) Me

func (c *Client) Me(ctx context.Context) (User, error)

Me returns the logged-in user (GET /me).

func (*Client) Patch

func (c *Client) Patch(ctx context.Context, t CreativeTonie, fields map[string]any) (CreativeTonie, error)

Patch applies an arbitrary set of fields to a tonie via PATCH.

func (*Client) Raw

func (c *Client) Raw(ctx context.Context, method, path string, body any) (json.RawMessage, error)

Raw performs an arbitrary authenticated API call and returns the raw JSON response. It is the escape hatch that makes every endpoint reachable, even ones without a dedicated method. method is GET/POST/PATCH/etc.; path is relative to the API base (leading slash optional); body is optional JSON.

func (*Client) RenameTonie

func (c *Client) RenameTonie(ctx context.Context, t CreativeTonie, name string) (CreativeTonie, error)

RenameTonie changes a tonie's display name via PATCH.

func (*Client) SetChapters

func (c *Client) SetChapters(ctx context.Context, t CreativeTonie, chapters []Chapter) (CreativeTonie, error)

SetChapters replaces the chapter list (and order) of a tonie via PATCH. This powers sorting, removing and renaming chapters and clearing the tonie.

func (*Client) UploadFile

func (c *Client) UploadFile(ctx context.Context, t CreativeTonie, path, title string) (UploadResult, error)

UploadFile uploads a local audio file and appends it as a new chapter on the tonie. If title is empty the file's base name (without extension) is used. This is the three-step flow: POST /file → presigned S3 upload → POST chapters.

func (*Client) UploadReader

func (c *Client) UploadReader(ctx context.Context, t CreativeTonie, r io.Reader, title, contentType string) (UploadResult, error)

UploadReader is like UploadFile but reads content from r, so callers can pipe data (e.g. from stdin). contentType may be empty.

func (*Client) WaitForTranscoding

func (c *Client) WaitForTranscoding(ctx context.Context, householdID, tonieID string, interval, timeout time.Duration) (CreativeTonie, error)

WaitForTranscoding polls the tonie until transcoding finishes, a transcoding error is reported, or the timeout/context expires, returning the final tonie state. A zero interval defaults to 2s; a non-positive timeout means "no overall deadline" (still cancellable via ctx). This prevents an unattended `--wait` from spinning forever when the backend stalls or fails a transcode.

type Config

type Config struct {
	Locales        []string `json:"locales"`
	UnicodeLocales []string `json:"unicodeLocales"`
	MaxChapters    int      `json:"maxChapters"`
	MaxSeconds     int      `json:"maxSeconds"`
	MaxBytes       int64    `json:"maxBytes"`
	Accepts        []string `json:"accepts"`
	StageWarning   bool     `json:"stageWarning"`
	PaypalClientID string   `json:"paypalClientId"`
	SSOEnabled     bool     `json:"ssoEnabled"`
}

Config is the backend configuration and upload limits (GET /config).

type CreativeTonie

type CreativeTonie struct {
	ID                string     `json:"id"`
	HouseholdID       string     `json:"householdId"`
	Name              string     `json:"name"`
	ImageURL          string     `json:"imageUrl"`
	SecondsRemaining  float64    `json:"secondsRemaining"`
	SecondsPresent    float64    `json:"secondsPresent"`
	ChaptersRemaining int        `json:"chaptersRemaining"`
	ChaptersPresent   int        `json:"chaptersPresent"`
	Transcoding       bool       `json:"transcoding"`
	Live              bool       `json:"live"`
	Private           bool       `json:"private"`
	LastUpdate        *time.Time `json:"lastUpdate"`
	TranscodingErrors []any      `json:"transcodingErrors,omitempty"`
	Chapters          []Chapter  `json:"chapters"`
}

CreativeTonie is a re-recordable tonie figurine (GET /households/{id}/creativetonies).

type FileUploadRequest

type FileUploadRequest struct {
	Request Request `json:"request"`
	FileID  string  `json:"fileId"`
}

FileUploadRequest is returned by POST /file and contains a presigned S3 form plus the fileId to reference once the upload completes.

type Household

type Household struct {
	ID        string `json:"id"`
	Name      string `json:"name"`
	OwnerName string `json:"ownerName"`
	Access    string `json:"access"`
	CanLeave  bool   `json:"canLeave"`
	Image     string `json:"image,omitempty"`

	// ForeignCreativeTonieContent indicates whether content from other
	// households may be played on this household's tonies.
	ForeignCreativeTonieContent bool `json:"foreignCreativeTonieContent,omitempty"`
}

Household groups creative tonies and is the unit of access control (GET /households).

type Request

type Request struct {
	URL    string            `json:"url"`
	Fields map[string]string `json:"fields"`
}

Request is the presigned S3 POST returned inside a FileUploadRequest.

type TokenCache

type TokenCache struct {
	Path string
	// contains filtered or unexported fields
}

TokenCache is a tiny JSON file ({"entries": {key: entry}}) holding bearer tokens, written with 0600 permissions. It is keyed by the normalized (lowercased) username so multiple accounts can coexist.

func NewTokenCache

func NewTokenCache(path string) *TokenCache

NewTokenCache returns a cache backed by path. A blank path yields an in-memory cache that is never persisted.

func (*TokenCache) Delete

func (c *TokenCache) Delete(key string)

Delete removes an entry (all entries if key is empty) and persists.

func (*TokenCache) Entries

func (c *TokenCache) Entries() map[string]TokenEntry

Entries returns a copy of all cached entries.

func (*TokenCache) Get

func (c *TokenCache) Get(key string) (TokenEntry, bool)

Get returns the entry for key, if present.

func (*TokenCache) Put

func (c *TokenCache) Put(key string, e TokenEntry)

Put stores an entry and persists the cache.

func (*TokenCache) SoleKey

func (c *TokenCache) SoleKey() (string, bool)

SoleKey returns the only key in the cache, if exactly one exists. This lets the CLI reuse a cached token when no username is supplied.

type TokenEntry

type TokenEntry struct {
	Username         string    `json:"username"`
	AccessToken      string    `json:"access_token"`
	RefreshToken     string    `json:"refresh_token,omitempty"`
	ExpiresAt        time.Time `json:"expires_at"`
	RefreshExpiresAt time.Time `json:"refresh_expires_at,omitempty"`
}

TokenEntry is one account's cached credentials.

func (TokenEntry) Valid

func (e TokenEntry) Valid(now time.Time) bool

Valid reports whether the access token is still usable (with a safety skew).

type UploadResult

type UploadResult struct {
	FileID string `json:"fileId"`
	Title  string `json:"title"`
}

UploadResult describes a completed upload + chapter creation.

type User

type User struct {
	UUID  string `json:"uuid"`
	Email string `json:"email"`

	// Extra holds every other field returned by /me verbatim.
	Extra map[string]any `json:"-"`
}

User is the account behind a set of credentials (GET /me).

The TonieCloud API returns many more fields than the upstream Python library models; we keep the documented essentials as typed fields and preserve everything else (firstName, locale, country, region, ...) in Extra so no data is lost and agents can read arbitrary attributes.

func (User) MarshalJSON

func (u User) MarshalJSON() ([]byte, error)

MarshalJSON re-flattens Extra back alongside the typed fields.

func (*User) UnmarshalJSON

func (u *User) UnmarshalJSON(b []byte) error

UnmarshalJSON keeps the typed uuid/email fields while preserving every other attribute the API returns in Extra.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL