Skip to content

Lavalink-Client

Easy, flexible and feature-rich lavalink@v4 Client. Both for Beginners and Proficients.

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 Queue Filtering: Search and filter tracks in the queue by title, author, duration, and more with powerful query options.
  • ⚙️ 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.9.x

👉 via NPM
Terminal window
# Stable (install release)
npm install --save lavalink-client
# Development (Install github dev-branch)
npm install --save tomato6966/lavalink-client
👉 via YARN
Terminal window
# Stable (install release)
yarn add lavalink-client
# Development (Install github dev-branch)
yarn add tomato6966/lavalink-client
👉 via BUN
Terminal window
# Stable (install release)
bun add lavalink-client
# Development (Install github dev-branch)
bun add tomato6966/lavalink-client
👉 via pnpm
Terminal window
# Stable (install release)
pnpm add lavalink-client
# Development (Install github dev-branch)
pnpm add tomato6966/lavalink-client

📖 Documentation & Guides


Node Link

This client can be used with nodelink too, but because nodelink’s websocket is different than the one from lavalink, you need to disable a few things on the NODE OPTIONS / NODE PROPERTIES:

nodeOptions.nodeType = "NodeLink";

this can be done directly when creating the node in the lavalinkmanager.

client.lavalink = new LavalinkManager({
nodes: [
{
host: "localhost",
nodeType: "NodeLink",
},
],
});
// or here if you need a bigger example.
client.lavalink = new LavalinkManager({
nodes: [
{
authorization: "youshallnotpass", // The password for your Lavalink server
host: "localhost",
port: 2333,
id: "Main Node",
// set to nodeLink
nodeType: "NodeLink",
},
],
// 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",
},
});

Now if you want to use NodeLink specific functions, you can use the type assertion checker function:

if (node.isNodeLink()) {
// node is now typed as NodeLink
node.addMixerLayer();
} else if (node.isLavalinkNode()) {
// node is now typed as LavalinkNode
} else {
// node is now typed as whatever it is..
}

or you have to assert the type…

const node = client.lavalink.lavalinkManager.getNode("id") as NodeLinkNode;
node.addMixerLayer()
  • node.getYoutubeOAUTH(refreshToken): Exchange a Refresh Token for an Access Token. Docs
  • node.updateYoutubeOAUTH(refreshToken): Update the OAUTH token. Docs
  • node.getChapters(player, track?): Retrieve Chapters of Youtube Videos. Docs
  • node.nodeLinkLyrics(player, track?, language?): Retrieve Lyrics of Youtube Videos. Docs
  • node.getDirectStream(track): Stream audio directly from NodeLink. Docs
  • node.listMixerLayers(player): Retrieves a list of currently active mix layers. Docs
  • node.addMixerLayer(player, track, volume): Adds a new audio track to be mixed. Docs
  • node.removeMixerLayer(player, mixId): Removes a specific mix layer. Docs
  • node.updateMixerLayerVolume(player, mixId, volume): Updates the volume of a specific mix layer. Docs
  • node.changeAudioTrackLanguage(player, language_audioTrackId): Changes the current language of the audio. Docs
  • node.updateYoutubeConfig(refreshToken?, visitorData?): Updates the YouTube configuration. Docs
  • node.getYoutubeConfig(validate?): Gets the YouTube configuration.
  • node.getConnectionMetrics(): Get connection metrics. Docs
  • node.loadDirectStream(track, volume, position, filters): Stream raw PCM audio. Docs
