<div style="font-family: Arial, sans-serif; border: 1px solid #fab788; border-radius: 15px; padding: 25px; ">

<h1 align="center" style="color: #fab788; border-bottom: 2px solid #3498db; padding-bottom: 10px;">Lavalink Client</h1>
<p align="center" style="font-size: 1.2em; color: #555;">An easy, flexible, and feature-rich Lavalink v4 Client for both beginners and experts.</p>

<div align="center">
  <p>
    <img src="https://madewithlove.now.sh/at?heart=true&template=for-the-badge" alt="Made with love in Austria">
    <img alt="Made with TypeScript" src="https://img.shields.io/badge/typescript-%23007ACC.svg?style=for-the-badge&logo=typescript&logoColor=white">
  </p>
  <p>
    <a href="https://www.npmjs.com/package/lavalink-client">
      <img src="https://img.shields.io/npm/v/lavalink-client.svg?maxAge=3600&style=for-the-badge&logo=npm&logoColor=red" alt="NPM Version" />
    </a>
    <a href="https://www.npmjs.com/package/lavalink-client">
      <img src="https://img.shields.io/npm/dt/lavalink-client.svg?maxAge=3600&style=for-the-badge&logo=npm&logoColor=red" alt="NPM Downloads" />
    </a>
    <a href="https://tomato6966.github.io/lavalink-client/">
      <img src="https://img.shields.io/badge/Documentation-%230288D1.svg?style=for-the-badge&logo=gitbook&logoColor=white" alt="Get Started Now">
    </a>
  </p>
  <p>
    <a href="https://www.npmjs.com/package/lavalink-client"><img src="https://nodei.co/npm/lavalink-client.png?downloads=true&stars=true" alt="NPM Install: lavalink-client" /></a>
  </p>
</div>

---

## 🚀 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`**

<details>
<summary><strong>👉 via NPM</strong></summary>

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

# Development (Install github dev-branch)
npm install --save tomato6966/lavalink-client
```

</details>

<details>
<summary><strong>👉 via YARN</strong></summary>

```bash
# Stable (install release)
yarn add lavalink-client

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

</details>

<details>
<summary><strong>👉 via BUN</strong></summary>

```bash
# Stable (install release)
bun add lavalink-client

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

</details>

<details>
<summary><strong>👉 via pnpm</strong></summary>

```bash
# Stable (install release)
pnpm add lavalink-client

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

</details>

## 📖 Documentation & Guides

