Easy, flexible and feature-rich lavalink@v4 Client. Both for Beginners and Proficients.
Latest stable Version: v2.4.0
Check out the Documentation
💯 Lavalink v4 Supported only (with Lavalink Plugins)
✅ Player-Destroy Reasons like:
✨ Choose able queue stores (maps, collections, redis, databases, ...)
😍 Included Filter & Equalizer Management
👍 Multiple Player Options for easier use
🛡️ Lavalink Validations
🛡️ Client Validations
🧑💻 Memory friendly and easy style
😘 Automated Handlings
😁 Much much more!
import { LavalinkManager } from "lavalink-client";
import { Client, GatewayIntentBits } from "discord.js"; // example for a discord bot
// you might want to extend the types of the client, to bind lavalink to it.
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildVoiceStates,
]
});
// create instance
client.lavalink = new LavalinkManager({
nodes: [
{
authorization: "localhoist",
host: "localhost",
port: 2333,
id: "testnode",
}
],
sendToShard: (guildId, payload) => client.guilds.cache.get(guildId)?.shard?.send(payload),
autoSkip: true,
client: {
id: envConfig.clientId,
username: "TESTBOT",
},
});
client.on("raw", d => client.lavalink.sendRawData(d)); // send raw data to lavalink-client to handle stuff
client.on("ready", () => {
client.lavalink.init(client.user); // init lavalink
});
Player related logs
playerCreate
➡️(player) => {}
playerDestroy
➡️(player, reason) => {}
playerDisconnect
➡️(player, voiceChannelId) => {}
playerMove
➡️(player, oldChannelId, newChannelId) => {}
- Updating the voice channel is handled by the client automatically
playerSocketClosed
➡️(player, payload) => {}
Track / Manager related logs
trackStart
➡️(player, track, payload) => {}
trackStuck
➡️(player, track, payload) => {}
trackError
➡️(player, track, payload) => {}
trackEnd
➡️(player, track, payload) => {}
queueEnd
➡️(player, track, payload) => {}
playerUpdate
➡️(player) => {}
client.lavalink.on("create", (node, payload) => {
console.log(`The Lavalink Node #${node.id} connected`);
});
// for all node based errors:
client.lavalink.on("error", (node, error, payload) => {
console.error(`The Lavalink Node #${node.id} errored: `, error);
console.error(`Error-Payload: `, payload)
});
raw
➡️ (node, payload) => {}
disconnect
➡️ (node, reason) => {}
connect
➡️ (node) => {}
reconnecting
➡️ (node) => {}
create
➡️ (node) => {}
destroy
➡️ (node) => {}
error
➡️ (node, error, payload) => {}
resumed
➡️ (node, payload, players) => {}
client.lavalink.nodeManager.on("create", (node, payload) => {
console.log(`The Lavalink Node #${node.id} connected`);
});
// for all node based errors:
client.lavalink.nodeManager.on("error", (node, error, payload) => {
console.error(`The Lavalink Node #${node.id} errored: `, error);
console.error(`Error-Payload: `, payload)
});
When creating the manager, add the option:
queueOptions.queueChangesWatcher: new myCustomWatcher(botClient)
E.g:import { QueueChangesWatcher, LavalinkManager } from "lavalink-client";
class myCustomWatcher implements QueueChangesWatcher {
constructor(client) {
this.client = client;
}
shuffled(guildId, oldStoredQueue, newStoredQueue) {
console.log(${this.client.guilds.cache.get(guildId)?.name || guildId}: Queue got shuffled
)
}
tracksAdd(guildId, tracks, position, oldStoredQueue, newStoredQueue) {
console.log(${this.client.guilds.cache.get(guildId)?.name || guildId}: ${tracks.length} Tracks got added into the Queue at position #${position}
);
}
tracksRemoved(guildId, tracks, position, oldStoredQueue, newStoredQueue) {
console.log(${this.client.guilds.cache.get(guildId)?.name || guildId}: ${tracks.length} Tracks got removed from the Queue at position #${position}
);
}
}
client.lavalink = new LavalinkManager({ // ... other options queueOptions: { queueChangesWatcher: new myCustomWatcher(client) } })
## How to do resuming
1. You need to enable resuming on a __connected__ Lavalink node : **` node.updateSession(true, 360e3) `**
2. The NodeManager#resumed event will emit when the node resumes, you retrieves all fetchedPlayers (fetched by the client), and thus all you need to do is re-create all player instances (and possibly the queues too)
- For that is the queuestore useful
- To save the playerData you can utilize smt like playerUpdate event.
## Resuming full Example
Full code sample: can be found on the [Testbot in here](https://github.com/Tomato6966/lavalink-client/blob/main/testBot/Utils/handleResuming.ts)
```js
// but here is the schema:
client.lavalink.nodeManager.on("connect", (node) => node.updateSession(true, 360e3));
client.lavalink.nodeManager.on("resumed", (node, payload, fetchedPlayers) => {
// create players:
for(const fetchedPlayer of fetchedPlayers) {
// fetchedPlayer is the live data from lavalink
// saved Player data is the config you should save in a database / file or smt
const savedPlayerData = await getSavedPlayerData(fetchedPlayer.guildId);
const player = client.lavalink.createPlayer({
guildId: fetchedPlayer.guildId,
});
// if lavalink says the bot got disconnected, we can skip the resuming, or force reconnect whatever you want!, here we choose to not do anything and thus delete the saved player data
if(!data.state.connected) {
console.log("skipping resuming player, because it already disconnected");
await deletedSavedPlayerData(data.guildId);
continue;
}
// now you can create the player based on the live and saved data
const player = client.lavalink.createPlayer({
guildId: data.guildId,
node: node.id,
// you need to update the volume of the player by the volume of lavalink which might got decremented by the volume decrementer
volume: client.lavalink.options.playerOptions?.volumeDecrementer
? Math.round(data.volume / client.lavalink.options.playerOptions.volumeDecrementer)
: data.volume,
// all of the following options are needed to be provided by some sort of player saving
voiceChannelId: dataOfSaving.voiceChannelId,
textChannelId: dataOfSaving.textChannelId,
// all of the following options can either be saved too, or you can use pre-defined defaults
selfDeaf: dataOfSaving.options?.selfDeaf || true,
selfMute: dataOfSaving.options?.selfMute || false,
applyVolumeAsFilter: dataOfSaving.options.applyVolumeAsFilter,
instaUpdateFiltersFix: dataOfSaving.options.instaUpdateFiltersFix,
vcRegion: dataOfSaving.options.vcRegion,
});
// player.voice = data.voice;
// normally just player.voice is enough, but if you restart the entire bot, you need to create a new connection, thus call player.connect();
await player.connect();
player.filterManager.data = data.filters; // override the filters data
await player.queue.utils.sync(true, false); // get the queue data including the current track (for the requester)
// override the current track with the data from lavalink
if(data.track) player.queue.current = client.lavalink.utils.buildTrack(data.track, player.queue.current?.requester || client.user);
// override the position of the player
player.lastPosition = data.state.position;
player.lastPositionChange = Date.now();
// you can also override the ping of the player, or wait about 30s till it's done automatically
player.ping.lavalink = data.state.ping;
// important to have skipping work correctly later
player.paused = data.paused;
player.playing = !data.paused && !!data.track;
// That's about it
}
})
client.lavalink.on("playerUpdate", (oldPlayer, newPlayer) => { // automatically sync player data on updates. if you don'T want to save everything you can instead also just save the data on playerCreate
setSavedPlayerData(newPlayer.toJSON());
});
// delete the player again
client.lavalink.on("playerDestroy", (player) => {
deleteSavedPlayerData(player.guildId);
})
const query = interaction.options.getString("text");
const voice = interaction.options.getString("voice");
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}`,
extraQueryUrlParams: extraParams, // as of my knowledge this is currently only used for flowertts, adjusting the playback url dynamically mid-request
source: "ftts"
}, interaction.user);
player.stopPlaying()
: When executed it clears the Queue and stops playing, without destroying the PlayerPlayer.skip()
throwError
Property to: player.skip(skipTo?:number = 0, throwError?:boolean = true)
.player.getSponsorBlock()
/ node.getSponsorBlock()
player.setSponsorBlock(segments:SponsorBlockSegment[])
/ node.setSponsorBlock(segments:SponsorBlockSegment[])
player.deleteSponsorBlock()
/ node.deleteSponsorBlock()
"SegmentsLoaded"
, "SegmentSkipped"
, "ChapterStarted"
, "ChaptersLoaded"
ManagerOptions#emitNewSongsOnly
. If set to true, it won't emit "trackStart" Event, when track.loop is active, or the new current track == the previous (current) track.ManagerOptions#linksBlacklist
which allows user to specify an array of regExp / strings to match query strings (for links / words) and if a match happens it doesn't allow the request (blacklist)ManagerOptions#linksWhitelist
which allows user to specify an array of regExp / strings to match query strings (for links only) and if a match does NOT HAPPEN it doesn't allow the request (whitelist)ManagerOptions#linksAllowed
if set to false, it does not allow requests which are linksManaagerOptions#debugOptions
to ManaagerOptions#advancedOptions.debugOptions
player.stopPlaying()
stopPlaying(clearQueue:boolean = true, executeAutoplay:boolean = false)
.parseLavalinkConnUrl(connectionUrl:string)
Util Function.
lavalink://<nodeId>:<nodeAuthorization(Password)>@<NodeHost>:<NodePort>
{ id: string, authorization: string, host: string, port: number }
parseLavalinkConnUrl("lavalink://LavalinkNode_1:strong%23password1@localhost:2345")
will give you:
{ id: "LavalinkNode_1", authorization: "strong#password1", host: "localhost", port: 2345 }
In one of the next updates, there will be more queueWatcher options and more custom nodeevents to trace
Most features of this update got tested, but if you encounter any bugs feel free to open an issue!
## **Version 2.1.0**
- Fixed that, if you skip and have trackloop enabled, it doesn't skip the track
- I fixed that in the past, but for some reason i removed the fix on accident ig.
- Reworked the Filter Manager for custom filters via [LavalinkFilterPlugin](https://github.com/rohank05/lavalink-filter-plugin) / [LavalinkLavaDSPX-Plugin](https://github.com/devoxin/LavaDSPX-Plugin/)
- Note that the [LavalinkLavaDSPX-Plugin](https://github.com/devoxin/LavaDSPX-Plugin/) is by a Community Member of Lavalink and UNOFFICIAL
- They now have individual state-variabels (booleans): `player.filterManager.filters.lavalinkLavaDspxPlugin`
- `player.filterManager.filters.lavalinkLavaDspxPlugin.echo`
- `player.filterManager.filters.lavalinkLavaDspxPlugin.normalization`
- `player.filterManager.filters.lavalinkLavaDspxPlugin.highPass`
- `player.filterManager.filters.lavalinkLavaDspxPlugin.lowPass`
- and for: `player.filterManager.filters.lavalinkFilterPlugin` (this plugins seems to not work on v4 at the moment)
- `player.filterManager.filters.lavalinkLavaDspxPlugin.echo`
- `player.filterManager.filters.lavalinkLavaDspxPlugin.reverb`
- They also now have individual state-changing-methods: `player.filterManager.lavalinkLavaDspxPlugin`
- `player.filterManager.lavalinkLavaDspxPlugin.toggleEcho(decay:number, echoLength:number)`
- `player.filterManager.lavalinkLavaDspxPlugin.toggleNormalization(maxAmplitude:number, adaptive:boolean)`
- `player.filterManager.lavalinkLavaDspxPlugin.toggleHighPass(boostFactor:number, cutoffFrequency:number)`
- `player.filterManager.lavalinkLavaDspxPlugin.toggleLowPass(boostFactor:number, cutoffFrequency:number)`
- and for: `player.filterManager.lavalinkFilterPlugin`
- `player.filterManager.lavalinkFilterPlugin.toggleEcho(delay:number, decay:number)`
- `player.filterManager.lavalinkFilterPlugin.toggleReverb(delays:number[], gains:number[])`
## **Version 2.1.1**
- Enforce link searches for users with following searchPlatform Options: "http" | "https" | "link" | "uri"
- Additionally strongend the code behind that
- Added searchPlatform for local tracks (aka files on the lavalink server...): "local"
## **Version 2.2.0**
- Changed console.error to throw error on queue.utils.sync if no data was provided/found
- Changed undici.fetch to native fetch, but requires nodejs v18+ to support other runtimes, e.g. bun
- Added sourceNames for `bandcamp` (from native lavalink) if it's supported it will use lavalink'S search, else the client search on player.search({ source: "bandcamp" }) (you can also use bcsearch or bc)
- Added sourceName for `phsearch` from the dunktebot plugin, released in v.1.7.0
- Support for youtube still going via the youtube-source plugin (disable youtube for lavalink, and use the plugin instead)
- Exporting events
- Added new debugOption: logCustomSearches
- *(Next version update i will remove the internal interval for position update, to calculations)*
## **Version 2.2.1**
- Player position is now calculated instead of using intervals
- Instaplayer fix update now requires quite good internet connection on the lavalink server due to removal of intervals for updating player.position (everything above 300mbps should be good)
- Internal updates for handling query params and url-requests (url-parsing) to fix quite few bugs and make the code more readable, now you don't have to ever provide stuff encoded via encodeURIComponent anymore.
- Added a bunch of jsdoc information, to make the autogenerated docs more accurate!
- Because of the adjustments from the encoding, you now need to pass url params for stuff like flowery tts like this:
```js
const query = interaction.options.getString("text");
const voice = interaction.options.getString("voice");
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}`,
extraQueryUrlParams: extraParams, // as of my knowledge this is currently only used for flowertts, adjusting the playback url dynamically mid-request
source: "ftts"
}, interaction.user);
const previousTrack = await player.queue.shiftPrevious()
-> removes the previously played track from the player.queue.previous array, and returns it, so you can use it for something like "play previous"await player.queue.shiftPrevious().then(clientTrack => player.play({ clientTrack }))
await player.queue.remove(removeQuery)
-> Remove function to remove stuff from the queue.tracks array., following params are valid:await player.queue.remove( player.queue.tracks.slice(4, 10) )
(would remove tracks from #4 (incl.) to #10 (excl.) aka those indexes: 4, 5, 6, 7, 8, 9 - this is how array.slice works)await player.queue.remove(player.queue.tracks[player.queue.tracks.length - 1]);
(would remove the last track)await player.queue.remove([1, 4, 5])
(Would remove track #1, #4 and #5)await player.queue.remove(5)
(would remove the #5 track from the queue)player.queue.splice()
function for mutating the queue:
track.pluginInfo.clientData?.previousTrack
handling:const previousTrack = await player.queue.shiftPrevious();
if(previousTrack) {
const previousClientData = previousTrack.pluginInfo.clientData || {};
previousTrack.pluginInfo.clientData = { previousTrack: true, ...previousClientData }
await player.play({ clientTrack: previousTrack });
}
Added a heartbeat + ping-pong system to check wether the client is still connected to the node, if the node doesn't receive a ping in time, it will destroy the node and thus cause a reconnect.
For that following new nodeOptions got added:
enablePingOnStatsCheck: boolean
(default: true)heartBeatInterval: number
(default: 30_000)isAlive: boolean
(if it's false, then it's not connected to the node anymore, and will AUTOMATICALLY Cause a reconnect within the heartBeatInterval)heartBeatPing: number
(the ping it takes lavalink to respond to the acknowledge of heartbeat)reconnectinprogress
(when the client internal reconnect system is triggered, the actual reconnect gets triggered by the node after your retryDelay)Refactored internal code for better readability and maintainability
Removed several intermediate promises
Added new types for better type safety
Updated types for better type safety
Reduced default retryDelay from 30s to 10s
Added example on the testbot how to store player data easily and how to use the resume feature, and updated the Resuming Example in the README jump by adding a full example
"Breaking Change" for providing track / clientTrack for player.play()
player.play({ clientTrack: searchResult.tracks[0] })
player.play({ track: { encoded: "base64string..." }, requester: interaction.user })
this.NodeManager.LavalinkManager.options.playerOptions.maxErrorsPerTime
:
{ threshold: number, maxAmount: number }
(set threshold to 0 or maxAmount to -1 to disable){ threshold: 10_000, maxAmount: 3 }
this.NodeManager.LavalinkManager.options.playerOptions.minAutoPlayMs
:
10_000
(default)0
to disableLavalinkManager#debug(event:DebugEvents, data:{ state: "log" | "warn" | "error", message:string, functionLayer:string, error?:Error })
true
: LavalinkManager.options.advancedOptions.enableDebugEvents
DebugEvents
Enum to listen to specific events and only show those you caredata.state
to only show the certain log-level statefunctionLayer
string will show you where the debug event was triggered frommessage
string will show what is debuggederror
object will show you the error that happened, if there was one.const lyrics = await player.getCurrentLyrics(false);
-> Get lyrics of current playing trackconst lyrics = await player.getLyrics(track, true);
-> Get lyrics of a specific track with ignoring it's sourcesplayer.subscribeLyrics();
-> Subscribe this guild to retrieve "live lyrics" as the song is playingplayer.unsubscribeLyrics();
-> *Unsubscribe from lyricsconst lyrics = await player.node.lyrics.getCurrent(player.guildId, false);
const lyrics = await player.node.lyrics.get(track, true);
player.node.lyrics.subscribe(player.guildId);
player.node.lyrics.unsubscribe(player.guildId);
lavalink.on("LyricsLine", (player, track, lyricsLine) => {});
lavalink.on("LyricsFound", (player, track, data) => {});
lavalink.on("LyricsNotFound", (player, track, lyricsLine) => {});
Refactored a little the project folder Structure
Added PR Packages to install all commits / packages at once https://pkg.pr.new/Tomato6966/lavalink-client
Removed the dist folder, and added prepare Scripts
Added attributes for git linting
Removed the old (gitbook) documentation, and swapped it to a NEW TSDOC Themed Documentation via astro.dev and mdx
Added new player events:
playerMuteChange
** ➡️ *(player, selfMuted, serverMuted) => {}
Triggered when the player's voice state related to muting changed*
playerDeafChange
➡️ (player, selfDeafed, serverDeafed) => {}
Triggered when the player's voice state related to deafing changed
playerSuppressChange
➡️ (player, suppress) => {}
Triggered when the player's voice state related to suppressing changed
playerQueueEmptyStart
➡️ (player, timeoutMs) => {}
Triggered when the queue empty handler started (the timeout)
playerQueueEmptyEnd
➡️ (player) => {}
Triggered when the queue empty handler finished (successfully) and thus destroyed the player
playerQueueEmptyCancel
➡️ (player) => {}
Triggered when the queue empty handler cancelled (e.g. because a new track got added)
-
playerVoiceEmptyStart
➡️ (player, timeoutMs) => {}
Triggered when the voice empty handler started (the timeout)
Removed again because of memory overhall and not wanting to handle voice states
-
playerVoiceEmptyEnd
➡️ (player) => {}
Triggered when the voice empty handler finished (successfully) and thus destroyed the player
Removed again because of memory overhall and not wanting to handle voice states
-
playerVoiceEmptyCancel
➡️ (player, userId) => {}
Triggered when the voice empty handler cancelled (e.g. when a user rejoined)
Removed again because of memory overhall and not wanting to handle voice states
playerVoiceJoin
➡️ (player, userId) => {}
~~
Added instead of the playerVoiceEmpty handler, emitted when a user joins the player-vc while there is a player
Allows you to inmplement a custom playerVoiceEmpty handler
playerVoiceLeave
➡️ (player, userId) => {}
~~
Added instead of the playerVoiceEmpty handler, emitted when a user leaves (or. switches away) the player-vc while there is a player
Allows you to inmplement a custom playerVoiceEmpty handler
Added the new events and configuration to the docs