auralis-sdk 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/client.ts","../src/errors.ts","../src/types.ts","../src/player.ts","../src/rest.ts"],"sourcesContent":["/**\n * auralis-sdk — the official Node.js SDK for the Auralis music platform.\n *\n * The queue, skip, loop, and shuffle all live in this library — the backend only\n * plays what it is told. See {@link Player} for the full command surface.\n *\n * ```ts\n * import { Client, LoopMode } from \"auralis-sdk\";\n *\n * const client = new Client(\"http://localhost:8080\", \"my-token\");\n * await client.start();\n * client.on(\"trackStart\", (player) => console.log(\"now playing:\", player.current?.title));\n *\n * const player = client.getPlayer(guildId);\n * // ... wire player.handleVoiceServerUpdate / handleVoiceStateUpdate from your bot ...\n * await player.play(\"ytsearch:never gonna give you up\");\n * ```\n */\n\nexport { Client } from \"./client.js\";\nexport type { ClientOptions, ClientEvents } from \"./client.js\";\nexport { Player } from \"./player.js\";\nexport type { PlayerHost, AutoplayHook } from \"./player.js\";\nexport { REST } from \"./rest.js\";\nexport {\n LoopMode,\n LoadType,\n EndReason,\n endReasonAdvances,\n} from \"./types.js\";\nexport type {\n Track,\n LoadResult,\n PlayerState,\n VoiceServerUpdate,\n VoiceStateUpdate,\n} from \"./types.js\";\nexport {\n AuralisError,\n AuthError,\n RateLimited,\n APIError,\n NotConnected,\n LoadError,\n} from \"./errors.js\";\n","/** The top-level client: owns the REST wrapper, the WebSocket event loop, the\n * per-guild players, and event dispatch (via Node's EventEmitter). */\n\nimport { EventEmitter } from \"node:events\";\nimport WebSocket from \"ws\";\n\nimport { Player, PlayerHost } from \"./player.js\";\nimport { REST } from \"./rest.js\";\nimport { LoadResult, Track, endReasonAdvances, playerStateFromJson } from \"./types.js\";\n\nexport interface ClientOptions {\n reconnect?: boolean;\n}\n\n/** Strongly-typed event map for `client.on(...)` / `client.emit(...)`. */\nexport interface ClientEvents {\n ready: [];\n /** Fired instead of `ready` when a reconnect re-establishes the session. */\n resumed: [];\n disconnect: [];\n error: [err: unknown];\n trackStart: [player: Player | number, data: any];\n trackEnd: [player: Player | number, data: any];\n trackException: [player: Player | number, data: any];\n trackStuck: [player: Player | number, data: any];\n playerUpdate: [player: Player | number, data: any];\n voiceClosed: [player: Player | number, data: any];\n // STATS is node-wide, so there may be no player/guild attached.\n stats: [player: Player | number | null, data: any];\n queueEnd: [player: Player];\n}\n\n// WS op -> the event name we emit to listeners.\nconst EVENT_OPS: Record<string, keyof ClientEvents> = {\n TRACK_START: \"trackStart\",\n TRACK_END: \"trackEnd\",\n TRACK_EXCEPTION: \"trackException\",\n TRACK_STUCK: \"trackStuck\",\n PLAYER_UPDATE: \"playerUpdate\",\n VOICE_CLOSED: \"voiceClosed\",\n STATS: \"stats\",\n};\n\n// Declaration-merge typed on/once/off/emit overloads onto the class.\nexport interface Client {\n on<K extends keyof ClientEvents>(event: K, listener: (...args: ClientEvents[K]) => void): this;\n once<K extends keyof ClientEvents>(event: K, listener: (...args: ClientEvents[K]) => void): this;\n off<K extends keyof ClientEvents>(event: K, listener: (...args: ClientEvents[K]) => void): this;\n emit<K extends keyof ClientEvents>(event: K, ...args: ClientEvents[K]): boolean;\n}\n\nexport class Client extends EventEmitter implements PlayerHost {\n readonly rest: REST;\n private baseUrl: string;\n private token: string;\n private reconnect: boolean;\n\n private playersMap = new Map<number, Player>();\n private ws: WebSocket | null = null;\n private heartbeatTimer: ReturnType<typeof setInterval> | null = null;\n private reconnectTimer: ReturnType<typeof setTimeout> | null = null;\n private sessionId: string | null = null;\n private closed = false;\n private backoff = 1000;\n private hasConnected = false;\n private heartbeatSentAt = 0;\n\n /** Last measured heartbeat round-trip latency in ms (-1 until first ack). */\n latencyMs = -1;\n /** Most recent STATS frame payload from the node, or null. */\n lastStats: Record<string, unknown> | null = null;\n\n constructor(baseUrl: string, token: string, opts: ClientOptions = {}) {\n super();\n this.baseUrl = baseUrl.replace(/\\/$/, \"\");\n this.token = token;\n this.reconnect = opts.reconnect ?? true;\n this.rest = new REST(this.baseUrl, token);\n }\n\n // --- lifecycle --------------------------------------------------------\n async start(): Promise<void> {\n this.closed = false;\n this.openSocket();\n }\n\n async close(): Promise<void> {\n this.closed = true;\n if (this.heartbeatTimer) {\n clearInterval(this.heartbeatTimer);\n this.heartbeatTimer = null;\n }\n if (this.reconnectTimer) {\n clearTimeout(this.reconnectTimer);\n this.reconnectTimer = null;\n }\n // Drop our listeners so a lingering socket can't fire callbacks post-close.\n this.ws?.removeAllListeners();\n this.ws?.close();\n this.ws = null;\n }\n\n /** Emit \"error\" without the Node EventEmitter footgun: an unlistened \"error\"\n * throws and would crash the host process, so fall back to a console warning. */\n private safeError(err: unknown): void {\n if (this.listenerCount(\"error\") > 0) this.emit(\"error\", err);\n else console.warn(\"[auralis] unhandled client error:\", err);\n }\n\n // --- players ----------------------------------------------------------\n getPlayer(guildId: number): Player {\n let p = this.playersMap.get(guildId);\n if (!p) {\n p = new Player(this, guildId);\n this.playersMap.set(guildId, p);\n }\n return p;\n }\n\n players(): Player[] {\n return [...this.playersMap.values()];\n }\n\n dropPlayer(guildId: number): void {\n this.playersMap.delete(guildId);\n }\n\n // --- metadata convenience --------------------------------------------\n loadTracks(identifier: string): Promise<LoadResult> {\n return this.rest.loadTracks(identifier);\n }\n\n /** `ytsearch:`-prefix a bare query and return the hits (empty on miss). */\n async search(query: string): Promise<Track[]> {\n const first = query.split(\" \", 1)[0];\n const ident = first.includes(\":\") ? query : `ytsearch:${query}`;\n return (await this.loadTracks(ident)).tracks;\n }\n\n // --- websocket --------------------------------------------------------\n private get wsUrl(): string {\n return this.baseUrl.replace(/^http/, \"ws\") + \"/v1/ws\";\n }\n\n private openSocket(): void {\n if (this.closed) return;\n const ws = new WebSocket(this.wsUrl, {\n headers: { Authorization: `Bearer ${this.token}` },\n });\n this.ws = ws;\n\n ws.on(\"open\", () => {\n this.backoff = 1000;\n this.subscribeAll();\n });\n ws.on(\"message\", (raw) => {\n let frame: any;\n try {\n frame = JSON.parse(raw.toString());\n } catch {\n return;\n }\n // handleFrame is async; a rejected promise here would be an unhandled\n // rejection (process-fatal on newer Node). Route failures to safeError.\n this.handleFrame(frame).catch((e) => this.safeError(e));\n });\n ws.on(\"error\", (err) => this.safeError(err));\n ws.on(\"close\", () => {\n if (this.heartbeatTimer) {\n clearInterval(this.heartbeatTimer);\n this.heartbeatTimer = null;\n }\n this.ws = null;\n if (this.closed || !this.reconnect) return;\n this.emit(\"disconnect\");\n // Backoff with ±25% jitter so many clients don't reconnect in lockstep.\n const jitter = this.backoff * (0.75 + Math.random() * 0.5);\n this.reconnectTimer = setTimeout(() => this.openSocket(), jitter);\n this.backoff = Math.min(this.backoff * 2, 30000);\n });\n }\n\n /** After a reconnect, pull each player's authoritative state from the node so\n * position/volume/paused are accurate again. Per-player failures are ignored. */\n private async resyncPlayers(): Promise<void> {\n await Promise.all(\n this.players().map(async (p) => {\n try {\n p.state = await this.rest.getState(p.guildId);\n } catch {\n // node may not have this player (it was dropped during the gap) — skip\n }\n }),\n );\n }\n\n private subscribeAll(): void {\n if (this.ws?.readyState === WebSocket.OPEN && this.playersMap.size > 0) {\n this.send({\n op: \"SUBSCRIBE\",\n d: { guilds: [...this.playersMap.keys()].map(String) },\n });\n }\n }\n\n private send(payload: unknown): void {\n if (this.ws?.readyState === WebSocket.OPEN) this.ws.send(JSON.stringify(payload));\n }\n\n private async handleFrame(frame: any): Promise<void> {\n const op: string = frame.op;\n const d = frame.d ?? {};\n\n if (op === \"HELLO\") {\n const interval = (d.heartbeat_interval_ms ?? 30000) as number;\n if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);\n let seq = 0;\n this.heartbeatTimer = setInterval(() => {\n this.heartbeatSentAt = Date.now();\n this.send({ op: \"HEARTBEAT\", d: { seq: ++seq } });\n }, interval);\n return;\n }\n if (op === \"READY\") {\n const resumed = this.hasConnected;\n this.sessionId = d.session_id ?? null;\n this.hasConnected = true;\n this.subscribeAll();\n if (resumed) {\n // Reconnect: resync each player's live state from the node so callers\n // see accurate position/volume/paused after the gap (a practical resume).\n await this.resyncPlayers();\n this.emit(\"resumed\");\n } else {\n this.emit(\"ready\");\n }\n return;\n }\n if (op === \"HEARTBEAT_ACK\") {\n if (this.heartbeatSentAt) this.latencyMs = Date.now() - this.heartbeatSentAt;\n return;\n }\n if (op === \"STATS\") {\n this.lastStats = d;\n }\n\n const guildId = d.guild_id ? Number(d.guild_id) : null;\n const player = guildId !== null ? this.playersMap.get(guildId) : undefined;\n\n if (op === \"TRACK_END\" && player) {\n // Drive auto-advance BEFORE notifying the user so handlers see new state.\n await player.onTrackEnd(endReasonAdvances(d.reason ?? \"FINISHED\"));\n } else if (op === \"PLAYER_UPDATE\" && player) {\n player.state = playerStateFromJson(d.state ?? {});\n }\n\n const eventName = EVENT_OPS[op];\n if (eventName) this.emit(eventName, player ?? guildId, d);\n }\n}\n","/** Exception hierarchy. Every network/protocol failure is an `AuralisError`. */\n\nexport class AuralisError extends Error {}\n\n/** 401/403 — the bearer token is missing, wrong, or not allowed. */\nexport class AuthError extends AuralisError {}\n\n/** 429 — too many requests. `retryAfter` is seconds if the server gave one. */\nexport class RateLimited extends AuralisError {\n constructor(message: string, public retryAfter?: number) {\n super(message);\n }\n}\n\n/** Any other non-2xx HTTP response. */\nexport class APIError extends AuralisError {\n constructor(public status: number, message: string) {\n super(`HTTP ${status}: ${message}`);\n }\n}\n\n/** Tried to play before the guild's voice connection was established. */\nexport class NotConnected extends AuralisError {}\n\n/** `loadtracks` returned `error` or nothing usable. */\nexport class LoadError extends AuralisError {}\n","/** Wire vocabulary and data models — mirror the control-plane JSON exactly. */\n\n/** How the queue advances when a track finishes. */\nexport enum LoopMode {\n Off = \"off\",\n Track = \"track\", // replay the current track forever\n Queue = \"queue\", // loop the whole queue\n}\n\n/** `load_type` returned by `GET /v1/loadtracks`. */\nexport enum LoadType {\n Track = \"track\",\n Playlist = \"playlist\",\n Search = \"search\",\n Empty = \"empty\",\n Error = \"error\",\n}\n\n/** Why a track ended. Only FINISHED / LOAD_FAILED advance the queue. */\nexport enum EndReason {\n Finished = \"FINISHED\",\n LoadFailed = \"LOAD_FAILED\",\n Stopped = \"STOPPED\",\n Replaced = \"REPLACED\",\n Cleanup = \"CLEANUP\",\n}\n\nexport function endReasonAdvances(reason: string): boolean {\n return reason === EndReason.Finished || reason === EndReason.LoadFailed;\n}\n\n/** A playable track. `encoded` is the opaque token the backend needs; the rest\n * is display metadata the bot keeps for now-playing embeds. */\nexport interface Track {\n encoded: string;\n identifier: string;\n title: string;\n author: string;\n lengthMs: number;\n isStream: boolean;\n uri?: string;\n sourceName: string;\n artworkUrl?: string;\n isrc?: string;\n /** Free-form, set by the bot (e.g. who requested it). Never sent upstream. */\n extras: Record<string, unknown>;\n}\n\nexport function trackFromJson(d: any): Track {\n return {\n encoded: d.encoded,\n identifier: d.identifier ?? \"\",\n title: d.title ?? \"\",\n author: d.author ?? \"\",\n lengthMs: d.length_ms ?? 0,\n isStream: d.is_stream ?? false,\n uri: d.uri ?? undefined,\n sourceName: d.source_name ?? \"\",\n artworkUrl: d.artwork_url ?? undefined,\n isrc: d.isrc ?? undefined,\n extras: {},\n };\n}\n\nexport interface LoadResult {\n loadType: LoadType;\n tracks: Track[];\n playlistName?: string;\n error?: string;\n}\n\nexport function loadResultFromJson(d: any): LoadResult {\n return {\n loadType: (d.load_type ?? \"empty\") as LoadType,\n tracks: (d.tracks ?? []).map(trackFromJson),\n playlistName: d.playlist_name ?? undefined,\n error: d.error ?? undefined,\n };\n}\n\nexport interface PlayerState {\n connected: boolean;\n playing: boolean;\n paused: boolean;\n positionMs: number;\n volume: number;\n track?: Track;\n}\n\nexport function playerStateFromJson(d: any): PlayerState {\n return {\n connected: d.connected ?? false,\n playing: d.playing ?? false,\n paused: d.paused ?? false,\n positionMs: d.position_ms ?? 0,\n volume: d.volume ?? 100,\n track: d.track ? trackFromJson(d.track) : undefined,\n };\n}\n\n/** Raw Discord voice payloads the bot forwards from its gateway. */\nexport interface VoiceServerUpdate {\n token: string;\n endpoint: string;\n}\nexport interface VoiceStateUpdate {\n session_id: string;\n channel_id: string | number | null;\n user_id: string | number;\n}\n","/** Per-guild player: the client-side queue brain.\n *\n * The backend is deliberately queue-agnostic — it only knows play / stop / pause\n * / seek / volume. Everything queue-shaped (skip, loop, shuffle, auto-advance,\n * autoplay) lives here, driven by the `TRACK_END` event.\n *\n * Advance rules (the one path everything funnels through):\n * - `play()` issues a REST *replace* → the old track's `TRACK_END(REPLACED)` is\n * ignored by the client, so we never double-advance.\n * - a natural finish → `TRACK_END(FINISHED|LOAD_FAILED)` → `advance()`.\n * - `skip()` forces `advance()` (ignoring `LoopMode.Track`).\n */\n\nimport { LoadError, NotConnected } from \"./errors.js\";\nimport { REST } from \"./rest.js\";\nimport {\n LoadType,\n LoopMode,\n PlayerState,\n Track,\n VoiceServerUpdate,\n VoiceStateUpdate,\n} from \"./types.js\";\n\n/** What a Player needs from its owner. The `Client` implements this. */\nexport interface PlayerHost {\n rest: REST;\n emit(event: string, ...args: unknown[]): void;\n dropPlayer(guildId: number): void;\n}\n\nexport type AutoplayHook = (player: Player) => Promise<Track | null>;\n\nexport class Player {\n queue: Track[] = [];\n history: Track[] = [];\n current: Track | null = null;\n /** Cap on retained history so a long-lived player can't leak memory. Queue-loop\n * still works; it just restores the most recent `historyLimit` tracks. */\n historyLimit = 200;\n loop: LoopMode = LoopMode.Off;\n volume = 100;\n paused = false;\n state: PlayerState = {\n connected: false,\n playing: false,\n paused: false,\n positionMs: 0,\n volume: 100,\n };\n /** Optional async hook used when the queue and loop are exhausted. */\n autoplay: AutoplayHook | null = null;\n\n private created = false;\n private voiceServer: { token: string; endpoint: string } | null = null;\n private voiceState: { session_id: string; channel_id: number; user_id: number } | null = null;\n\n constructor(private host: PlayerHost, public guildId: number) {}\n\n // --- voice plumbing ---------------------------------------------------\n handleVoiceServerUpdate(data: VoiceServerUpdate): void {\n this.voiceServer = { token: data.token, endpoint: data.endpoint };\n void this.maybeStart();\n }\n\n handleVoiceStateUpdate(data: VoiceStateUpdate): void {\n if (data.channel_id === null || data.channel_id === undefined) {\n this.voiceState = null;\n return;\n }\n this.voiceState = {\n session_id: data.session_id,\n channel_id: Number(data.channel_id),\n user_id: Number(data.user_id),\n };\n void this.maybeStart();\n }\n\n get connected(): boolean {\n return this.voiceServer !== null && this.voiceState !== null;\n }\n\n private voicePayload(): Record<string, unknown> {\n return { ...this.voiceServer!, ...this.voiceState!, guild_id: this.guildId };\n }\n\n private async maybeStart(): Promise<void> {\n if (this.connected && this.current === null && this.queue.length > 0) {\n await this.advance();\n }\n }\n\n // --- public commands --------------------------------------------------\n async play(query: string | Track, requester?: number): Promise<Track[]> {\n const tracks = await this.resolve(query);\n for (const t of tracks) {\n if (requester !== undefined) t.extras.requester = requester;\n this.queue.push(t);\n }\n if (this.current === null && this.connected) await this.advance();\n return tracks;\n }\n\n async add(query: string | Track, requester?: number): Promise<Track[]> {\n const tracks = await this.resolve(query);\n for (const t of tracks) {\n if (requester !== undefined) t.extras.requester = requester;\n this.queue.push(t);\n }\n return tracks;\n }\n\n async skip(): Promise<void> {\n await this.advance(true);\n }\n\n async stop(): Promise<void> {\n this.queue = [];\n this.current = null;\n if (this.created) await this.host.rest.stop(this.guildId);\n }\n\n async pause(): Promise<void> {\n await this.setPausedInternal(true);\n }\n\n async resume(): Promise<void> {\n await this.setPausedInternal(false);\n }\n\n async setPause(paused: boolean): Promise<void> {\n await this.setPausedInternal(paused);\n }\n\n async seek(positionMs: number): Promise<void> {\n if (this.created) await this.host.rest.seek(this.guildId, positionMs);\n }\n\n async setVolume(volume: number): Promise<void> {\n this.volume = Math.max(0, Math.min(1000, volume));\n if (this.created) await this.host.rest.setVolume(this.guildId, this.volume);\n }\n\n setLoop(mode: LoopMode): void {\n this.loop = mode;\n }\n\n shuffle(): void {\n for (let i = this.queue.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1));\n [this.queue[i], this.queue[j]] = [this.queue[j], this.queue[i]];\n }\n }\n\n /** Apply a raw ffmpeg `-af` filtergraph (e.g. `\"bass=g=8\"`); `null` clears. */\n async setFilters(audioGraph: string | null): Promise<void> {\n if (this.created) await this.host.rest.setFilters(this.guildId, audioGraph);\n }\n\n remove(index: number): Track | undefined {\n return this.queue.splice(index, 1)[0];\n }\n\n clear(): void {\n this.queue = [];\n }\n\n /** Reorder the queue: move the track at `from` to `to`. No-op on bad indices. */\n move(from: number, to: number): void {\n if (from < 0 || from >= this.queue.length || to < 0 || to >= this.queue.length) return;\n const [t] = this.queue.splice(from, 1);\n this.queue.splice(to, 0, t);\n }\n\n /** Resolve `query` and insert it at `index` (clamped) instead of appending. */\n async insert(index: number, query: string | Track, requester?: number): Promise<Track[]> {\n const tracks = await this.resolve(query);\n for (const t of tracks) if (requester !== undefined) t.extras.requester = requester;\n const at = Math.max(0, Math.min(index, this.queue.length));\n this.queue.splice(at, 0, ...tracks);\n if (this.current === null && this.connected) await this.advance();\n return tracks;\n }\n\n /** Batch-enqueue several queries/tracks in order. Failed resolves are skipped. */\n async addMany(queries: (string | Track)[], requester?: number): Promise<Track[]> {\n const added: Track[] = [];\n for (const q of queries) {\n try {\n const tracks = await this.resolve(q);\n for (const t of tracks) {\n if (requester !== undefined) t.extras.requester = requester;\n this.queue.push(t);\n added.push(t);\n }\n } catch {\n // skip a bad query rather than aborting the whole batch\n }\n }\n if (this.current === null && this.connected && added.length > 0) await this.advance();\n return added;\n }\n\n /** Skip forward to queue position `index`, discarding the ones in between. */\n async jump(index: number): Promise<void> {\n if (index < 0 || index >= this.queue.length) return;\n const skipped = this.queue.splice(0, index);\n for (const t of skipped) this.pushHistory(t);\n await this.advance(true);\n }\n\n /** Replay the previous track (from history), pushing the current one back on. */\n async previous(): Promise<void> {\n const prev = this.history.pop();\n if (!prev) return;\n if (this.current !== null) this.queue.unshift(this.current);\n this.queue.unshift(prev);\n await this.advance(true);\n }\n\n /** Convenience EQ: bass boost in dB (0 clears). Built on {@link setFilters}. */\n async setBassBoost(gainDb: number): Promise<void> {\n await this.setFilters(gainDb === 0 ? null : `bass=g=${gainDb}`);\n }\n\n /** Convenience speed/pitch (nightcore/vaporwave): `rate` 1.0 = normal. */\n async setSpeed(rate: number): Promise<void> {\n const r = Math.max(0.5, Math.min(2.0, rate));\n await this.setFilters(r === 1 ? null : `atempo=${r},asetrate=48000*${r}`);\n }\n\n nowPlaying(): Track | null {\n return this.current;\n }\n\n async disconnect(): Promise<void> {\n this.queue = [];\n this.current = null;\n if (this.created) await this.host.rest.destroyPlayer(this.guildId);\n this.created = false;\n this.voiceServer = null;\n this.voiceState = null;\n this.host.dropPlayer(this.guildId);\n }\n\n // --- internals --------------------------------------------------------\n /** Append to history, evicting the oldest past `historyLimit` (leak guard). */\n private pushHistory(track: Track): void {\n this.history.push(track);\n if (this.history.length > this.historyLimit) {\n this.history.splice(0, this.history.length - this.historyLimit);\n }\n }\n\n private async resolve(query: string | Track): Promise<Track[]> {\n if (typeof query !== \"string\") return [query];\n const result = await this.host.rest.loadTracks(query);\n if (result.error || result.tracks.length === 0) {\n throw new LoadError(result.error ?? `no tracks for ${query}`);\n }\n return result.loadType === LoadType.Playlist ? result.tracks : [result.tracks[0]];\n }\n\n private async nextTrack(forced: boolean): Promise<Track | null> {\n if (this.loop === LoopMode.Track && this.current !== null && !forced) {\n return this.current;\n }\n if (this.queue.length > 0) {\n if (this.current !== null) this.pushHistory(this.current);\n return this.queue.shift()!;\n }\n if (this.loop === LoopMode.Queue && this.history.length > 0) {\n this.queue = [...this.history, ...(this.current ? [this.current] : [])];\n this.history = [];\n return this.queue.shift() ?? null;\n }\n if (this.autoplay !== null) return this.autoplay(this);\n return null;\n }\n\n private async advance(forced = false): Promise<void> {\n const next = await this.nextTrack(forced);\n if (next === null) {\n this.current = null;\n if (this.created) await this.host.rest.stop(this.guildId);\n this.host.emit(\"queueEnd\", this);\n return;\n }\n await this.playTrack(next);\n }\n\n private async playTrack(track: Track): Promise<void> {\n if (!this.connected) throw new NotConnected(`guild ${this.guildId} has no voice connection`);\n this.current = track;\n if (!this.created) {\n await this.host.rest.createPlayer(this.guildId, this.voicePayload(), {\n track: track.encoded,\n volume: this.volume,\n paused: this.paused,\n });\n this.created = true;\n } else {\n await this.host.rest.play(this.guildId, track.encoded);\n }\n }\n\n private async setPausedInternal(paused: boolean): Promise<void> {\n this.paused = paused;\n if (this.created) await this.host.rest.setPaused(this.guildId, paused);\n }\n\n /** Called by the client's WS loop when a TRACK_END for this guild arrives. */\n async onTrackEnd(advances: boolean): Promise<void> {\n if (advances) await this.advance();\n }\n}\n","/** Thin wrapper over the control-plane HTTP API using the global `fetch`\n * (Node >= 18). Knows nothing about queues — just request/response. */\n\nimport { APIError, AuthError, RateLimited } from \"./errors.js\";\nimport {\n LoadResult,\n PlayerState,\n loadResultFromJson,\n playerStateFromJson,\n} from \"./types.js\";\n\nexport class REST {\n private base: string;\n private headers: Record<string, string>;\n\n constructor(baseUrl: string, token: string) {\n this.base = baseUrl.replace(/\\/$/, \"\");\n this.headers = {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n };\n }\n\n private async request(method: string, path: string, body?: unknown): Promise<any> {\n const res = await fetch(`${this.base}${path}`, {\n method,\n headers: this.headers,\n body: body === undefined ? undefined : JSON.stringify(body),\n });\n if (res.status === 401 || res.status === 403) throw new AuthError(await res.text());\n if (res.status === 429) {\n const retry = res.headers.get(\"Retry-After\");\n throw new RateLimited(await res.text(), retry ? Number(retry) : undefined);\n }\n if (res.status >= 400) throw new APIError(res.status, await res.text());\n const ct = res.headers.get(\"Content-Type\") ?? \"\";\n return ct.includes(\"application/json\") ? res.json() : res.text();\n }\n\n // --- metadata ---------------------------------------------------------\n async loadTracks(identifier: string): Promise<LoadResult> {\n const q = new URLSearchParams({ identifier });\n return loadResultFromJson(await this.request(\"GET\", `/v1/loadtracks?${q}`));\n }\n\n async prewarm(encoded: string[], guildId?: number): Promise<any> {\n const body: any = { tracks: encoded.map((e) => ({ encoded: e })) };\n if (guildId !== undefined) body.guild_id = guildId;\n return this.request(\"POST\", \"/v1/tracks/prewarm\", body);\n }\n\n // --- player lifecycle -------------------------------------------------\n async createPlayer(\n guildId: number,\n voice: Record<string, unknown>,\n opts: { track?: string; volume?: number; paused?: boolean; audioGraph?: string } = {},\n ): Promise<any> {\n const body: any = {\n voice,\n volume: opts.volume ?? 100,\n paused: opts.paused ?? false,\n };\n if (opts.track) body.track = { encoded: opts.track };\n if (opts.audioGraph !== undefined) body.filters = { audio_graph: opts.audioGraph };\n return this.request(\"POST\", `/v1/guilds/${guildId}/players`, body);\n }\n\n async destroyPlayer(guildId: number): Promise<any> {\n return this.request(\"DELETE\", `/v1/guilds/${guildId}/players`);\n }\n\n async getState(guildId: number): Promise<PlayerState> {\n return playerStateFromJson(await this.request(\"GET\", `/v1/guilds/${guildId}/player`));\n }\n\n // --- playback control -------------------------------------------------\n async play(guildId: number, track: string, audioGraph?: string): Promise<any> {\n const body: any = { track: { encoded: track } };\n if (audioGraph !== undefined) body.filters = { audio_graph: audioGraph };\n return this.request(\"POST\", `/v1/guilds/${guildId}/player/play`, body);\n }\n\n async stop(guildId: number): Promise<any> {\n return this.request(\"POST\", `/v1/guilds/${guildId}/player/stop`, {});\n }\n\n async setPaused(guildId: number, paused: boolean): Promise<any> {\n return this.request(\"POST\", `/v1/guilds/${guildId}/player/pause`, { paused });\n }\n\n async setVolume(guildId: number, volume: number): Promise<any> {\n return this.request(\"POST\", `/v1/guilds/${guildId}/player/volume`, { volume });\n }\n\n async seek(guildId: number, positionMs: number): Promise<any> {\n return this.request(\"POST\", `/v1/guilds/${guildId}/player/seek`, {\n position_ms: positionMs,\n });\n }\n\n async setFilters(guildId: number, audioGraph: string | null): Promise<any> {\n return this.request(\"POST\", `/v1/guilds/${guildId}/player/filters`, {\n filters: { audio_graph: audioGraph },\n });\n }\n}\n"],"mappings":"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","names":["LoopMode","LoadType","EndReason","WebSocket"]}
@@ -0,0 +1,271 @@
1
+ import { EventEmitter } from 'node:events';
2
+
3
+ /** Wire vocabulary and data models — mirror the control-plane JSON exactly. */
4
+ /** How the queue advances when a track finishes. */
5
+ declare enum LoopMode {
6
+ Off = "off",
7
+ Track = "track",// replay the current track forever
8
+ Queue = "queue"
9
+ }
10
+ /** `load_type` returned by `GET /v1/loadtracks`. */
11
+ declare enum LoadType {
12
+ Track = "track",
13
+ Playlist = "playlist",
14
+ Search = "search",
15
+ Empty = "empty",
16
+ Error = "error"
17
+ }
18
+ /** Why a track ended. Only FINISHED / LOAD_FAILED advance the queue. */
19
+ declare enum EndReason {
20
+ Finished = "FINISHED",
21
+ LoadFailed = "LOAD_FAILED",
22
+ Stopped = "STOPPED",
23
+ Replaced = "REPLACED",
24
+ Cleanup = "CLEANUP"
25
+ }
26
+ declare function endReasonAdvances(reason: string): boolean;
27
+ /** A playable track. `encoded` is the opaque token the backend needs; the rest
28
+ * is display metadata the bot keeps for now-playing embeds. */
29
+ interface Track {
30
+ encoded: string;
31
+ identifier: string;
32
+ title: string;
33
+ author: string;
34
+ lengthMs: number;
35
+ isStream: boolean;
36
+ uri?: string;
37
+ sourceName: string;
38
+ artworkUrl?: string;
39
+ isrc?: string;
40
+ /** Free-form, set by the bot (e.g. who requested it). Never sent upstream. */
41
+ extras: Record<string, unknown>;
42
+ }
43
+ interface LoadResult {
44
+ loadType: LoadType;
45
+ tracks: Track[];
46
+ playlistName?: string;
47
+ error?: string;
48
+ }
49
+ interface PlayerState {
50
+ connected: boolean;
51
+ playing: boolean;
52
+ paused: boolean;
53
+ positionMs: number;
54
+ volume: number;
55
+ track?: Track;
56
+ }
57
+ /** Raw Discord voice payloads the bot forwards from its gateway. */
58
+ interface VoiceServerUpdate {
59
+ token: string;
60
+ endpoint: string;
61
+ }
62
+ interface VoiceStateUpdate {
63
+ session_id: string;
64
+ channel_id: string | number | null;
65
+ user_id: string | number;
66
+ }
67
+
68
+ /** Thin wrapper over the control-plane HTTP API using the global `fetch`
69
+ * (Node >= 18). Knows nothing about queues — just request/response. */
70
+
71
+ declare class REST {
72
+ private base;
73
+ private headers;
74
+ constructor(baseUrl: string, token: string);
75
+ private request;
76
+ loadTracks(identifier: string): Promise<LoadResult>;
77
+ prewarm(encoded: string[], guildId?: number): Promise<any>;
78
+ createPlayer(guildId: number, voice: Record<string, unknown>, opts?: {
79
+ track?: string;
80
+ volume?: number;
81
+ paused?: boolean;
82
+ audioGraph?: string;
83
+ }): Promise<any>;
84
+ destroyPlayer(guildId: number): Promise<any>;
85
+ getState(guildId: number): Promise<PlayerState>;
86
+ play(guildId: number, track: string, audioGraph?: string): Promise<any>;
87
+ stop(guildId: number): Promise<any>;
88
+ setPaused(guildId: number, paused: boolean): Promise<any>;
89
+ setVolume(guildId: number, volume: number): Promise<any>;
90
+ seek(guildId: number, positionMs: number): Promise<any>;
91
+ setFilters(guildId: number, audioGraph: string | null): Promise<any>;
92
+ }
93
+
94
+ /** Per-guild player: the client-side queue brain.
95
+ *
96
+ * The backend is deliberately queue-agnostic — it only knows play / stop / pause
97
+ * / seek / volume. Everything queue-shaped (skip, loop, shuffle, auto-advance,
98
+ * autoplay) lives here, driven by the `TRACK_END` event.
99
+ *
100
+ * Advance rules (the one path everything funnels through):
101
+ * - `play()` issues a REST *replace* → the old track's `TRACK_END(REPLACED)` is
102
+ * ignored by the client, so we never double-advance.
103
+ * - a natural finish → `TRACK_END(FINISHED|LOAD_FAILED)` → `advance()`.
104
+ * - `skip()` forces `advance()` (ignoring `LoopMode.Track`).
105
+ */
106
+
107
+ /** What a Player needs from its owner. The `Client` implements this. */
108
+ interface PlayerHost {
109
+ rest: REST;
110
+ emit(event: string, ...args: unknown[]): void;
111
+ dropPlayer(guildId: number): void;
112
+ }
113
+ type AutoplayHook = (player: Player) => Promise<Track | null>;
114
+ declare class Player {
115
+ private host;
116
+ guildId: number;
117
+ queue: Track[];
118
+ history: Track[];
119
+ current: Track | null;
120
+ /** Cap on retained history so a long-lived player can't leak memory. Queue-loop
121
+ * still works; it just restores the most recent `historyLimit` tracks. */
122
+ historyLimit: number;
123
+ loop: LoopMode;
124
+ volume: number;
125
+ paused: boolean;
126
+ state: PlayerState;
127
+ /** Optional async hook used when the queue and loop are exhausted. */
128
+ autoplay: AutoplayHook | null;
129
+ private created;
130
+ private voiceServer;
131
+ private voiceState;
132
+ constructor(host: PlayerHost, guildId: number);
133
+ handleVoiceServerUpdate(data: VoiceServerUpdate): void;
134
+ handleVoiceStateUpdate(data: VoiceStateUpdate): void;
135
+ get connected(): boolean;
136
+ private voicePayload;
137
+ private maybeStart;
138
+ play(query: string | Track, requester?: number): Promise<Track[]>;
139
+ add(query: string | Track, requester?: number): Promise<Track[]>;
140
+ skip(): Promise<void>;
141
+ stop(): Promise<void>;
142
+ pause(): Promise<void>;
143
+ resume(): Promise<void>;
144
+ setPause(paused: boolean): Promise<void>;
145
+ seek(positionMs: number): Promise<void>;
146
+ setVolume(volume: number): Promise<void>;
147
+ setLoop(mode: LoopMode): void;
148
+ shuffle(): void;
149
+ /** Apply a raw ffmpeg `-af` filtergraph (e.g. `"bass=g=8"`); `null` clears. */
150
+ setFilters(audioGraph: string | null): Promise<void>;
151
+ remove(index: number): Track | undefined;
152
+ clear(): void;
153
+ /** Reorder the queue: move the track at `from` to `to`. No-op on bad indices. */
154
+ move(from: number, to: number): void;
155
+ /** Resolve `query` and insert it at `index` (clamped) instead of appending. */
156
+ insert(index: number, query: string | Track, requester?: number): Promise<Track[]>;
157
+ /** Batch-enqueue several queries/tracks in order. Failed resolves are skipped. */
158
+ addMany(queries: (string | Track)[], requester?: number): Promise<Track[]>;
159
+ /** Skip forward to queue position `index`, discarding the ones in between. */
160
+ jump(index: number): Promise<void>;
161
+ /** Replay the previous track (from history), pushing the current one back on. */
162
+ previous(): Promise<void>;
163
+ /** Convenience EQ: bass boost in dB (0 clears). Built on {@link setFilters}. */
164
+ setBassBoost(gainDb: number): Promise<void>;
165
+ /** Convenience speed/pitch (nightcore/vaporwave): `rate` 1.0 = normal. */
166
+ setSpeed(rate: number): Promise<void>;
167
+ nowPlaying(): Track | null;
168
+ disconnect(): Promise<void>;
169
+ /** Append to history, evicting the oldest past `historyLimit` (leak guard). */
170
+ private pushHistory;
171
+ private resolve;
172
+ private nextTrack;
173
+ private advance;
174
+ private playTrack;
175
+ private setPausedInternal;
176
+ /** Called by the client's WS loop when a TRACK_END for this guild arrives. */
177
+ onTrackEnd(advances: boolean): Promise<void>;
178
+ }
179
+
180
+ /** The top-level client: owns the REST wrapper, the WebSocket event loop, the
181
+ * per-guild players, and event dispatch (via Node's EventEmitter). */
182
+
183
+ interface ClientOptions {
184
+ reconnect?: boolean;
185
+ }
186
+ /** Strongly-typed event map for `client.on(...)` / `client.emit(...)`. */
187
+ interface ClientEvents {
188
+ ready: [];
189
+ /** Fired instead of `ready` when a reconnect re-establishes the session. */
190
+ resumed: [];
191
+ disconnect: [];
192
+ error: [err: unknown];
193
+ trackStart: [player: Player | number, data: any];
194
+ trackEnd: [player: Player | number, data: any];
195
+ trackException: [player: Player | number, data: any];
196
+ trackStuck: [player: Player | number, data: any];
197
+ playerUpdate: [player: Player | number, data: any];
198
+ voiceClosed: [player: Player | number, data: any];
199
+ stats: [player: Player | number | null, data: any];
200
+ queueEnd: [player: Player];
201
+ }
202
+ interface Client {
203
+ on<K extends keyof ClientEvents>(event: K, listener: (...args: ClientEvents[K]) => void): this;
204
+ once<K extends keyof ClientEvents>(event: K, listener: (...args: ClientEvents[K]) => void): this;
205
+ off<K extends keyof ClientEvents>(event: K, listener: (...args: ClientEvents[K]) => void): this;
206
+ emit<K extends keyof ClientEvents>(event: K, ...args: ClientEvents[K]): boolean;
207
+ }
208
+ declare class Client extends EventEmitter implements PlayerHost {
209
+ readonly rest: REST;
210
+ private baseUrl;
211
+ private token;
212
+ private reconnect;
213
+ private playersMap;
214
+ private ws;
215
+ private heartbeatTimer;
216
+ private reconnectTimer;
217
+ private sessionId;
218
+ private closed;
219
+ private backoff;
220
+ private hasConnected;
221
+ private heartbeatSentAt;
222
+ /** Last measured heartbeat round-trip latency in ms (-1 until first ack). */
223
+ latencyMs: number;
224
+ /** Most recent STATS frame payload from the node, or null. */
225
+ lastStats: Record<string, unknown> | null;
226
+ constructor(baseUrl: string, token: string, opts?: ClientOptions);
227
+ start(): Promise<void>;
228
+ close(): Promise<void>;
229
+ /** Emit "error" without the Node EventEmitter footgun: an unlistened "error"
230
+ * throws and would crash the host process, so fall back to a console warning. */
231
+ private safeError;
232
+ getPlayer(guildId: number): Player;
233
+ players(): Player[];
234
+ dropPlayer(guildId: number): void;
235
+ loadTracks(identifier: string): Promise<LoadResult>;
236
+ /** `ytsearch:`-prefix a bare query and return the hits (empty on miss). */
237
+ search(query: string): Promise<Track[]>;
238
+ private get wsUrl();
239
+ private openSocket;
240
+ /** After a reconnect, pull each player's authoritative state from the node so
241
+ * position/volume/paused are accurate again. Per-player failures are ignored. */
242
+ private resyncPlayers;
243
+ private subscribeAll;
244
+ private send;
245
+ private handleFrame;
246
+ }
247
+
248
+ /** Exception hierarchy. Every network/protocol failure is an `AuralisError`. */
249
+ declare class AuralisError extends Error {
250
+ }
251
+ /** 401/403 — the bearer token is missing, wrong, or not allowed. */
252
+ declare class AuthError extends AuralisError {
253
+ }
254
+ /** 429 — too many requests. `retryAfter` is seconds if the server gave one. */
255
+ declare class RateLimited extends AuralisError {
256
+ retryAfter?: number | undefined;
257
+ constructor(message: string, retryAfter?: number | undefined);
258
+ }
259
+ /** Any other non-2xx HTTP response. */
260
+ declare class APIError extends AuralisError {
261
+ status: number;
262
+ constructor(status: number, message: string);
263
+ }
264
+ /** Tried to play before the guild's voice connection was established. */
265
+ declare class NotConnected extends AuralisError {
266
+ }
267
+ /** `loadtracks` returned `error` or nothing usable. */
268
+ declare class LoadError extends AuralisError {
269
+ }
270
+
271
+ export { APIError, AuralisError, AuthError, type AutoplayHook, Client, type ClientEvents, type ClientOptions, EndReason, LoadError, type LoadResult, LoadType, LoopMode, NotConnected, Player, type PlayerHost, type PlayerState, REST, RateLimited, type Track, type VoiceServerUpdate, type VoiceStateUpdate, endReasonAdvances };
@@ -0,0 +1,271 @@
1
+ import { EventEmitter } from 'node:events';
2
+
3
+ /** Wire vocabulary and data models — mirror the control-plane JSON exactly. */
4
+ /** How the queue advances when a track finishes. */
5
+ declare enum LoopMode {
6
+ Off = "off",
7
+ Track = "track",// replay the current track forever
8
+ Queue = "queue"
9
+ }
10
+ /** `load_type` returned by `GET /v1/loadtracks`. */
11
+ declare enum LoadType {
12
+ Track = "track",
13
+ Playlist = "playlist",
14
+ Search = "search",
15
+ Empty = "empty",
16
+ Error = "error"
17
+ }
18
+ /** Why a track ended. Only FINISHED / LOAD_FAILED advance the queue. */
19
+ declare enum EndReason {
20
+ Finished = "FINISHED",
21
+ LoadFailed = "LOAD_FAILED",
22
+ Stopped = "STOPPED",
23
+ Replaced = "REPLACED",
24
+ Cleanup = "CLEANUP"
25
+ }
26
+ declare function endReasonAdvances(reason: string): boolean;
27
+ /** A playable track. `encoded` is the opaque token the backend needs; the rest
28
+ * is display metadata the bot keeps for now-playing embeds. */
29
+ interface Track {
30
+ encoded: string;
31
+ identifier: string;
32
+ title: string;
33
+ author: string;
34
+ lengthMs: number;
35
+ isStream: boolean;
36
+ uri?: string;
37
+ sourceName: string;
38
+ artworkUrl?: string;
39
+ isrc?: string;
40
+ /** Free-form, set by the bot (e.g. who requested it). Never sent upstream. */
41
+ extras: Record<string, unknown>;
42
+ }
43
+ interface LoadResult {
44
+ loadType: LoadType;
45
+ tracks: Track[];
46
+ playlistName?: string;
47
+ error?: string;
48
+ }
49
+ interface PlayerState {
50
+ connected: boolean;
51
+ playing: boolean;
52
+ paused: boolean;
53
+ positionMs: number;
54
+ volume: number;
55
+ track?: Track;
56
+ }
57
+ /** Raw Discord voice payloads the bot forwards from its gateway. */
58
+ interface VoiceServerUpdate {
59
+ token: string;
60
+ endpoint: string;
61
+ }
62
+ interface VoiceStateUpdate {
63
+ session_id: string;
64
+ channel_id: string | number | null;
65
+ user_id: string | number;
66
+ }
67
+
68
+ /** Thin wrapper over the control-plane HTTP API using the global `fetch`
69
+ * (Node >= 18). Knows nothing about queues — just request/response. */
70
+
71
+ declare class REST {
72
+ private base;
73
+ private headers;
74
+ constructor(baseUrl: string, token: string);
75
+ private request;
76
+ loadTracks(identifier: string): Promise<LoadResult>;
77
+ prewarm(encoded: string[], guildId?: number): Promise<any>;
78
+ createPlayer(guildId: number, voice: Record<string, unknown>, opts?: {
79
+ track?: string;
80
+ volume?: number;
81
+ paused?: boolean;
82
+ audioGraph?: string;
83
+ }): Promise<any>;
84
+ destroyPlayer(guildId: number): Promise<any>;
85
+ getState(guildId: number): Promise<PlayerState>;
86
+ play(guildId: number, track: string, audioGraph?: string): Promise<any>;
87
+ stop(guildId: number): Promise<any>;
88
+ setPaused(guildId: number, paused: boolean): Promise<any>;
89
+ setVolume(guildId: number, volume: number): Promise<any>;
90
+ seek(guildId: number, positionMs: number): Promise<any>;
91
+ setFilters(guildId: number, audioGraph: string | null): Promise<any>;
92
+ }
93
+
94
+ /** Per-guild player: the client-side queue brain.
95
+ *
96
+ * The backend is deliberately queue-agnostic — it only knows play / stop / pause
97
+ * / seek / volume. Everything queue-shaped (skip, loop, shuffle, auto-advance,
98
+ * autoplay) lives here, driven by the `TRACK_END` event.
99
+ *
100
+ * Advance rules (the one path everything funnels through):
101
+ * - `play()` issues a REST *replace* → the old track's `TRACK_END(REPLACED)` is
102
+ * ignored by the client, so we never double-advance.
103
+ * - a natural finish → `TRACK_END(FINISHED|LOAD_FAILED)` → `advance()`.
104
+ * - `skip()` forces `advance()` (ignoring `LoopMode.Track`).
105
+ */
106
+
107
+ /** What a Player needs from its owner. The `Client` implements this. */
108
+ interface PlayerHost {
109
+ rest: REST;
110
+ emit(event: string, ...args: unknown[]): void;
111
+ dropPlayer(guildId: number): void;
112
+ }
113
+ type AutoplayHook = (player: Player) => Promise<Track | null>;
114
+ declare class Player {
115
+ private host;
116
+ guildId: number;
117
+ queue: Track[];
118
+ history: Track[];
119
+ current: Track | null;
120
+ /** Cap on retained history so a long-lived player can't leak memory. Queue-loop
121
+ * still works; it just restores the most recent `historyLimit` tracks. */
122
+ historyLimit: number;
123
+ loop: LoopMode;
124
+ volume: number;
125
+ paused: boolean;
126
+ state: PlayerState;
127
+ /** Optional async hook used when the queue and loop are exhausted. */
128
+ autoplay: AutoplayHook | null;
129
+ private created;
130
+ private voiceServer;
131
+ private voiceState;
132
+ constructor(host: PlayerHost, guildId: number);
133
+ handleVoiceServerUpdate(data: VoiceServerUpdate): void;
134
+ handleVoiceStateUpdate(data: VoiceStateUpdate): void;
135
+ get connected(): boolean;
136
+ private voicePayload;
137
+ private maybeStart;
138
+ play(query: string | Track, requester?: number): Promise<Track[]>;
139
+ add(query: string | Track, requester?: number): Promise<Track[]>;
140
+ skip(): Promise<void>;
141
+ stop(): Promise<void>;
142
+ pause(): Promise<void>;
143
+ resume(): Promise<void>;
144
+ setPause(paused: boolean): Promise<void>;
145
+ seek(positionMs: number): Promise<void>;
146
+ setVolume(volume: number): Promise<void>;
147
+ setLoop(mode: LoopMode): void;
148
+ shuffle(): void;
149
+ /** Apply a raw ffmpeg `-af` filtergraph (e.g. `"bass=g=8"`); `null` clears. */
150
+ setFilters(audioGraph: string | null): Promise<void>;
151
+ remove(index: number): Track | undefined;
152
+ clear(): void;
153
+ /** Reorder the queue: move the track at `from` to `to`. No-op on bad indices. */
154
+ move(from: number, to: number): void;
155
+ /** Resolve `query` and insert it at `index` (clamped) instead of appending. */
156
+ insert(index: number, query: string | Track, requester?: number): Promise<Track[]>;
157
+ /** Batch-enqueue several queries/tracks in order. Failed resolves are skipped. */
158
+ addMany(queries: (string | Track)[], requester?: number): Promise<Track[]>;
159
+ /** Skip forward to queue position `index`, discarding the ones in between. */
160
+ jump(index: number): Promise<void>;
161
+ /** Replay the previous track (from history), pushing the current one back on. */
162
+ previous(): Promise<void>;
163
+ /** Convenience EQ: bass boost in dB (0 clears). Built on {@link setFilters}. */
164
+ setBassBoost(gainDb: number): Promise<void>;
165
+ /** Convenience speed/pitch (nightcore/vaporwave): `rate` 1.0 = normal. */
166
+ setSpeed(rate: number): Promise<void>;
167
+ nowPlaying(): Track | null;
168
+ disconnect(): Promise<void>;
169
+ /** Append to history, evicting the oldest past `historyLimit` (leak guard). */
170
+ private pushHistory;
171
+ private resolve;
172
+ private nextTrack;
173
+ private advance;
174
+ private playTrack;
175
+ private setPausedInternal;
176
+ /** Called by the client's WS loop when a TRACK_END for this guild arrives. */
177
+ onTrackEnd(advances: boolean): Promise<void>;
178
+ }
179
+
180
+ /** The top-level client: owns the REST wrapper, the WebSocket event loop, the
181
+ * per-guild players, and event dispatch (via Node's EventEmitter). */
182
+
183
+ interface ClientOptions {
184
+ reconnect?: boolean;
185
+ }
186
+ /** Strongly-typed event map for `client.on(...)` / `client.emit(...)`. */
187
+ interface ClientEvents {
188
+ ready: [];
189
+ /** Fired instead of `ready` when a reconnect re-establishes the session. */
190
+ resumed: [];
191
+ disconnect: [];
192
+ error: [err: unknown];
193
+ trackStart: [player: Player | number, data: any];
194
+ trackEnd: [player: Player | number, data: any];
195
+ trackException: [player: Player | number, data: any];
196
+ trackStuck: [player: Player | number, data: any];
197
+ playerUpdate: [player: Player | number, data: any];
198
+ voiceClosed: [player: Player | number, data: any];
199
+ stats: [player: Player | number | null, data: any];
200
+ queueEnd: [player: Player];
201
+ }
202
+ interface Client {
203
+ on<K extends keyof ClientEvents>(event: K, listener: (...args: ClientEvents[K]) => void): this;
204
+ once<K extends keyof ClientEvents>(event: K, listener: (...args: ClientEvents[K]) => void): this;
205
+ off<K extends keyof ClientEvents>(event: K, listener: (...args: ClientEvents[K]) => void): this;
206
+ emit<K extends keyof ClientEvents>(event: K, ...args: ClientEvents[K]): boolean;
207
+ }
208
+ declare class Client extends EventEmitter implements PlayerHost {
209
+ readonly rest: REST;
210
+ private baseUrl;
211
+ private token;
212
+ private reconnect;
213
+ private playersMap;
214
+ private ws;
215
+ private heartbeatTimer;
216
+ private reconnectTimer;
217
+ private sessionId;
218
+ private closed;
219
+ private backoff;
220
+ private hasConnected;
221
+ private heartbeatSentAt;
222
+ /** Last measured heartbeat round-trip latency in ms (-1 until first ack). */
223
+ latencyMs: number;
224
+ /** Most recent STATS frame payload from the node, or null. */
225
+ lastStats: Record<string, unknown> | null;
226
+ constructor(baseUrl: string, token: string, opts?: ClientOptions);
227
+ start(): Promise<void>;
228
+ close(): Promise<void>;
229
+ /** Emit "error" without the Node EventEmitter footgun: an unlistened "error"
230
+ * throws and would crash the host process, so fall back to a console warning. */
231
+ private safeError;
232
+ getPlayer(guildId: number): Player;
233
+ players(): Player[];
234
+ dropPlayer(guildId: number): void;
235
+ loadTracks(identifier: string): Promise<LoadResult>;
236
+ /** `ytsearch:`-prefix a bare query and return the hits (empty on miss). */
237
+ search(query: string): Promise<Track[]>;
238
+ private get wsUrl();
239
+ private openSocket;
240
+ /** After a reconnect, pull each player's authoritative state from the node so
241
+ * position/volume/paused are accurate again. Per-player failures are ignored. */
242
+ private resyncPlayers;
243
+ private subscribeAll;
244
+ private send;
245
+ private handleFrame;
246
+ }
247
+
248
+ /** Exception hierarchy. Every network/protocol failure is an `AuralisError`. */
249
+ declare class AuralisError extends Error {
250
+ }
251
+ /** 401/403 — the bearer token is missing, wrong, or not allowed. */
252
+ declare class AuthError extends AuralisError {
253
+ }
254
+ /** 429 — too many requests. `retryAfter` is seconds if the server gave one. */
255
+ declare class RateLimited extends AuralisError {
256
+ retryAfter?: number | undefined;
257
+ constructor(message: string, retryAfter?: number | undefined);
258
+ }
259
+ /** Any other non-2xx HTTP response. */
260
+ declare class APIError extends AuralisError {
261
+ status: number;
262
+ constructor(status: number, message: string);
263
+ }
264
+ /** Tried to play before the guild's voice connection was established. */
265
+ declare class NotConnected extends AuralisError {
266
+ }
267
+ /** `loadtracks` returned `error` or nothing usable. */
268
+ declare class LoadError extends AuralisError {
269
+ }
270
+
271
+ export { APIError, AuralisError, AuthError, type AutoplayHook, Client, type ClientEvents, type ClientOptions, EndReason, LoadError, type LoadResult, LoadType, LoopMode, NotConnected, Player, type PlayerHost, type PlayerState, REST, RateLimited, type Track, type VoiceServerUpdate, type VoiceStateUpdate, endReasonAdvances };