// NodeLink specific events
client.lavalink.nodeManager.on("nodeLinkEvent", (node, eventName, player, track, payload) => {
switch (eventName) {
// -------------Player LifeCycle Events-------------
// https://nodelink.js.org/docs/api/websocket#playercreatedevent
case "PlayerCreatedEvent":
{
// { "guildId": "987654321098765432", "track": null, "paused": false, "volume": 100 }
const playerInfo = payload.player;
console.log(`Player created in guildId: ${playerInfo.guildId}`);
}
break;
// https://nodelink.js.org/docs/api/websocket#playerdestroyedevent
case "PlayerDestroyedEvent":
{
// "987654321098765432"
const playerInfo = payload.guildId;
console.log(`Player destroyed in guildId: ${playerInfo.guildId}`);
}
break;
// https://nodelink.js.org/docs/api/websocket#playerconnectedevent
case "PlayerConnectedEvent":
{
// { "sessionId": "abc", "token": "token", "endpoint": "us-central123.discord.media", "channelId": "123456789012345678" }
const playerInfo = payload.voice;
console.log(`Player connected in guildId: ${playerInfo.guildId}`);
}
break;
// https://nodelink.js.org/docs/api/websocket#playerreconnectingevent
case "PlayerReconnectingEvent":
{
// { "sessionId": "abc", "token": "token", "endpoint": "us-central123.discord.media", "channelId": "123456789012345678" }
const playerInfo = payload.voice;
console.log(`Player reconnecting in guildId: ${playerInfo.guildId}`);
}
break;
// -------------Player State Events-------------
// https://nodelink.js.org/docs/api/websocket#volumechangedevent
case "VolumeChangedEvent":
{
// "guildId": "987654321098765432",
// "volume": 100
const { guildId, volume } = payload;
console.log(`Player volume changed in guildId: ${guildId} to ${volume}`);
}
break;
// https://nodelink.js.org/docs/api/websocket#filterschangedevent
case "FiltersChangedEvent":
{
// { ...Filtersdata... }
const { guildId, filters } = payload;
console.log(`Player filters changed in guildId: ${guildId}, new Data: `, filters);
}
break;
// https://nodelink.js.org/docs/api/websocket#seekevent
case "SeekEvent":
{
// "guildId": "987654321098765432",
// "position": 10000,
const { guildId, position } = payload;
console.log(`Player seeked in guildId: ${guildId}, new position: ${position}`);
}
break;
// https://nodelink.js.org/docs/api/websocket#pauseevent
case "PauseEvent":
{
// "guildId": "987654321098765432",
// "paused": true
const { guildId, paused } = payload;
console.log(`Player paused in guildId: ${guildId}, paused true/false: ${paused}`);
}
break;
// https://nodelink.js.org/docs/api/websocket#connectionstatusevent
case "ConnectionStatusEvent":
{
// "guildId": "987654321098765432",
// "status": "CONNECTED"
// "metrics": { ... }
const { guildId, status, metrics } = payload;
console.log(
`Player connection status changed in guildId: ${guildId}, status: ${status}, metrics: `,
metrics,
);
}
break;
// -------------LYRICS EVENTS-------------
// https://nodelink.js.org/docs/api/websocket#lyrics-events
case "LyricsFoundEvent":
{
// "guildId": "987654321098765432",
// "lyrics": "..."
const { guildId, lyrics } = payload;
console.log(`Lyrics found in guildId: ${guildId}, lyrics: `, lyrics);
}
break;
// https://nodelink.js.org/docs/api/websocket#lyricslineevent
case "LyricsLineEvent":
{
// "guildId": "987654321098765432",
// "lineIndex": 0,
// "line": "..."
const { guildId, lineIndex, line } = payload;
console.log(`Lyrics line in guildId: ${guildId}, lineIndex: ${lineIndex}, line: `, line);
}
break;
// https://nodelink.js.org/docs/api/websocket#lyricsnotfoundevent
case "LyricsNotFoundEvent":
{
// "guildId": "987654321098765432",
const { guildId } = payload;
console.log(`Lyrics not found in guildId: ${guildId}`);
}
break;
// -------------AUDIO MIXER EVENTS-------------
// https://nodelink.js.org/docs/api/websocket#mixstartedevent
case "MixStartedEvent":
{
// "guildId": "987654321098765432",
// "mixId": "123456789012345678"
// "volume": 0.8,
// "track": { ...TrackData... }
const { guildId, mixId, volume, track } = payload.player;
console.log(
`Player mix started in guildId: ${guildId}, mixId: ${mixId}, volume: ${volume}, track: `,
track,
);
}
break;
// https://nodelink.js.org/docs/api/websocket#mixendedevent
case "MixEndedEvent":
{
// "guildId": "987654321098765432",
// "mixId": "123456789012345678",
// "reason": "USER_STOPPED"
const { guildId, mixId, reason } = payload;
console.log(`Player mix ended in guildId: ${guildId}, mixId: ${mixId}, reason: ${reason}`);
}
break;
}
});

💖 Used In

This client powers various Discord bots:


🛠️ 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 manager
declare 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 it
client.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 type
declare 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 Store
class 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 Watcher
class 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.

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 event
client.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 end
client.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 errors
client.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.

  1. Enable Resuming on the Node: When a node connects, enable resuming with a timeout.
  2. Listen for the resumed Event: This event fires on a successful reconnect, providing all player data from Lavalink.
  3. 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 connect
client.lavalink.nodeManager.on("connect", (node) => {
// Enable resuming for 5 minutes (300,000 ms)
node.updateSession(true, 300_000);
});
// 2. Listen for the resumed event
client.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 later
client.lavalink.on("playerUpdate", (oldPlayer, newPlayer) => {
saveToDatabase(newPlayer.toJSON());
});
// Clean up data when a player is permanently destroyed
client.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 plugin
const 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/docs
const 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 queue
if (response.tracks.length > 0) {
player.queue.add(response.tracks[0]);
if (!player.playing) player.play();
}