npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

lavalink-client

v2.7.7

Published

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

Readme


🚀 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

# Stable (install release)
npm install --save lavalink-client

# Development (Install github dev-branch)
npm install --save tomato6966/lavalink-client
# Stable (install release)
yarn add lavalink-client

# Development (Install github dev-branch)
yarn add tomato6966/lavalink-client
# Stable (install release)
bun add lavalink-client

# Development (Install github dev-branch)
bun add tomato6966/lavalink-client
# Stable (install release)
pnpm add lavalink-client

# Development (Install github dev-branch)
pnpm add tomato6966/lavalink-client

📖 Documentation & Guides


💖 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);
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.

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: 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: 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.

// 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.

// 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();
}