Player
Player owns three managers — nodes, voices, and queues — each responsible for one layer of the system. All three are immutable after construction and safe to destructure.
Player├── NodeManager (player.nodes) — Lavalink WebSocket + REST connections├── VoiceManager (player.voices) — Discord voice connections per guild└── QueueManager (player.queues) — playback queues per guildPlayer options reference
Section titled “Player options reference”| Option | Default | Description |
|---|---|---|
nodes | required | Array of node options |
forwardVoiceUpdate | required | Callback to send voice gateway payloads to Discord |
autoInit | true | Call init() automatically on Discord READY |
autoSync | true | Push local queue state to Lavalink when a node reconnects without resuming |
queryPrefix | "ytsearch" | Default search prefix for non-URL queries |
relocateQueues | true | Migrate queues to another node when their node closes or disconnects |
fetchRelatedTracks | returns [] | Called by autoplay when the queue empties — return Track[] to continue, [] to stop |
plugins | [] | Plugins to initialize after nodes are created but before they connect |
Working with managers
Section titled “Working with managers”All three managers implement a partial Map interface. You get get, has, size, keys, values, entries, and [Symbol.iterator] — so every iteration pattern that works on a Map works here too.
// Sizeplayer.nodes.size; // number of registered nodesplayer.voices.size; // number of active voice connectionsplayer.queues.size; // number of active queues
// Lookupplayer.nodes.get("us-east"); // Node | undefinedplayer.voices.get(guildId); // VoiceState | undefinedplayer.queues.get(guildId); // Queue | undefined
// Existence checkplayer.nodes.has("us-east"); // booleanplayer.voices.has(guildId); // booleanplayer.queues.has(guildId); // boolean
// Explicit iterationfor (const [name, node] of player.nodes.entries()) { /* ... */}for (const [id, voice] of player.voices.entries()) { /* ... */}for (const [id, queue] of player.queues.entries()) { /* ... */}
// Implicit iteration — managers are directly iterablefor (const [name, node] of player.nodes) { /* ... */}for (const [id, voice] of player.voices) { /* ... */}for (const [id, queue] of player.queues) { /* ... */}
// Keys, valuesfor (const name of player.nodes.keys()) { /* ... */}for (const queue of player.queues.values()) { /* ... */}
// Spread / Array.fromconst allQueues = [...player.queues.values()];const nodeNames = Array.from(player.nodes.keys());NodeManager also exposes two additional read-only maps:
player.nodes.info; // Map<string, LavalinkInfo> — cached node info per node nameplayer.nodes.metrics; // Map<string, NodeMetrics> — live scoring metrics per node nameThese are populated automatically — info on nodeReady, metrics on every stats dispatch.
All player events
Section titled “All player events”Player extends EventEmitter. Subscribe with player.on(event, handler).
Node events
player.on("nodeConnect", (node, reconnects) => { // Socket opened. reconnects > 0 means this was a reconnect attempt.});
player.on("nodeReady", (node, resumed, sessionId) => { // Node has a session ID and is ready to use. // resumed: true if Lavalink kept the previous session.});
player.on("nodeClose", (node, code, reason) => { // Unexpected close — reconnection will be attempted. // Queue relocation may happen here if relocateQueues is true.});
player.on("nodeDisconnect", (node, code, reason, byLocal) => { // Final disconnect — no more reconnect attempts. // byLocal: true if your code called disconnect().});
player.on("nodeError", (node, error) => { /* WebSocket error */});
player.on("nodeDispatch", (node, payload) => { // Every raw Lavalink WebSocket payload — useful for plugins and debugging.});Voice events
player.on("voiceConnect", (voice) => { // Received and forwarded voice server update to Lavalink.});
player.on("voiceClose", (voice, code, reason, byRemote) => { // Discord voice WebSocket closed. // byRemote: true if Discord closed it (kick, server crash, etc.) // Recoverable codes trigger automatic reconnection.});
player.on("voiceChange", (voice, previousNode, wasPlaying) => { // Queue migrated to a different node. // wasPlaying: true if a track was playing when the migration happened.});
player.on("voiceDestroy", (voice, reason) => { // Voice connection torn down. Queue is also destroyed at this point.});Queue events
player.on("queueCreate", (queue) => { /* queue created */});
player.on("queueUpdate", (queue, state) => { // Fires on every Lavalink player state tick (every 5s by default). // state: { time, position, connected, ping } // Use queue.currentTime for an interpolated position between ticks.});
player.on("queueFinish", (queue) => { // Queue is empty and autoplay returned nothing. player.voices.destroy(queue.guildId, "Queue finished");});
player.on("queueDestroy", (queue, reason) => { /* clean up UI state */});Track events
player.on("trackStart", (queue, track, inQueue) => { console.log(`▶ ${track.title} [${track.formattedDuration}]`);});
player.on("trackFinish", (queue, track, reason, inQueue) => { // reason: 'finished' | 'loadFailed' | 'stopped' | 'replaced' | 'cleanup' if (reason === "loadFailed") console.error(`Failed: ${track.title}`);});
player.on("trackError", (queue, track, exception, inQueue) => { // exception.severity: 'common' | 'suspicious' | 'fault' console.error(`[${exception.severity}] ${exception.message}`);});
player.on("trackStuck", (queue, track, thresholdMs, inQueue) => { queue.next(); // skip the stuck track});The inQueue boolean indicates whether track is the exact object instance in queue.tracks. It’s false only for the replaced end reason — in that case use queue.track for the currently playing track instead.