Skip to content
RadioAnimuPublic

About

TypeScript API client for Rádio Animu — now playing, programs, history, music requests, live requests, streams and auth. Zero runtime dependencies.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

animu-api

TypeScript client for the Animu radio API.

  • Zero runtime dependencies. fetch + valibot (bundled).
  • Every response validated at the boundary; malformed payloads degrade instead of throwing.
  • Timeouts, GET micro-cache, uniform errors.
  • ESM + CJS, tree-shakeable, fully typed.

Works on Node ≥ 18, browsers, Deno, Bun and React Native.

Install

pnpm add animu-api
# or: npm install animu-api

Usage

import { AnimuApi, AnimuAuth } from "animu-api";

const animu = new AnimuApi();

const { track, listeners } = await animu.getStreamMetadata();
const program = await animu.getProgram();
const played = await animu.getTrackHistory("played");
const streams = await animu.getStreams();
const page = await animu.searchMusicByTitle("attack");

Realtime (SSE)

animu.live consumes the station's realtime Server-Sent Events stream (https://api.animu.moe/tungtungtung/) — no polling. The first subscriber opens one shared connection; drops reconnect automatically with capped exponential backoff.

const stop = animu.live.subscribe({
  onSongChange: ({ track, listeners, status, album }) =>
    console.log(track?.title, listeners.value, status),
  onListeners: (listeners) => console.log("listeners:", listeners.value),
  onError: (error) => console.warn(error),
});

stop.close();

Async iteration works too:

for await (const event of animu.live.events()) {
  if (event.type === "song_change") console.log(event.song.track?.title);
}

Late subscribers immediately receive the last known song and listener count. The stream is long-lived, so it applies no request timeout — it needs a streaming-capable fetch (browsers, Node ≥ 18, Deno, Bun natively; expo/fetch in React Native) and works with any custom fetchImpl — the client decodes UTF-8 internally, no TextDecoder global required.

Auth (v5)

Multi-provider OAuth (Discord, Google, Apple), Animu Connect (passwordless email codes) and profile management via the Animu Login System. Also reachable as animu.auth.

const auth = new AnimuAuth(); // or: new AnimuApi({ authBaseUrl }).auth

const providers = await auth.getProviders();
const { user, sessionToken } = await auth.exchangeToken({
  provider: "discord",
  code,
  redirectUri: "myapp://callback",
});
const profile = await auth.getProfile();      // reuses the stored sessionToken
await auth.requestEmailLoginCode("meu@email.com");   // email → emailed 4-digit code
await auth.verifyEmailLoginCode({ email: "meu@email.com", code: "1234" });
const linked = await auth.linkProvider({ provider: "google", code, redirectUri });
await auth.unlinkProvider("google");
const avatar = await auth.getAvatar();        // { bytes, contentType }
await auth.logout();

Server failures throw AnimuApiError with .statusCode and a machine-readable .code (e.g. token_exchange_failed, last_provider, no_banner).

Native Google Sign-In (React Native / iOS / Android) exchanges the platform SDK's serverAuthCode with no redirectUri/PKCE: auth.exchangeToken({ provider: "google", code: serverAuthCode }).

Native Sign in with Apple posts the SDK's RS256 identityToken (no redirectUri, Services ID or .p8); forward the name fields since Apple only sends them on first consent:

await auth.exchangeToken({
  provider: "apple",
  identityToken,
  name: fullName,
  firstName: givenName,
  lastName: familyName,
});

Alternatively, Discord, Google and Apple can log in server-side (no native SDK): open auth.mobileStartUrl("discord" | "google" | "apple") in a browser session, then feed the deep link back to auth.completeMobileAuth(url) — it parses token/error and adopts the session. Pass the current session token (mobileStartUrl(provider, auth.sessionToken)) to link the provider to the account instead (action=linked, token unchanged). The pending OAuth state is DB-backed, so the callback works even when the browser drops the session cookie (Apple form_post, Android Custom Tabs).

Configuration

All options are optional.