- **[Full Documentation](https://tomato6966.github.io/lavalink-client/)** - Your starting point for everything.
- **[Manager Events](https://tomato6966.github.io/lavalink-client/extra/manager-events)** - Handle track, player, and general client events.
- **[NodeManager Events](https://tomato6966.github.io/lavalink-client/extra/node-events)** - Manage node connections, errors, and logs.
- **[Session Resuming Guide](https://tomato6966.github.io/lavalink-client/extra/resuming)** - Learn how to implement session resuming for seamless restarts.

---

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

Import the NodeType Enum:

```
import { NodeType } from "lavalink-client"
```

// You must assign NodeLink enum to nodeType of the node options, before creating the node.

```ts
nodeOptions.nodeType = NodeType.NodeLink;
```

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

```ts
client.lavalink = new LavalinkManager({
    nodes: [
        {
            host: "localhost",
            nodeType: NodeType.NodeLink, // provide nodeType "nodelink" to it.
        },
    ],
});
// 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: NodeType.NodeLink, // provide it here
        },
        // you can also use the util like this, and it will return a valid node option object. must start with: lavalink:// | nodelink://
        // parseLavalinkConnUrl("nodelink://<nodeId>:<nodeAuthorization(Password)>@<NodeHost>:<NodePort>")
    ],
    // 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:

```ts
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 does not really require you to run "instaUpdateFiltersFix" on player creation.

### NodeLink Specific Methods

- **`node.getYoutubeOAUTH(refreshToken)`**: Exchange a Refresh Token for an Access Token. [Docs](https://nodelink.js.org/docs/api/nodelink-features#oauth)
- **`node.updateYoutubeOAUTH(refreshToken)`**: Update the OAUTH token. [Docs](https://nodelink.js.org/docs/api/nodelink-features#oauth)
- **`node.getChapters(player, track?)`**: Retrieve Chapters of Youtube Videos. [Docs](https://nodelink.js.org/docs/api/nodelink-features#loadchapters)
- **`node.nodeLinkLyrics(player, track?, language?)`**: Retrieve Lyrics of Youtube Videos. [Docs](https://nodelink.js.org/docs/api/nodelink-features#lyrics--chapters)
- **`node.getDirectStream(track)`**: Stream audio directly from NodeLink. [Docs](https://nodelink.js.org/docs/api/nodelink-features#direct-streaming)
- **`node.listMixerLayers(player)`**: Retrieves a list of currently active mix layers. [Docs](https://nodelink.js.org/docs/api/rest#get-active-mixes)
- **`node.addMixerLayer(player, track, volume)`**: Adds a new audio track to be mixed. [Docs](https://nodelink.js.org/docs/api/rest#add-mix-layer)
- **`node.removeMixerLayer(player, mixId)`**: Removes a specific mix layer. [Docs](https://nodelink.js.org/docs/api/rest#remove-mix-layer)
- **`node.updateMixerLayerVolume(player, mixId, volume)`**: Updates the volume of a specific mix layer. [Docs](https://nodelink.js.org/docs/api/rest#update-mix-volume)
- **`node.changeAudioTrackLanguage(player, language_audioTrackId)`**: Changes the current language of the audio. [Docs](https://nodelink.js.org/docs/api/nodelink-features#additional-filters)
- **`node.updateYoutubeConfig(refreshToken?, visitorData?)`**: Updates the YouTube configuration. [Docs](https://nodelink.js.org/docs/api/nodelink-features#update-config)
- **`node.getYoutubeConfig(validate?)`**: Gets the YouTube configuration.
- **`node.getConnectionMetrics()`**: Get connection metrics. [Docs](https://nodelink.js.org/docs/api/rest#node-information)
- **`node.loadDirectStream(track, volume, position, filters)`**: Stream raw PCM audio. [Docs](https://nodelink.js.org/docs/api/nodelink-features#loadstream)

### NodeLink Specific Sources (`SourcesRecord`)

By default, the client uses a generic `DefaultSources` record to map human-friendly prefixes like `youtube:`, `spotify:`, etc. to Lavalink search sources.

For NodeLink, you can override this behavior to use the NodeLink-specific source prefixes (including `admsearch`, `audiomack`, `gaanasearch`, `gtts`, `pipertts`, etc.) via the `NodeLinkDefaultSources` export and the `ManagerUtils#SourcesRecord` property:

```ts
import { LavalinkManager, NodeType, NodeLinkNode, NodeLinkDefaultSources } from "lavalink-client";

// create the manager as usual, but with a NodeLink node
client.lavalink = new LavalinkManager({
    nodes: [
        {
            authorization: "youshallnotpass",
            host: "localhost",
            port: 2333,
            id: "nodelink-main",
            nodeType: NodeType.NodeLink,
        },
    ],
    sendToShard: (guildId, payload) => client.guilds.cache.get(guildId)?.shard?.send(payload),
    autoSkip: true,
    client: {
        id: envConfig.clientId,
        username: "MyBot",
    },
});

// IMPORTANT: Override the default source mapping so `ytsearch:`, `amsearch:`, `admsearch:`, etc.
// use the NodeLink-specific prefixes.
client.lavalink.utils.SourcesRecord = NodeLinkDefaultSources;

// Optional: You can also provide your own custom record extending NodeLinkDefaultSources:
client.lavalink.utils.SourcesRecord = {
    ...NodeLinkDefaultSources,
    // custom alias that only your bot understands, which resolves to a NodeLink source
    mycustomsource: "ytsearch",
};
```

This makes all helper methods that use `ManagerUtils` (such as `transformQuery`, `transformLavaSearchQuery` and `validateSourceString`) work with NodeLink’s extended search prefixes when you are connected to a NodeLink node.

### NodeLink Specififc Events?

```ts
// 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:

- **[Mivator](https://discord.gg/5dUb7M2qCj)** (Public Bot by @Tomato6966)
- **[Betty](https://betty.bot/?utm_source=lavalink-client)** (Public Bot by @fb_sean)
- **[Nero](https://betty.bot/?utm_source=lavalink-client)** (Public Bot by @fb_sean)
- **Bots by Contributors:**
    - [Mintone](https://mintone.tech/) (@appujet)
    - [Stelle](https://github.com/Ganyu-Studios/stelle-music) (@EvilG-MC)
    - [Panais](https://panais.xyz/) (@LucasB25)
    - [Akyn](https://akynbot.vercel.app/) (@notdeltaxd)
    - [ARINO](https://site.arinoapp.qzz.io/) (@ryanwtf88)
    - [iHorizon](https://github.com/ihrz/ihrz) (@iHorizon)
- **Bots By Community (Users):**
    - [Soundy](https://github.com/idMJA/Soundy) (@idMJA)
    - [BeatBot](https://getbeatbot.vercel.app/) (@zenitsujs)
    - [Atom Music](https://top.gg/bot/1320469557411971165) (@sakshamyep)
    - [All Time Bot](https://top.gg/bot/1163027457671180418) (@PeterGamez)
    - [BeatDock](https://github.com/lazaroagomez/BeatDock) (@lazaroagomez)
    - [Nazha](https://top.gg/bot/1124681788070055967) (@Nazha-Team)
    - [Arii Music](https://arimusic.me/) (@friston_ae)

---

## 🛠️ Configuration Examples

### Basic Setup

A minimal example to get you started quickly.

```typescript
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",
        },
        // you can also use the util like this, and it will return a valid node option object.
        // parseLavalinkConnUrl("nodelink://<nodeId>:<nodeAuthorization(Password)>@<NodeHost>:<NodePort>")
    ],
    // 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);
```

<details>
<summary><strong>🔩 Complete Configuration Example (almost all Options)</strong></summary>

```typescript
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
        },
        // you can also use the util like this, and it will return a valid node option object. must start with: lavalink:// | nodelink://
        // parseLavalinkConnUrl("lavalink://<nodeId>:<nodeAuthorization(Password)>@<NodeHost>:<NodePort>")
    ],
    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}`);
    }
}
```

</details>

---

## 📢 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)`

<details>
<summary><strong>📢 Example for Manager-Event-Listeners</strong></summary>

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

</details>

### 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)`

<details>
<summary><strong>📢 Example for Node-Event-Listeners</strong></summary>

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

</details>

---

## 📚 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](https://github.com/Tomato6966/lavalink-client/blob/main/testBot/Utils/handleResuming.ts).**

<details>
<summary><strong>💡 Principle of how to enable **resuming**</strong></summary>

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

</details>

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

<details>
<summary><strong>How to use the flowerytts plugin</strong></summary>

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

</details>

</div>
