lavalink-client 2.8.0 → 2.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (6) hide show
  1. package/README.md +726 -518
  2. package/dist/index.d.mts +2427 -1893
  3. package/dist/index.d.ts +2427 -1893
  4. package/dist/index.js +1776 -566
  5. package/dist/index.mjs +1773 -566
  6. package/package.json +79 -81
package/README.md CHANGED
@@ -1,518 +1,726 @@
1
- <div style="font-family: Arial, sans-serif; border: 1px solid #fab788; border-radius: 15px; padding: 25px; ">
2
-
3
- <h1 align="center" style="color: #fab788; border-bottom: 2px solid #3498db; padding-bottom: 10px;">Lavalink Client</h1>
4
- <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>
5
-
6
- <div align="center">
7
- <p>
8
- <img src="https://madewithlove.now.sh/at?heart=true&template=for-the-badge" alt="Made with love in Austria">
9
- <img alt="Made with TypeScript" src="https://img.shields.io/badge/typescript-%23007ACC.svg?style=for-the-badge&logo=typescript&logoColor=white">
10
- </p>
11
- <p>
12
- <a href="https://www.npmjs.com/package/lavalink-client">
13
- <img src="https://img.shields.io/npm/v/lavalink-client.svg?maxAge=3600&style=for-the-badge&logo=npm&logoColor=red" alt="NPM Version" />
14
- </a>
15
- <a href="https://www.npmjs.com/package/lavalink-client">
16
- <img src="https://img.shields.io/npm/dt/lavalink-client.svg?maxAge=3600&style=for-the-badge&logo=npm&logoColor=red" alt="NPM Downloads" />
17
- </a>
18
- <a href="https://tomato6966.github.io/lavalink-client/">
19
- <img src="https://img.shields.io/badge/Documentation-%230288D1.svg?style=for-the-badge&logo=gitbook&logoColor=white" alt="Get Started Now">
20
- </a>
21
- </p>
22
- <p>
23
- <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>
24
- </p>
25
- </div>
26
-
27
- ***
28
-
29
- ## 🚀 Features
30
- - 💯 **Lavalink v4 Native:** Full support for Lavalink v4, including its powerful plugin ecosystem.
31
- - **Detailed Player-Destroy Reasons:** Understand precisely why a player was destroyed (e.g., channel deleted, bot disconnected).
32
- - **Flexible Queue Stores:** Use the default in-memory store or bring your own (Redis, databases, etc.) to sync queues across multiple processes.
33
- - 🎶 **Unresolved Tracks:** Supports unresolved track objects, fetching full data only when a track is about to play, saving API requests and resources.
34
- - 🎚️ **Built-in Filters & EQ:** Easy-to-use management for audio filters and equalizers.
35
- - 🔍 **Advanced Queue Filtering:** Search and filter tracks in the queue by title, author, duration, and more with powerful query options.
36
- - ⚙️ **Advanced Player Options:** Fine-tune player behavior for disconnects, empty queues, volume handling, and more.
37
- - 🛡️ **Lavalink-Side Validation:** Ensures you only use filters, plugins, and sources that your Lavalink node actually supports.
38
- - 🔒 **Client-Side Validation:** Whitelist and blacklist URLs or domains to prevent unwanted requests and protect your bot.
39
- - 🧑‍💻 **Developer-Friendly:** A memory-efficient design with a clean, intuitive API that mirrors Lavalink's own implementation.
40
- - 🤖 **Automated Handling:** Automatically handles track skipping on errors, voice channel deletions, server-wide mutes, and much more.
41
-
42
- ***
43
-
44
- ## 📦 Installation
45
-
46
- **Latest Stable Version: `v2.5.x`**
47
-
48
- <details>
49
- <summary><strong>👉 via NPM</strong></summary>
50
-
51
- ```bash
52
- # Stable (install release)
53
- npm install --save lavalink-client
54
-
55
- # Development (Install github dev-branch)
56
- npm install --save tomato6966/lavalink-client
57
- ```
58
-
59
- </details>
60
-
61
- <details>
62
- <summary><strong>👉 via YARN</strong></summary>
63
-
64
- ```bash
65
- # Stable (install release)
66
- yarn add lavalink-client
67
-
68
- # Development (Install github dev-branch)
69
- yarn add tomato6966/lavalink-client
70
- ```
71
-
72
- </details>
73
-
74
- <details>
75
- <summary><strong>👉 via BUN</strong></summary>
76
-
77
- ```bash
78
- # Stable (install release)
79
- bun add lavalink-client
80
-
81
- # Development (Install github dev-branch)
82
- bun add tomato6966/lavalink-client
83
- ```
84
-
85
- </details>
86
-
87
- <details>
88
- <summary><strong>👉 via pnpm</strong></summary>
89
-
90
- ```bash
91
- # Stable (install release)
92
- pnpm add lavalink-client
93
-
94
- # Development (Install github dev-branch)
95
- pnpm add tomato6966/lavalink-client
96
- ```
97
-
98
- </details>
99
-
100
- ## 📖 Documentation & Guides
101
-
102
- - **[Full Documentation](https://tomato6966.github.io/lavalink-client/)** - Your starting point for everything.
103
- - **[Manager Events](https://tomato6966.github.io/lavalink-client/extra/manager-events)** - Handle track, player, and general client events.
104
- - **[NodeManager Events](https://tomato6966.github.io/lavalink-client/extra/node-events)** - Manage node connections, errors, and logs.
105
- - **[Session Resuming Guide](https://tomato6966.github.io/lavalink-client/extra/resuming)** - Learn how to implement session resuming for seamless restarts.
106
-
107
- ***
108
-
109
- # Node Link
110
-
111
- 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:
112
-
113
- ```ts
114
- nodeOptions.heartBeatInterval = -1
115
- nodeOptions.enablePingOnStatsCheck = false
116
- ```
117
-
118
- this can be done directly when creating the node in the lavalinkmanager.
119
-
120
- ```ts
121
- client.lavalink = new LavalinkManager({
122
- nodes: [
123
- {
124
- host: "localhost",
125
-
126
- heartBeatInterval: -1,
127
- enablePingOnStatsCheck: false,
128
- }
129
- ]
130
- })
131
- client.lavalink = new LavalinkManager({
132
- nodes: [
133
- {
134
- authorization: "youshallnotpass", // The password for your Lavalink server
135
- host: "localhost",
136
- port: 2333,
137
- id: "Main Node",
138
- // DISABLE HEARTBEAT CHECKS AND regular PING CHECKS IN ORDER TO HAVE A STABLE CONNECTION TO NODELINK
139
- heartBeatInterval: -1,
140
- enablePingOnStatsCheck: false,
141
- }
142
- ],
143
- // A function to send voice server updates to the Lavalink client
144
- sendToShard: (guildId, payload) => {
145
- const guild = client.guilds.cache.get(guildId);
146
- if (guild) guild.shard.send(payload);
147
- },
148
- autoSkip: true,
149
- client: {
150
- id: process.env.CLIENT_ID, // Your bot's user ID
151
- username: "MyBot",
152
- },
153
- });
154
- ```
155
-
156
- ***
157
-
158
- ## 💖 Used In
159
- This client powers various Discord bots:
160
- - **[Mivator](https://discord.gg/5dUb7M2qCj)** (Public Bot by @Tomato6966)
161
- - **[Betty](https://betty.bot/?utm_source=lavalink-client)** (Public Bot by @fb_sean)
162
- - **[Nero](https://betty.bot/?utm_source=lavalink-client)** (Public Bot by @fb_sean)
163
- - **Bots by Contributors:**
164
- - [Mintone](https://mintone.tech/) (@appujet)
165
- - [Stelle](https://github.com/Ganyu-Studios/stelle-music) (@EvilG-MC)
166
- - [Panais](https://panais.xyz/) (@LucasB25)
167
- - [Akyn](https://akynbot.vercel.app/) (@notdeltaxd)
168
- - [ARINO](https://site.arinoapp.qzz.io/) (@ryanwtf88)
169
- - [iHorizon](https://github.com/ihrz/ihrz) (@iHorizon)
170
- - **Bots By Community (Users):**
171
- - [Soundy](https://github.com/idMJA/Soundy) (@idMJA)
172
- - [BeatBot](https://getbeatbot.vercel.app/) (@zenitsujs)
173
- - [Atom Music](https://top.gg/bot/1320469557411971165) (@sakshamyep)
174
- - [All Time Bot](https://top.gg/bot/1163027457671180418) (@PeterGamez)
175
- - [BeatDock](https://github.com/lazaroagomez/BeatDock) (@lazaroagomez)
176
- - [Nazha](https://top.gg/bot/1124681788070055967) (@Nazha-Team)
177
-
178
- ***
179
-
180
- ## 🛠️ Configuration Examples
181
-
182
- ### Basic Setup
183
- A minimal example to get you started quickly.
184
- ```typescript
185
- import { LavalinkManager } from "lavalink-client";
186
- import { Client, GatewayIntentBits } from "discord.js"; // example for a discord bot
187
-
188
- // Extend the Client type to include the lavalink manager
189
- declare module "discord.js" {
190
- interface Client {
191
- lavalink: LavalinkManager;
192
- }
193
- }
194
-
195
- const client = new Client({
196
- intents: [
197
- GatewayIntentBits.Guilds,
198
- GatewayIntentBits.GuildVoiceStates,
199
- ]
200
- });
201
-
202
- client.lavalink = new LavalinkManager({
203
- nodes: [
204
- {
205
- authorization: "youshallnotpass", // The password for your Lavalink server
206
- host: "localhost",
207
- port: 2333,
208
- id: "Main Node",
209
- }
210
- ],
211
- // A function to send voice server updates to the Lavalink client
212
- sendToShard: (guildId, payload) => {
213
- const guild = client.guilds.cache.get(guildId);
214
- if (guild) guild.shard.send(payload);
215
- },
216
- autoSkip: true,
217
- client: {
218
- id: process.env.CLIENT_ID, // Your bot's user ID
219
- username: "MyBot",
220
- },
221
- });
222
-
223
- // Listen for the 'raw' event from discord.js and forward it
224
- client.on("raw", (d) => client.lavalink.sendRawData(d));
225
-
226
- client.on("ready", () => {
227
- console.log(`Logged in as ${client.user.tag}!`);
228
- // Initialize the Lavalink client
229
- client.lavalink.init({ ...client.user });
230
- });
231
-
232
- client.login(process.env.DISCORD_TOKEN);
233
- ```
234
-
235
- <details>
236
- <summary><strong>🔩 Complete Configuration Example (almost all Options)</strong></summary>
237
-
238
- ```typescript
239
- import { LavalinkManager, QueueChangesWatcher, QueueStoreManager, StoredQueue } from "lavalink-client";
240
- import { RedisClientType, createClient } from "redis";
241
- import { Client, GatewayIntentBits, User } from "discord.js";
242
-
243
- // It's recommended to extend the Client type
244
- declare module "discord.js" {
245
- interface Client {
246
- lavalink: LavalinkManager;
247
- redis: RedisClientType;
248
- }
249
- }
250
-
251
- const client = new Client({
252
- intents: [
253
- GatewayIntentBits.Guilds,
254
- GatewayIntentBits.GuildVoiceStates,
255
- ]
256
- });
257
-
258
- client.lavalink = new LavalinkManager({
259
- nodes: [
260
- {
261
- authorization: "youshallnotpass",
262
- host: "localhost",
263
- port: 2333,
264
- id: "testnode",
265
- secure: false, // Set to true for wss://
266
- retryAmount: 5,
267
- retryDelay: 10_000, // 10 seconds
268
- }
269
- ],
270
- sendToShard: (guildId, payload) => client.guilds.cache.get(guildId)?.shard?.send(payload),
271
- autoSkip: true, // automatically play the next song of the queue, on: trackend, trackerror, trackexception
272
- client: {
273
- id: process.env.CLIENT_ID,
274
- username: "TESTBOT",
275
- },
276
- playerOptions: {
277
- applyVolumeAsFilter: false,
278
- clientBasedPositionUpdateInterval: 50,
279
- defaultSearchPlatform: "ytmsearch",
280
- volumeDecrementer: 0.75,
281
- onDisconnect: {
282
- autoReconnect: true,
283
- destroyPlayer: false,
284
- },
285
- onEmptyQueue: {
286
- destroyAfterMs: 30_000,
287
- // function get's called onqueueempty, and if there are songs added to the queue, it continues playing. if not then not (autoplay functionality)
288
- // autoPlayFunction: async (player) => { /* ... */ },
289
- },
290
- useUnresolvedData: true,
291
- },
292
- queueOptions: {
293
- maxPreviousTracks: 10,
294
- queueStore: new MyCustomRedisStore(client.redis),
295
- queueChangesWatcher: new MyCustomQueueWatcher(client),
296
- },
297
- // Whitelist/Blacklist links or words
298
- linksAllowed: true,
299
- linksBlacklist: ["somebadsite.com"],
300
- linksWhitelist: [],
301
- advancedOptions: {
302
- debugOptions: {
303
- noAudio: false,
304
- playerDestroy: { dontThrowError: false, debugLog: false },
305
- }
306
- }
307
- });
308
-
309
- client.on("raw", d => client.lavalink.sendRawData(d));
310
- client.on("ready", () => client.lavalink.init({ ...client.user }));
311
-
312
- // Example Custom Redis Queue Store
313
- class MyCustomRedisStore implements QueueStoreManager {
314
- private redis: RedisClientType;
315
- constructor(redisClient: RedisClientType) {
316
- this.redis = redisClient;
317
- }
318
- private key(guildId: string) { return `lavalinkqueue_${guildId}`; }
319
- async get(guildId: string) { return await this.redis.get(this.key(guildId)); }
320
- async set(guildId: string, data: string) { return await this.redis.set(this.key(guildId), data); }
321
- async delete(guildId: string) { return await this.redis.del(this.key(guildId)); }
322
- async parse(data: string): Promise<Partial<StoredQueue>> { return JSON.parse(data); }
323
- stringify(data: Partial<StoredQueue>): string { return JSON.stringify(data); }
324
- }
325
-
326
- // Example Custom Queue Watcher
327
- class MyCustomQueueWatcher implements QueueChangesWatcher {
328
- private client: Client;
329
- constructor(client: Client) { this.client = client; }
330
- shuffled(guildId: string) { console.log(`Queue shuffled in guild: ${guildId}`); }
331
- tracksAdd(guildId: string, tracks: any[], position: number) { console.log(`${tracks.length} tracks added at position ${position} in guild: ${guildId}`); }
332
- tracksRemoved(guildId: string, tracks: any[], position: number) { console.log(`${tracks.length} tracks removed at position ${position} in guild: ${guildId}`); }
333
- }
334
- ```
335
-
336
- </details>
337
-
338
- ***
339
-
340
- ## 📢 Events
341
- Listen to events to create interactive and responsive logic.
342
-
343
- ### Lavalink Manager Events
344
- These events are emitted from the main `LavalinkManager` instance and relate to players and tracks.
345
-
346
- - `playerCreate (player)`
347
- - `playerDestroy (player, reason)`
348
- - `playerDisconnect (player, voiceChannelId)`
349
- - `playerMove (player, oldChannelId, newChannelId)`
350
- - `trackStart (player, track)`
351
- - `trackEnd (player, track)`
352
- - `trackStuck (player, track, payload)`
353
- - `trackError (player, track, payload)`
354
- - `queueEnd (player)`
355
-
356
- <details>
357
- <summary><strong>📢 Example for Manager-Event-Listeners</strong></summary>
358
-
359
- ```javascript
360
- // Example: Listening to a track start event
361
- client.lavalink.on("trackStart", (player, track) => {
362
- const channel = client.channels.cache.get(player.textChannelId);
363
- if(channel) channel.send(`Now playing: ${track.info.title}`);
364
- });
365
-
366
- // Example: Handling queue end
367
- client.lavalink.on("queueEnd", (player) => {
368
- const channel = client.channels.cache.get(player.textChannelId);
369
- if(channel) channel.send("The queue has finished. Add more songs!");
370
- player.destroy();
371
- });
372
- ```
373
- </details>
374
-
375
- ### Node Manager Events
376
- These events are emitted from `lavalink.nodeManager` and relate to the Lavalink node connections.
377
-
378
- - `create (node)`
379
- - `connect (node)`
380
- - `disconnect (node, reason)`
381
- - `reconnecting (node)`
382
- - `destroy (node)`
383
- - `error (node, error, payload)`
384
- - `resumed (node, payload, players)`
385
-
386
- <details>
387
- <summary><strong>📢 Example for Node-Event-Listeners</strong></summary>
388
-
389
- ```javascript
390
- // Example: Logging node connections and errors
391
- client.lavalink.nodeManager.on("connect", (node) => {
392
- console.log(`Node "${node.id}" connected!`);
393
- });
394
-
395
- client.lavalink.nodeManager.on("error", (node, error) => {
396
- console.error(`Node "${node.id}" encountered an error:`, error.message);
397
- });
398
- ```
399
- </details>
400
-
401
- ***
402
-
403
- ## 📚 Advanced How-To Guides
404
-
405
- ### How to Implement Session Resuming
406
- Resuming allows your music bot to continue playback even after a restart.
407
-
408
- 1. **Enable Resuming on the Node:** When a node connects, enable resuming with a timeout.
409
- 2. **Listen for the `resumed` Event:** This event fires on a successful reconnect, providing all player data from Lavalink.
410
- 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.
411
-
412
- > 💡 **For a complete, working example, see the [official test bot's implementation](https://github.com/Tomato6966/lavalink-client/blob/main/testBot/Utils/handleResuming.ts).**
413
-
414
- <details>
415
- <summary><strong>💡 Principle of how to enable **resuming**</strong></summary>
416
-
417
- ```javascript
418
- // 1. Enable resuming on connect
419
- client.lavalink.nodeManager.on("connect", (node) => {
420
- // Enable resuming for 5 minutes (300,000 ms)
421
- node.updateSession(true, 300_000);
422
- });
423
-
424
- // 2. Listen for the resumed event
425
- client.lavalink.nodeManager.on("resumed", async (node, payload, fetchedPlayers) => {
426
- console.log(`Node "${node.id}" successfully resumed with ${fetchedPlayers.length} players.`);
427
-
428
- for (const lavalinkData of fetchedPlayers) {
429
- // 3. Get your saved data (e.g., from Redis/DB)
430
- const savedData = await getFromDatabase(lavalinkData.guildId);
431
- if (!savedData || !lavalinkData.state.connected) {
432
- if(savedData) await deleteFromDatabase(lavalinkData.guildId);
433
- continue; // Skip if no saved data or Lavalink reports disconnected
434
- }
435
-
436
- // Re-create the player instance
437
- const player = client.lavalink.createPlayer({
438
- guildId: lavalinkData.guildId,
439
- voiceChannelId: savedData.voiceChannelId,
440
- textChannelId: savedData.textChannelId,
441
- // Important: Use the same node that was resumed
442
- node: node.id,
443
- // Set volume from Lavalink's data, accounting for the volume decrementer
444
- volume: lavalinkData.volume,
445
- selfDeaf: savedData.selfDeaf,
446
- });
447
-
448
- // Re-establish voice connection
449
- await player.connect();
450
-
451
- // Restore player state
452
- player.paused = lavalinkData.paused;
453
- player.lastPosition = lavalinkData.state.position;
454
- player.filterManager.data = lavalinkData.filters;
455
-
456
- // Restore the queue
457
- await player.queue.utils.sync(true, false); // Syncs with your QueueStore
458
-
459
- // Restore the current track
460
- if (lavalinkData.track) {
461
- player.queue.current = client.lavalink.utils.buildTrack(lavalinkData.track, savedData.requester);
462
- }
463
- }
464
- });
465
-
466
- // Persist player data on updates to use for resuming later
467
- client.lavalink.on("playerUpdate", (oldPlayer, newPlayer) => {
468
- saveToDatabase(newPlayer.toJSON());
469
- });
470
-
471
- // Clean up data when a player is permanently destroyed
472
- client.lavalink.on("playerDestroy", (player) => {
473
- deleteFromDatabase(player.guildId);
474
- });
475
- ```
476
- </details>
477
-
478
- ### How to Use Plugins
479
- Lavalink client supports most of the major lavalink-plugins.
480
- The client itself is - for beginner friendly reasons - atm not extendable (via plugins)
481
- 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.
482
-
483
- Some plugins require extra-parameters, such as flowerytts:
484
- Pass extra parameters to the search function to use plugin-specific features.
485
-
486
- <details>
487
- <summary><strong>How to use the flowerytts plugin</strong></summary>
488
-
489
- ```javascript
490
- // Example for flowertts plugin
491
- const query = interaction.options.getString("text");
492
- const voice = interaction.options.getString("voice"); // e.g., "MALE_1"
493
-
494
- const extraParams = new URLSearchParams();
495
- if (voice) extraParams.append(`voice`, voice);
496
-
497
- // All params for flowertts can be found here: https://flowery.pw/docs
498
- const response = await player.search(
499
- {
500
- query: `${query}`,
501
- // This is used by plugins like ftts to adjust the request
502
- extraQueryUrlParams: extraParams,
503
- source: "ftts" // Specify the plugin source
504
- },
505
- interaction.user // The requester
506
- );
507
-
508
- // Add the TTS track to the queue
509
- if (response.tracks.length > 0) {
510
- player.queue.add(response.tracks[0]);
511
- if (!player.playing) player.play();
512
- }
513
- ```
514
- </details>
515
-
516
- </div>
517
-
518
-
1
+ <div style="font-family: Arial, sans-serif; border: 1px solid #fab788; border-radius: 15px; padding: 25px; ">
2
+
3
+ <h1 align="center" style="color: #fab788; border-bottom: 2px solid #3498db; padding-bottom: 10px;">Lavalink Client</h1>
4
+ <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>
5
+
6
+ <div align="center">
7
+ <p>
8
+ <img src="https://madewithlove.now.sh/at?heart=true&template=for-the-badge" alt="Made with love in Austria">
9
+ <img alt="Made with TypeScript" src="https://img.shields.io/badge/typescript-%23007ACC.svg?style=for-the-badge&logo=typescript&logoColor=white">
10
+ </p>
11
+ <p>
12
+ <a href="https://www.npmjs.com/package/lavalink-client">
13
+ <img src="https://img.shields.io/npm/v/lavalink-client.svg?maxAge=3600&style=for-the-badge&logo=npm&logoColor=red" alt="NPM Version" />
14
+ </a>
15
+ <a href="https://www.npmjs.com/package/lavalink-client">
16
+ <img src="https://img.shields.io/npm/dt/lavalink-client.svg?maxAge=3600&style=for-the-badge&logo=npm&logoColor=red" alt="NPM Downloads" />
17
+ </a>
18
+ <a href="https://tomato6966.github.io/lavalink-client/">
19
+ <img src="https://img.shields.io/badge/Documentation-%230288D1.svg?style=for-the-badge&logo=gitbook&logoColor=white" alt="Get Started Now">
20
+ </a>
21
+ </p>
22
+ <p>
23
+ <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>
24
+ </p>
25
+ </div>
26
+
27
+ ---
28
+
29
+ ## 🚀 Features
30
+
31
+ - 💯 **Lavalink v4 Native:** Full support for Lavalink v4, including its powerful plugin ecosystem.
32
+ - **Detailed Player-Destroy Reasons:** Understand precisely why a player was destroyed (e.g., channel deleted, bot disconnected).
33
+ - **Flexible Queue Stores:** Use the default in-memory store or bring your own (Redis, databases, etc.) to sync queues across multiple processes.
34
+ - 🎶 **Unresolved Tracks:** Supports unresolved track objects, fetching full data only when a track is about to play, saving API requests and resources.
35
+ - 🎚️ **Built-in Filters & EQ:** Easy-to-use management for audio filters and equalizers.
36
+ - 🔍 **Advanced Queue Filtering:** Search and filter tracks in the queue by title, author, duration, and more with powerful query options.
37
+ - ⚙️ **Advanced Player Options:** Fine-tune player behavior for disconnects, empty queues, volume handling, and more.
38
+ - 🛡️ **Lavalink-Side Validation:** Ensures you only use filters, plugins, and sources that your Lavalink node actually supports.
39
+ - 🔒 **Client-Side Validation:** Whitelist and blacklist URLs or domains to prevent unwanted requests and protect your bot.
40
+ - 🧑‍💻 **Developer-Friendly:** A memory-efficient design with a clean, intuitive API that mirrors Lavalink's own implementation.
41
+ - 🤖 **Automated Handling:** Automatically handles track skipping on errors, voice channel deletions, server-wide mutes, and much more.
42
+
43
+ ---
44
+
45
+ ## 📦 Installation
46
+
47
+ **Latest Stable Version: `v2.5.x`**
48
+
49
+ <details>
50
+ <summary><strong>👉 via NPM</strong></summary>
51
+
52
+ ```bash
53
+ # Stable (install release)
54
+ npm install --save lavalink-client
55
+
56
+ # Development (Install github dev-branch)
57
+ npm install --save tomato6966/lavalink-client
58
+ ```
59
+
60
+ </details>
61
+
62
+ <details>
63
+ <summary><strong>👉 via YARN</strong></summary>
64
+
65
+ ```bash
66
+ # Stable (install release)
67
+ yarn add lavalink-client
68
+
69
+ # Development (Install github dev-branch)
70
+ yarn add tomato6966/lavalink-client
71
+ ```
72
+
73
+ </details>
74
+
75
+ <details>
76
+ <summary><strong>👉 via BUN</strong></summary>
77
+
78
+ ```bash
79
+ # Stable (install release)
80
+ bun add lavalink-client
81
+
82
+ # Development (Install github dev-branch)
83
+ bun add tomato6966/lavalink-client
84
+ ```
85
+
86
+ </details>
87
+
88
+ <details>
89
+ <summary><strong>👉 via pnpm</strong></summary>
90
+
91
+ ```bash
92
+ # Stable (install release)
93
+ pnpm add lavalink-client
94
+
95
+ # Development (Install github dev-branch)
96
+ pnpm add tomato6966/lavalink-client
97
+ ```
98
+
99
+ </details>
100
+
101
+ ## 📖 Documentation & Guides
102
+
103
+ - **[Full Documentation](https://tomato6966.github.io/lavalink-client/)** - Your starting point for everything.
104
+ - **[Manager Events](https://tomato6966.github.io/lavalink-client/extra/manager-events)** - Handle track, player, and general client events.
105
+ - **[NodeManager Events](https://tomato6966.github.io/lavalink-client/extra/node-events)** - Manage node connections, errors, and logs.
106
+ - **[Session Resuming Guide](https://tomato6966.github.io/lavalink-client/extra/resuming)** - Learn how to implement session resuming for seamless restarts.
107
+
108
+ ---
109
+
110
+ # Node Link
111
+
112
+ 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:
113
+
114
+ ```ts
115
+ nodeOptions.nodeType = "NodeLink";
116
+ ```
117
+
118
+ this can be done directly when creating the node in the lavalinkmanager.
119
+
120
+ ```ts
121
+ client.lavalink = new LavalinkManager({
122
+ nodes: [
123
+ {
124
+ host: "localhost",
125
+ nodeType: "NodeLink",
126
+ },
127
+ ],
128
+ });
129
+ // or here if you need a bigger example.
130
+ client.lavalink = new LavalinkManager({
131
+ nodes: [
132
+ {
133
+ authorization: "youshallnotpass", // The password for your Lavalink server
134
+ host: "localhost",
135
+ port: 2333,
136
+ id: "Main Node",
137
+ // set to nodeLink
138
+ nodeType: "NodeLink",
139
+ },
140
+ ],
141
+ // A function to send voice server updates to the Lavalink client
142
+ sendToShard: (guildId, payload) => {
143
+ const guild = client.guilds.cache.get(guildId);
144
+ if (guild) guild.shard.send(payload);
145
+ },
146
+ autoSkip: true,
147
+ client: {
148
+ id: process.env.CLIENT_ID, // Your bot's user ID
149
+ username: "MyBot",
150
+ },
151
+ });
152
+ ```
153
+
154
+ Now if you want to use NodeLink specific functions, you can use the type assertion checker function:
155
+
156
+ ```ts
157
+ if (node.isNodeLink()) {
158
+ // node is now typed as NodeLink
159
+ node.addMixerLayer();
160
+ } else if (node.isLavalinkNode()) {
161
+ // node is now typed as LavalinkNode
162
+ } else {
163
+ // node is now typed as whatever it is..
164
+ }
165
+ ```
166
+
167
+ or you have to assert the type...
168
+
169
+ ```
170
+ const node = client.lavalink.lavalinkManager.getNode("id") as NodeLinkNode;
171
+ node.addMixerLayer()
172
+ ```
173
+
174
+ ### NodeLink Specific Methods
175
+
176
+ - **`node.getYoutubeOAUTH(refreshToken)`**: Exchange a Refresh Token for an Access Token. [Docs](https://nodelink.js.org/docs/api/nodelink-features#oauth)
177
+ - **`node.updateYoutubeOAUTH(refreshToken)`**: Update the OAUTH token. [Docs](https://nodelink.js.org/docs/api/nodelink-features#oauth)
178
+ - **`node.getChapters(player, track?)`**: Retrieve Chapters of Youtube Videos. [Docs](https://nodelink.js.org/docs/api/nodelink-features#loadchapters)
179
+ - **`node.nodeLinkLyrics(player, track?, language?)`**: Retrieve Lyrics of Youtube Videos. [Docs](https://nodelink.js.org/docs/api/nodelink-features#lyrics--chapters)
180
+ - **`node.getDirectStream(track)`**: Stream audio directly from NodeLink. [Docs](https://nodelink.js.org/docs/api/nodelink-features#direct-streaming)
181
+ - **`node.listMixerLayers(player)`**: Retrieves a list of currently active mix layers. [Docs](https://nodelink.js.org/docs/api/rest#get-active-mixes)
182
+ - **`node.addMixerLayer(player, track, volume)`**: Adds a new audio track to be mixed. [Docs](https://nodelink.js.org/docs/api/rest#add-mix-layer)
183
+ - **`node.removeMixerLayer(player, mixId)`**: Removes a specific mix layer. [Docs](https://nodelink.js.org/docs/api/rest#remove-mix-layer)
184
+ - **`node.updateMixerLayerVolume(player, mixId, volume)`**: Updates the volume of a specific mix layer. [Docs](https://nodelink.js.org/docs/api/rest#update-mix-volume)
185
+ - **`node.changeAudioTrackLanguage(player, language_audioTrackId)`**: Changes the current language of the audio. [Docs](https://nodelink.js.org/docs/api/nodelink-features#additional-filters)
186
+ - **`node.updateYoutubeConfig(refreshToken?, visitorData?)`**: Updates the YouTube configuration. [Docs](https://nodelink.js.org/docs/api/nodelink-features#update-config)
187
+ - **`node.getYoutubeConfig(validate?)`**: Gets the YouTube configuration.
188
+ - **`node.getConnectionMetrics()`**: Get connection metrics. [Docs](https://nodelink.js.org/docs/api/rest#node-information)
189
+ - **`node.loadDirectStream(track, volume, position, filters)`**: Stream raw PCM audio. [Docs](https://nodelink.js.org/docs/api/nodelink-features#loadstream)
190
+
191
+ ### NodeLink Specififc Events?
192
+
193
+ ```ts
194
+ // NodeLink specific events
195
+ client.lavalink.nodeManager.on("nodeLinkEvent", (node, eventName, player, track, payload) => {
196
+ switch (eventName) {
197
+ // -------------Player LifeCycle Events-------------
198
+ // https://nodelink.js.org/docs/api/websocket#playercreatedevent
199
+ case "PlayerCreatedEvent":
200
+ {
201
+ // { "guildId": "987654321098765432", "track": null, "paused": false, "volume": 100 }
202
+ const playerInfo = payload.player;
203
+ console.log(`Player created in guildId: ${playerInfo.guildId}`);
204
+ }
205
+ break;
206
+ // https://nodelink.js.org/docs/api/websocket#playerdestroyedevent
207
+ case "PlayerDestroyedEvent":
208
+ {
209
+ // "987654321098765432"
210
+ const playerInfo = payload.guildId;
211
+ console.log(`Player destroyed in guildId: ${playerInfo.guildId}`);
212
+ }
213
+ break;
214
+ // https://nodelink.js.org/docs/api/websocket#playerconnectedevent
215
+ case "PlayerConnectedEvent":
216
+ {
217
+ // { "sessionId": "abc", "token": "token", "endpoint": "us-central123.discord.media", "channelId": "123456789012345678" }
218
+ const playerInfo = payload.voice;
219
+ console.log(`Player connected in guildId: ${playerInfo.guildId}`);
220
+ }
221
+ break;
222
+ // https://nodelink.js.org/docs/api/websocket#playerreconnectingevent
223
+ case "PlayerReconnectingEvent":
224
+ {
225
+ // { "sessionId": "abc", "token": "token", "endpoint": "us-central123.discord.media", "channelId": "123456789012345678" }
226
+ const playerInfo = payload.voice;
227
+ console.log(`Player reconnecting in guildId: ${playerInfo.guildId}`);
228
+ }
229
+ break;
230
+
231
+ // -------------Player State Events-------------
232
+ // https://nodelink.js.org/docs/api/websocket#volumechangedevent
233
+ case "VolumeChangedEvent":
234
+ {
235
+ // "guildId": "987654321098765432",
236
+ // "volume": 100
237
+ const { guildId, volume } = payload;
238
+ console.log(`Player volume changed in guildId: ${guildId} to ${volume}`);
239
+ }
240
+ break;
241
+ // https://nodelink.js.org/docs/api/websocket#filterschangedevent
242
+ case "FiltersChangedEvent":
243
+ {
244
+ // { ...Filtersdata... }
245
+ const { guildId, filters } = payload;
246
+ console.log(`Player filters changed in guildId: ${guildId}, new Data: `, filters);
247
+ }
248
+ break;
249
+ // https://nodelink.js.org/docs/api/websocket#seekevent
250
+ case "SeekEvent":
251
+ {
252
+ // "guildId": "987654321098765432",
253
+ // "position": 10000,
254
+ const { guildId, position } = payload;
255
+ console.log(`Player seeked in guildId: ${guildId}, new position: ${position}`);
256
+ }
257
+ break;
258
+ // https://nodelink.js.org/docs/api/websocket#pauseevent
259
+ case "PauseEvent":
260
+ {
261
+ // "guildId": "987654321098765432",
262
+ // "paused": true
263
+ const { guildId, paused } = payload;
264
+ console.log(`Player paused in guildId: ${guildId}, paused true/false: ${paused}`);
265
+ }
266
+ break;
267
+ // https://nodelink.js.org/docs/api/websocket#connectionstatusevent
268
+ case "ConnectionStatusEvent":
269
+ {
270
+ // "guildId": "987654321098765432",
271
+ // "status": "CONNECTED"
272
+ // "metrics": { ... }
273
+ const { guildId, status, metrics } = payload;
274
+ console.log(
275
+ `Player connection status changed in guildId: ${guildId}, status: ${status}, metrics: `,
276
+ metrics,
277
+ );
278
+ }
279
+ break;
280
+
281
+ // -------------LYRICS EVENTS-------------
282
+ // https://nodelink.js.org/docs/api/websocket#lyrics-events
283
+ case "LyricsFoundEvent":
284
+ {
285
+ // "guildId": "987654321098765432",
286
+ // "lyrics": "..."
287
+ const { guildId, lyrics } = payload;
288
+ console.log(`Lyrics found in guildId: ${guildId}, lyrics: `, lyrics);
289
+ }
290
+ break;
291
+ // https://nodelink.js.org/docs/api/websocket#lyricslineevent
292
+ case "LyricsLineEvent":
293
+ {
294
+ // "guildId": "987654321098765432",
295
+ // "lineIndex": 0,
296
+ // "line": "..."
297
+ const { guildId, lineIndex, line } = payload;
298
+ console.log(`Lyrics line in guildId: ${guildId}, lineIndex: ${lineIndex}, line: `, line);
299
+ }
300
+ break;
301
+ // https://nodelink.js.org/docs/api/websocket#lyricsnotfoundevent
302
+ case "LyricsNotFoundEvent":
303
+ {
304
+ // "guildId": "987654321098765432",
305
+ const { guildId } = payload;
306
+ console.log(`Lyrics not found in guildId: ${guildId}`);
307
+ }
308
+ break;
309
+
310
+ // -------------AUDIO MIXER EVENTS-------------
311
+ // https://nodelink.js.org/docs/api/websocket#mixstartedevent
312
+ case "MixStartedEvent":
313
+ {
314
+ // "guildId": "987654321098765432",
315
+ // "mixId": "123456789012345678"
316
+ // "volume": 0.8,
317
+ // "track": { ...TrackData... }
318
+ const { guildId, mixId, volume, track } = payload.player;
319
+ console.log(
320
+ `Player mix started in guildId: ${guildId}, mixId: ${mixId}, volume: ${volume}, track: `,
321
+ track,
322
+ );
323
+ }
324
+ break;
325
+ // https://nodelink.js.org/docs/api/websocket#mixendedevent
326
+ case "MixEndedEvent":
327
+ {
328
+ // "guildId": "987654321098765432",
329
+ // "mixId": "123456789012345678",
330
+ // "reason": "USER_STOPPED"
331
+ const { guildId, mixId, reason } = payload;
332
+ console.log(`Player mix ended in guildId: ${guildId}, mixId: ${mixId}, reason: ${reason}`);
333
+ }
334
+ break;
335
+ }
336
+ });
337
+ ```
338
+
339
+ ---
340
+
341
+ ## 💖 Used In
342
+
343
+ This client powers various Discord bots:
344
+
345
+ - **[Mivator](https://discord.gg/5dUb7M2qCj)** (Public Bot by @Tomato6966)
346
+ - **[Betty](https://betty.bot/?utm_source=lavalink-client)** (Public Bot by @fb_sean)
347
+ - **[Nero](https://betty.bot/?utm_source=lavalink-client)** (Public Bot by @fb_sean)
348
+ - **Bots by Contributors:**
349
+ - [Mintone](https://mintone.tech/) (@appujet)
350
+ - [Stelle](https://github.com/Ganyu-Studios/stelle-music) (@EvilG-MC)
351
+ - [Panais](https://panais.xyz/) (@LucasB25)
352
+ - [Akyn](https://akynbot.vercel.app/) (@notdeltaxd)
353
+ - [ARINO](https://site.arinoapp.qzz.io/) (@ryanwtf88)
354
+ - [iHorizon](https://github.com/ihrz/ihrz) (@iHorizon)
355
+ - **Bots By Community (Users):**
356
+ - [Soundy](https://github.com/idMJA/Soundy) (@idMJA)
357
+ - [BeatBot](https://getbeatbot.vercel.app/) (@zenitsujs)
358
+ - [Atom Music](https://top.gg/bot/1320469557411971165) (@sakshamyep)
359
+ - [All Time Bot](https://top.gg/bot/1163027457671180418) (@PeterGamez)
360
+ - [BeatDock](https://github.com/lazaroagomez/BeatDock) (@lazaroagomez)
361
+ - [Nazha](https://top.gg/bot/1124681788070055967) (@Nazha-Team)
362
+
363
+ ---
364
+
365
+ ## 🛠️ Configuration Examples
366
+
367
+ ### Basic Setup
368
+
369
+ A minimal example to get you started quickly.
370
+
371
+ ```typescript
372
+ import { LavalinkManager } from "lavalink-client";
373
+ import { Client, GatewayIntentBits } from "discord.js"; // example for a discord bot
374
+
375
+ // Extend the Client type to include the lavalink manager
376
+ declare module "discord.js" {
377
+ interface Client {
378
+ lavalink: LavalinkManager;
379
+ }
380
+ }
381
+
382
+ const client = new Client({
383
+ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates],
384
+ });
385
+
386
+ client.lavalink = new LavalinkManager({
387
+ nodes: [
388
+ {
389
+ authorization: "youshallnotpass", // The password for your Lavalink server
390
+ host: "localhost",
391
+ port: 2333,
392
+ id: "Main Node",
393
+ },
394
+ ],
395
+ // A function to send voice server updates to the Lavalink client
396
+ sendToShard: (guildId, payload) => {
397
+ const guild = client.guilds.cache.get(guildId);
398
+ if (guild) guild.shard.send(payload);
399
+ },
400
+ autoSkip: true,
401
+ client: {
402
+ id: process.env.CLIENT_ID, // Your bot's user ID
403
+ username: "MyBot",
404
+ },
405
+ });
406
+
407
+ // Listen for the 'raw' event from discord.js and forward it
408
+ client.on("raw", (d) => client.lavalink.sendRawData(d));
409
+
410
+ client.on("ready", () => {
411
+ console.log(`Logged in as ${client.user.tag}!`);
412
+ // Initialize the Lavalink client
413
+ client.lavalink.init({ ...client.user });
414
+ });
415
+
416
+ client.login(process.env.DISCORD_TOKEN);
417
+ ```
418
+
419
+ <details>
420
+ <summary><strong>🔩 Complete Configuration Example (almost all Options)</strong></summary>
421
+
422
+ ```typescript
423
+ import { LavalinkManager, QueueChangesWatcher, QueueStoreManager, StoredQueue } from "lavalink-client";
424
+ import { RedisClientType, createClient } from "redis";
425
+ import { Client, GatewayIntentBits, User } from "discord.js";
426
+
427
+ // It's recommended to extend the Client type
428
+ declare module "discord.js" {
429
+ interface Client {
430
+ lavalink: LavalinkManager;
431
+ redis: RedisClientType;
432
+ }
433
+ }
434
+
435
+ const client = new Client({
436
+ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates],
437
+ });
438
+
439
+ client.lavalink = new LavalinkManager({
440
+ nodes: [
441
+ {
442
+ authorization: "youshallnotpass",
443
+ host: "localhost",
444
+ port: 2333,
445
+ id: "testnode",
446
+ secure: false, // Set to true for wss://
447
+ retryAmount: 5,
448
+ retryDelay: 10_000, // 10 seconds
449
+ },
450
+ ],
451
+ sendToShard: (guildId, payload) => client.guilds.cache.get(guildId)?.shard?.send(payload),
452
+ autoSkip: true, // automatically play the next song of the queue, on: trackend, trackerror, trackexception
453
+ client: {
454
+ id: process.env.CLIENT_ID,
455
+ username: "TESTBOT",
456
+ },
457
+ playerOptions: {
458
+ applyVolumeAsFilter: false,
459
+ clientBasedPositionUpdateInterval: 50,
460
+ defaultSearchPlatform: "ytmsearch",
461
+ volumeDecrementer: 0.75,
462
+ onDisconnect: {
463
+ autoReconnect: true,
464
+ destroyPlayer: false,
465
+ },
466
+ onEmptyQueue: {
467
+ destroyAfterMs: 30_000,
468
+ // function get's called onqueueempty, and if there are songs added to the queue, it continues playing. if not then not (autoplay functionality)
469
+ // autoPlayFunction: async (player) => { /* ... */ },
470
+ },
471
+ useUnresolvedData: true,
472
+ },
473
+ queueOptions: {
474
+ maxPreviousTracks: 10,
475
+ queueStore: new MyCustomRedisStore(client.redis),
476
+ queueChangesWatcher: new MyCustomQueueWatcher(client),
477
+ },
478
+ // Whitelist/Blacklist links or words
479
+ linksAllowed: true,
480
+ linksBlacklist: ["somebadsite.com"],
481
+ linksWhitelist: [],
482
+ advancedOptions: {
483
+ debugOptions: {
484
+ noAudio: false,
485
+ playerDestroy: { dontThrowError: false, debugLog: false },
486
+ },
487
+ },
488
+ });
489
+
490
+ client.on("raw", (d) => client.lavalink.sendRawData(d));
491
+ client.on("ready", () => client.lavalink.init({ ...client.user }));
492
+
493
+ // Example Custom Redis Queue Store
494
+ class MyCustomRedisStore implements QueueStoreManager {
495
+ private redis: RedisClientType;
496
+ constructor(redisClient: RedisClientType) {
497
+ this.redis = redisClient;
498
+ }
499
+ private key(guildId: string) {
500
+ return `lavalinkqueue_${guildId}`;
501
+ }
502
+ async get(guildId: string) {
503
+ return await this.redis.get(this.key(guildId));
504
+ }
505
+ async set(guildId: string, data: string) {
506
+ return await this.redis.set(this.key(guildId), data);
507
+ }
508
+ async delete(guildId: string) {
509
+ return await this.redis.del(this.key(guildId));
510
+ }
511
+ async parse(data: string): Promise<Partial<StoredQueue>> {
512
+ return JSON.parse(data);
513
+ }
514
+ stringify(data: Partial<StoredQueue>): string {
515
+ return JSON.stringify(data);
516
+ }
517
+ }
518
+
519
+ // Example Custom Queue Watcher
520
+ class MyCustomQueueWatcher implements QueueChangesWatcher {
521
+ private client: Client;
522
+ constructor(client: Client) {
523
+ this.client = client;
524
+ }
525
+ shuffled(guildId: string) {
526
+ console.log(`Queue shuffled in guild: ${guildId}`);
527
+ }
528
+ tracksAdd(guildId: string, tracks: any[], position: number) {
529
+ console.log(`${tracks.length} tracks added at position ${position} in guild: ${guildId}`);
530
+ }
531
+ tracksRemoved(guildId: string, tracks: any[], position: number) {
532
+ console.log(`${tracks.length} tracks removed at position ${position} in guild: ${guildId}`);
533
+ }
534
+ }
535
+ ```
536
+
537
+ </details>
538
+
539
+ ---
540
+
541
+ ## 📢 Events
542
+
543
+ Listen to events to create interactive and responsive logic.
544
+
545
+ ### Lavalink Manager Events
546
+
547
+ These events are emitted from the main `LavalinkManager` instance and relate to players and tracks.
548
+
549
+ - `playerCreate (player)`
550
+ - `playerDestroy (player, reason)`
551
+ - `playerDisconnect (player, voiceChannelId)`
552
+ - `playerMove (player, oldChannelId, newChannelId)`
553
+ - `trackStart (player, track)`
554
+ - `trackEnd (player, track)`
555
+ - `trackStuck (player, track, payload)`
556
+ - `trackError (player, track, payload)`
557
+ - `queueEnd (player)`
558
+
559
+ <details>
560
+ <summary><strong>📢 Example for Manager-Event-Listeners</strong></summary>
561
+
562
+ ```javascript
563
+ // Example: Listening to a track start event
564
+ client.lavalink.on("trackStart", (player, track) => {
565
+ const channel = client.channels.cache.get(player.textChannelId);
566
+ if (channel) channel.send(`Now playing: ${track.info.title}`);
567
+ });
568
+
569
+ // Example: Handling queue end
570
+ client.lavalink.on("queueEnd", (player) => {
571
+ const channel = client.channels.cache.get(player.textChannelId);
572
+ if (channel) channel.send("The queue has finished. Add more songs!");
573
+ player.destroy();
574
+ });
575
+ ```
576
+
577
+ </details>
578
+
579
+ ### Node Manager Events
580
+
581
+ These events are emitted from `lavalink.nodeManager` and relate to the Lavalink node connections.
582
+
583
+ - `create (node)`
584
+ - `connect (node)`
585
+ - `disconnect (node, reason)`
586
+ - `reconnecting (node)`
587
+ - `destroy (node)`
588
+ - `error (node, error, payload)`
589
+ - `resumed (node, payload, players)`
590
+
591
+ <details>
592
+ <summary><strong>📢 Example for Node-Event-Listeners</strong></summary>
593
+
594
+ ```javascript
595
+ // Example: Logging node connections and errors
596
+ client.lavalink.nodeManager.on("connect", (node) => {
597
+ console.log(`Node "${node.id}" connected!`);
598
+ });
599
+
600
+ client.lavalink.nodeManager.on("error", (node, error) => {
601
+ console.error(`Node "${node.id}" encountered an error:`, error.message);
602
+ });
603
+ ```
604
+
605
+ </details>
606
+
607
+ ---
608
+
609
+ ## 📚 Advanced How-To Guides
610
+
611
+ ### How to Implement Session Resuming
612
+
613
+ Resuming allows your music bot to continue playback even after a restart.
614
+
615
+ 1. **Enable Resuming on the Node:** When a node connects, enable resuming with a timeout.
616
+ 2. **Listen for the `resumed` Event:** This event fires on a successful reconnect, providing all player data from Lavalink.
617
+ 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.
618
+
619
+ > 💡 **For a complete, working example, see the [official test bot's implementation](https://github.com/Tomato6966/lavalink-client/blob/main/testBot/Utils/handleResuming.ts).**
620
+
621
+ <details>
622
+ <summary><strong>💡 Principle of how to enable **resuming**</strong></summary>
623
+
624
+ ```javascript
625
+ // 1. Enable resuming on connect
626
+ client.lavalink.nodeManager.on("connect", (node) => {
627
+ // Enable resuming for 5 minutes (300,000 ms)
628
+ node.updateSession(true, 300_000);
629
+ });
630
+
631
+ // 2. Listen for the resumed event
632
+ client.lavalink.nodeManager.on("resumed", async (node, payload, fetchedPlayers) => {
633
+ console.log(`Node "${node.id}" successfully resumed with ${fetchedPlayers.length} players.`);
634
+
635
+ for (const lavalinkData of fetchedPlayers) {
636
+ // 3. Get your saved data (e.g., from Redis/DB)
637
+ const savedData = await getFromDatabase(lavalinkData.guildId);
638
+ if (!savedData || !lavalinkData.state.connected) {
639
+ if (savedData) await deleteFromDatabase(lavalinkData.guildId);
640
+ continue; // Skip if no saved data or Lavalink reports disconnected
641
+ }
642
+
643
+ // Re-create the player instance
644
+ const player = client.lavalink.createPlayer({
645
+ guildId: lavalinkData.guildId,
646
+ voiceChannelId: savedData.voiceChannelId,
647
+ textChannelId: savedData.textChannelId,
648
+ // Important: Use the same node that was resumed
649
+ node: node.id,
650
+ // Set volume from Lavalink's data, accounting for the volume decrementer
651
+ volume: lavalinkData.volume,
652
+ selfDeaf: savedData.selfDeaf,
653
+ });
654
+
655
+ // Re-establish voice connection
656
+ await player.connect();
657
+
658
+ // Restore player state
659
+ player.paused = lavalinkData.paused;
660
+ player.lastPosition = lavalinkData.state.position;
661
+ player.filterManager.data = lavalinkData.filters;
662
+
663
+ // Restore the queue
664
+ await player.queue.utils.sync(true, false); // Syncs with your QueueStore
665
+
666
+ // Restore the current track
667
+ if (lavalinkData.track) {
668
+ player.queue.current = client.lavalink.utils.buildTrack(lavalinkData.track, savedData.requester);
669
+ }
670
+ }
671
+ });
672
+
673
+ // Persist player data on updates to use for resuming later
674
+ client.lavalink.on("playerUpdate", (oldPlayer, newPlayer) => {
675
+ saveToDatabase(newPlayer.toJSON());
676
+ });
677
+
678
+ // Clean up data when a player is permanently destroyed
679
+ client.lavalink.on("playerDestroy", (player) => {
680
+ deleteFromDatabase(player.guildId);
681
+ });
682
+ ```
683
+
684
+ </details>
685
+
686
+ ### How to Use Plugins
687
+
688
+ Lavalink client supports most of the major lavalink-plugins.
689
+ The client itself is - for beginner friendly reasons - atm not extendable (via plugins)
690
+ 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.
691
+
692
+ Some plugins require extra-parameters, such as flowerytts:
693
+ Pass extra parameters to the search function to use plugin-specific features.
694
+
695
+ <details>
696
+ <summary><strong>How to use the flowerytts plugin</strong></summary>
697
+
698
+ ```javascript
699
+ // Example for flowertts plugin
700
+ const query = interaction.options.getString("text");
701
+ const voice = interaction.options.getString("voice"); // e.g., "MALE_1"
702
+
703
+ const extraParams = new URLSearchParams();
704
+ if (voice) extraParams.append(`voice`, voice);
705
+
706
+ // All params for flowertts can be found here: https://flowery.pw/docs
707
+ const response = await player.search(
708
+ {
709
+ query: `${query}`,
710
+ // This is used by plugins like ftts to adjust the request
711
+ extraQueryUrlParams: extraParams,
712
+ source: "ftts", // Specify the plugin source
713
+ },
714
+ interaction.user, // The requester
715
+ );
716
+
717
+ // Add the TTS track to the queue
718
+ if (response.tracks.length > 0) {
719
+ player.queue.add(response.tracks[0]);
720
+ if (!player.playing) player.play();
721
+ }
722
+ ```
723
+
724
+ </details>
725
+
726
+ </div>