
Lavalink-Client
Next steps
Section titled “Next steps”Overview
Section titled “Overview”Lavalink Client
An easy, flexible, and feature-rich Lavalink v4 Client for both beginners and experts.
🚀 Features
- 💯 Lavalink v4 Native: Full support for Lavalink v4, including its powerful plugin ecosystem.
- ✅ Detailed Player-Destroy Reasons: Understand precisely why a player was destroyed (e.g., channel deleted, bot disconnected).
- ✨ Flexible Queue Stores: Use the default in-memory store or bring your own (Redis, databases, etc.) to sync queues across multiple processes.
- 🎶 Unresolved Tracks: Supports unresolved track objects, fetching full data only when a track is about to play, saving API requests and resources.
- 🎚️ Built-in Filters & EQ: Easy-to-use management for audio filters and equalizers.
- ⚙️ Advanced Player Options: Fine-tune player behavior for disconnects, empty queues, volume handling, and more.
- 🛡️ Lavalink-Side Validation: Ensures you only use filters, plugins, and sources that your Lavalink node actually supports.
- 🔒 Client-Side Validation: Whitelist and blacklist URLs or domains to prevent unwanted requests and protect your bot.
- 🧑💻 Developer-Friendly: A memory-efficient design with a clean, intuitive API that mirrors Lavalink’s own implementation.
- 🤖 Automated Handling: Automatically handles track skipping on errors, voice channel deletions, server-wide mutes, and much more.
📦 Installation
Latest Stable Version: v2.5.x
👉 via NPM
# Stable (install release)npm install --save lavalink-client
# Development (Install github dev-branch)npm install --save tomato6966/lavalink-client
👉 via YARN
# Stable (install release)yarn add lavalink-client
# Development (Install github dev-branch)yarn add tomato6966/lavalink-client
👉 via BUN
# Stable (install release)bun add lavalink-client
# Development (Install github dev-branch)bun add tomato6966/lavalink-client
👉 via pnpm
# Stable (install release)pnpm add lavalink-client
# Development (Install github dev-branch)pnpm add tomato6966/lavalink-client
📖 Documentation & Guides
- Full Documentation - Your starting point for everything.
- Manager Events - Handle track, player, and general client events.
- NodeManager Events - Manage node connections, errors, and logs.
- Session Resuming Guide - Learn how to implement session resuming for seamless restarts.
💖 Used In
This client powers various Discord bots:
- Mivator (Public Bot by @Tomato6966)
- Betty (Public Bot by fb_sean)
- Bots by Contributors:
- Bots Community (Users):
- Soundy (@idMJA)
🛠️ Configuration Examples
Basic Setup
A minimal example to get you started quickly.
import { LavalinkManager } from "lavalink-client";import { Client, GatewayIntentBits } from "discord.js"; // example for a discord bot
// Extend the Client type to include the lavalink managerdeclare module "discord.js" { interface Client { lavalink: LavalinkManager; }}
const client = new Client({ intents: [ GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates, ]});
client.lavalink = new LavalinkManager({ nodes: [ { authorization: "youshallnotpass", // The password for your Lavalink server host: "localhost", port: 2333, id: "Main Node", } ], // A function to send voice server updates to the Lavalink client sendToShard: (guildId, payload) => { const guild = client.guilds.cache.get(guildId); if (guild) guild.shard.send(payload); }, autoSkip: true, client: { id: process.env.CLIENT_ID, // Your bot's user ID username: "MyBot", },});
// Listen for the 'raw' event from discord.js and forward itclient.on("raw", (d) => client.lavalink.sendRawData(d));
client.on("ready", () => { console.log(`Logged in as ${client.user.tag}!`); // Initialize the Lavalink client client.lavalink.init({ ...client.user });});
client.login(process.env.DISCORD_TOKEN);
🔩 Complete Configuration Example (almost all Options)
import { LavalinkManager, QueueChangesWatcher, QueueStoreManager, StoredQueue } from "lavalink-client";import { RedisClientType, createClient } from "redis";import { Client, GatewayIntentBits, User } from "discord.js";
// It's recommended to extend the Client typedeclare module "discord.js" { interface Client { lavalink: LavalinkManager; redis: RedisClientType; }}
const client = new Client({ intents: [ GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates, ]});
client.lavalink = new LavalinkManager({ nodes: [ { authorization: "youshallnotpass", host: "localhost", port: 2333, id: "testnode", secure: false, // Set to true for wss:// retryAmount: 5, retryDelay: 10_000, // 10 seconds } ], sendToShard: (guildId, payload) => client.guilds.cache.get(guildId)?.shard?.send(payload), autoSkip: true, // automatically play the next song of the queue, on: trackend, trackerror, trackexception client: { id: process.env.CLIENT_ID, username: "TESTBOT", }, playerOptions: { applyVolumeAsFilter: false, clientBasedPositionUpdateInterval: 50, defaultSearchPlatform: "ytmsearch", volumeDecrementer: 0.75, onDisconnect: { autoReconnect: true, destroyPlayer: false, }, onEmptyQueue: { destroyAfterMs: 30_000, // function get's called onqueueempty, and if there are songs added to the queue, it continues playing. if not then not (autoplay functionality) // autoPlayFunction: async (player) => { /* ... */ }, }, useUnresolvedData: true, }, queueOptions: { maxPreviousTracks: 10, queueStore: new MyCustomRedisStore(client.redis), queueChangesWatcher: new MyCustomQueueWatcher(client), }, // Whitelist/Blacklist links or words linksAllowed: true, linksBlacklist: ["somebadsite.com"], linksWhitelist: [], advancedOptions: { debugOptions: { noAudio: false, playerDestroy: { dontThrowError: false, debugLog: false }, } }});
client.on("raw", d => client.lavalink.sendRawData(d));client.on("ready", () => client.lavalink.init({ ...client.user }));
// Example Custom Redis Queue Storeclass MyCustomRedisStore implements QueueStoreManager { private redis: RedisClientType; constructor(redisClient: RedisClientType) { this.redis = redisClient; } private key(guildId: string) { return `lavalinkqueue_${guildId}`; } async get(guildId: string) { return await this.redis.get(this.key(guildId)); } async set(guildId: string, data: string) { return await this.redis.set(this.key(guildId), data); } async delete(guildId: string) { return await this.redis.del(this.key(guildId)); } async parse(data: string): Promise<Partial<StoredQueue>> { return JSON.parse(data); } stringify(data: Partial<StoredQueue>): string { return JSON.stringify(data); }}
// Example Custom Queue Watcherclass MyCustomQueueWatcher implements QueueChangesWatcher { private client: Client; constructor(client: Client) { this.client = client; } shuffled(guildId: string) { console.log(`Queue shuffled in guild: ${guildId}`); } tracksAdd(guildId: string, tracks: any[], position: number) { console.log(`${tracks.length} tracks added at position ${position} in guild: ${guildId}`); } tracksRemoved(guildId: string, tracks: any[], position: number) { console.log(`${tracks.length} tracks removed at position ${position} in guild: ${guildId}`); }}
📢 Events
Listen to events to create interactive and responsive logic.
Lavalink Manager Events
These events are emitted from the main LavalinkManager
instance and relate to players and tracks.
playerCreate (player)
playerDestroy (player, reason)
playerDisconnect (player, voiceChannelId)
playerMove (player, oldChannelId, newChannelId)
trackStart (player, track)
trackEnd (player, track)
trackStuck (player, track, payload)
trackError (player, track, payload)
queueEnd (player)
📢 Example for Manager-Event-Listeners
// Example: Listening to a track start eventclient.lavalink.on("trackStart", (player, track) => { const channel = client.channels.cache.get(player.textChannelId); if(channel) channel.send(`Now playing: ${track.info.title}`);});
// Example: Handling queue endclient.lavalink.on("queueEnd", (player) => { const channel = client.channels.cache.get(player.textChannelId); if(channel) channel.send("The queue has finished. Add more songs!"); player.destroy();});
Node Manager Events
These events are emitted from lavalink.nodeManager
and relate to the Lavalink node connections.
create (node)
connect (node)
disconnect (node, reason)
reconnecting (node)
destroy (node)
error (node, error, payload)
resumed (node, payload, players)
📢 Example for Node-Event-Listeners
// Example: Logging node connections and errorsclient.lavalink.nodeManager.on("connect", (node) => { console.log(`Node "${node.id}" connected!`);});
client.lavalink.nodeManager.on("error", (node, error) => { console.error(`Node "${node.id}" encountered an error:`, error.message);});
📚 Advanced How-To Guides
How to Implement Session Resuming
Resuming allows your music bot to continue playback even after a restart.
- Enable Resuming on the Node: When a node connects, enable resuming with a timeout.
- Listen for the
resumed
Event: This event fires on a successful reconnect, providing all player data from Lavalink. - Re-create Players: Use the data from the
resumed
event and your own saved data (from a database/store) to rebuild the players and their queues.
💡 For a complete, working example, see the official test bot’s implementation.
💡 Principle of how to enable **resuming**
// 1. Enable resuming on connectclient.lavalink.nodeManager.on("connect", (node) => { // Enable resuming for 5 minutes (300,000 ms) node.updateSession(true, 300_000);});
// 2. Listen for the resumed eventclient.lavalink.nodeManager.on("resumed", async (node, payload, fetchedPlayers) => { console.log(`Node "${node.id}" successfully resumed with ${fetchedPlayers.length} players.`);
for (const lavalinkData of fetchedPlayers) { // 3. Get your saved data (e.g., from Redis/DB) const savedData = await getFromDatabase(lavalinkData.guildId); if (!savedData || !lavalinkData.state.connected) { if(savedData) await deleteFromDatabase(lavalinkData.guildId); continue; // Skip if no saved data or Lavalink reports disconnected }
// Re-create the player instance const player = client.lavalink.createPlayer({ guildId: lavalinkData.guildId, voiceChannelId: savedData.voiceChannelId, textChannelId: savedData.textChannelId, // Important: Use the same node that was resumed node: node.id, // Set volume from Lavalink's data, accounting for the volume decrementer volume: lavalinkData.volume, selfDeaf: savedData.selfDeaf, });
// Re-establish voice connection await player.connect();
// Restore player state player.paused = lavalinkData.paused; player.lastPosition = lavalinkData.state.position; player.filterManager.data = lavalinkData.filters;
// Restore the queue await player.queue.utils.sync(true, false); // Syncs with your QueueStore
// Restore the current track if (lavalinkData.track) { player.queue.current = client.lavalink.utils.buildTrack(lavalinkData.track, savedData.requester); } }});
// Persist player data on updates to use for resuming laterclient.lavalink.on("playerUpdate", (oldPlayer, newPlayer) => { saveToDatabase(newPlayer.toJSON());});
// Clean up data when a player is permanently destroyedclient.lavalink.on("playerDestroy", (player) => { deleteFromDatabase(player.guildId);});
How to Use Plugins
Lavalink client supports most of the major lavalink-plugins. The client itself is - for beginner friendly reasons - atm not extendable (via plugins) You can just use the built in functions (sponsor block, lyrics) or search plattforms (deezer, spotify, apple music, youtube, …) and use the lavalink-plugins without any configuration on the client side.
Some plugins require extra-parameters, such as flowerytts: Pass extra parameters to the search function to use plugin-specific features.
How to use the flowerytts plugin
// Example for flowertts pluginconst query = interaction.options.getString("text");const voice = interaction.options.getString("voice"); // e.g., "MALE_1"
const extraParams = new URLSearchParams();if (voice) extraParams.append(`voice`, voice);
// All params for flowertts can be found here: https://flowery.pw/docsconst response = await player.search( { query: `${query}`, // This is used by plugins like ftts to adjust the request extraQueryUrlParams: extraParams, source: "ftts" // Specify the plugin source }, interaction.user // The requester);
// Add the TTS track to the queueif (response.tracks.length > 0) { player.queue.add(response.tracks[0]); if (!player.playing) player.play();}