new AnimuApi({
  userAgent,        // "animu-api" (or derived from clientInfo)
  clientInfo,       // { platform, app, version, build, os, model, language, ... }
  timeout,          // 20_000 ms
  artworkQuality,   // "medium" — "off" | "low" | "medium" | "high"
  defaultCover,     // Animu's default cover
  fallbackStreams,  // Animu's public relays
  authBaseUrl,      // Animu Auth (v5) deploy base for `api.auth`
  liveUrl,          // realtime SSE endpoint for `api.live`
  liveIdleTimeout,  // reconnect `api.live` after this many ms without a byte (0 = off)
});

AnimuLive accepts extra options (live.subscribe / new AnimuLive(...)) — notably inboxSize (replay buffer for late subscribers, default 128), maxPending (per-events() backlog bound, default 120) and idleTimeout (reconnect a connection silent this many ms — the server sends no keep-alive, so a socket that died without closing is otherwise never noticed; default 0, off).

Errors

  • AnimuApiError — network and HTTP failures. Inspect .statusCode, .url, .method, .code.
  • ValidationError — payload failed schema validation, or input was rejected before any network call.

submitMusicRequest reports business errors as data (RequestResult), not throws. Codes: PEDIBLOCK, ANIBLOCK, ARTISTBLOCK, COVERBLOCK, HARUBLOCK, STRIKE_AND_OUT, ONAIR, BLOCOBLOCK, NOLOGIN, NO2FA, PANEL_UNAVAILABLE, REQUEST_ERROR.

Methods

Method Purpose
getStreamMetadata() Current track + listener count
getListeners() Listener count
getProgram() Current program / DJ
getTrackHistory(type) "played" or "requests" history
searchMusic(params) Search requestable tracks
searchMusicByTitle(title) Search with endpoint defaults
submitMusicRequest(submission) Submit a request (structured result)
submitLiveRequest(request) Validate + submit a live shout-out
getStreams(forceRefresh?) Audio streams, cached with fallback
validateSession(sessionId) Legacy PHP session check
logout(sessionId) Legacy server-side logout (best-effort)
exchangeToken(params) Legacy Discord OAuth2 code → User
live.subscribe(handlers) Realtime SSE stream (song_change + listeners)
live.events(signal?) Realtime events as an async iterator
auth.* Auth API v5 — see below

Auth API v5 (AnimuAuth)

Method Purpose
getProviders() Configured login providers
exchangeToken(params) OAuth code → session (primary mobile login; server refreshes linked providers first)
requestEmailLoginCode(email) Animu Connect: email the 4-digit code
verifyEmailLoginCode({ email, code }) Verify the code → session (server refreshes linked providers first)
getSessionStatus(sessionId?) Is the token authenticated?
logout(sessionId?) Destroy the session
getProfile(sessionId?) Full profile, providers, banner, session
refreshProfile(sessionId?) Re-pull provider data + verified (cached media survives hiccups)
getEmails(sessionId?) List the account's Animu Connect emails
requestAddEmail(email, sessionId?) Email code to add/replace the extra email
verifyAddEmail(params, sessionId?) Verify + store the extra email
removeEmail(emailId, sessionId?) Remove the extra email
linkProvider(params, sessionId?) Link another provider via OAuth code
unlinkProvider(provider, sessionId?) Unlink a provider
getAvatar(sessionId?) Avatar bytes
uploadAvatar({ avatar, filename? }, sessionId?) Upload a custom avatar
resetAvatar(sessionId?) Reset to the provider avatar
getBanner(sessionId?) Banner bytes
deleteAccount(sessionId?) Permanently delete the account
browserLoginUrl(provider?) Browser login deep-link
mobileStartUrl(provider, sessionId?) Server-side mobile auth start URL (Discord/Google/Apple; login, or link with a token)
completeMobileAuth(callbackUrl) Parse the deep link + adopt the token
legacyExchangeToken(params) Legacy /mobile OAuth exchange (discord_data)
legacySessionStatus(sessionId) Legacy Discord session check
legacySessionLogout(sessionId) Legacy Discord session logout

Full endpoint reference with request/response schemas and business rules: API.md.

Development

pnpm install
pnpm run typecheck
pnpm test
pnpm run build

License

MIT

Quality gates

See CI and quality policy for local checks, GitHub Actions, Jenkins, SonarQube, coverage floors, and the native test scope.

About

TypeScript API client for Rádio Animu — now playing, programs, history, music requests, live requests, streams and auth. Zero runtime dependencies.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages