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

Published

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

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

# 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


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()

NodeLink Specific Methods

  • 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 Specififc Events?

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