Tomato6966 / lavalink-client

Easy, flexible and feature-rich lavalink@v4 Client. Both for Beginners and Proficients.
https://tomato6966.github.io/lavalink-client/
MIT License
61 stars 22 forks source link
bot client discord lavalink music

Lavalink Client

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

Made with love in Austria Made with TypeScript

NPM version NPM downloads Get Started Now

npm install lavalink-client

Install

Latest stable Version: v2.4.0

👉 via NPM ```bash npm install --save lavalink-client ``` Dev Version: (Current) ```bash npm install --save tomato6966/lavalink-client ```
👉 via YARN ```bash yarn add lavalink-client ``` Dev Version: (Current) ```bash yarn add tomato6966/lavalink-client ```

Documentation

Check out the Documentation

Used in:

Features


Sample Configuration

Complete Configuration Example, with all available options ```ts import { LavalinkManager, QueueChangesWatcher, QueueStoreManager } from "lavalink-client"; import { RedisClientType } from "redis"; // example for custom queue store 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, ] }); const previouslyUsedSessions = new Map(); //nodeManager.on("connect", node => previouslyUsedSessions.set(node.id, node.sessionId)) client.lavalink = new LavalinkManager({ nodes: [ { authorization: "localhoist", host: "localhost", port: 2333, id: "testnode", // get the previously used session, to restart with "resuming" enabled sessionId: previouslyUsedSessions.get("testnode"), requestSignalTimeoutMS: 3000, closeOnError: true, heartBeatInterval: 30_000, enablePingOnStatsCheck: true, retryDelay: 10e3, secure: false, retryAmount: 5, } ], sendToShard: (guildId, payload) => client.guilds.cache.get(guildId)?.shard?.send(payload), autoSkip: true, client: { // client: client.user id: envConfig.clientId, // REQUIRED! (at least after the .init) username: "TESTBOT", }, autoSkipOnResolveError: true, // skip song, if resolving an unresolved song fails emitNewSongsOnly: true, // don't emit "looping songs" playerOptions: { // These are the default prevention methods maxErrorsPerTime: { threshold: 10_000, maxAmount: 3, }, // only allow an autoplay function to execute, if the previous function was longer ago than this number. minAutoPlayMs: 10_000, applyVolumeAsFilter: false, clientBasedPositionUpdateInterval: 50, // in ms to up-calc player.position defaultSearchPlatform: "ytmsearch", volumeDecrementer: 0.75, // on client 100% == on lavalink 75% requesterTransformer: requesterTransformer, onDisconnect: { autoReconnect: true, // automatically attempts a reconnect, if the bot disconnects from the voice channel, if it fails, it get's destroyed destroyPlayer: false // overrides autoReconnect and directly destroys the player if the bot disconnects from the vc }, onEmptyQueue: { destroyAfterMs: 30_000, // 0 === instantly destroy | don't provide the option, to don't destroy the player autoPlayFunction: autoPlayFunction, }, useUnresolvedData: true, }, queueOptions: { maxPreviousTracks: 10, // only needed if you want and need external storage, don't provide if you don't need to queueStore: new myCustomStore(client.redis), // client.redis = new redis() // only needed, if you want to watch changes in the queue via a custom class, queueChangesWatcher: new myCustomWatcher(client) }, linksAllowed: true, // example: don't allow p*rn / youtube links., you can also use a regex pattern if you want. // linksBlacklist: ["porn", "youtube.com", "youtu.be"], linksBlacklist: [], linksWhitelist: [], advancedOptions: { enableDebugEvents: true, maxFilterFixDuration: 600_000, // only allow instafixfilterupdate for tracks sub 10mins debugOptions: { noAudio: false, playerDestroy: { dontThrowError: false, debugLog: false, }, logCustomSearches: false, } } }); 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 }); // for the custom queue Store create a redis instance client.redis = createClient({ url: "redis://localhost:6379", password: "securepass" }); client.redis.connect(); // Custom external queue Store export class myCustomStore implements QueueStoreManager { private redis:RedisClientType; constructor(redisClient:RedisClientType) { this.redis = redisClient; } async get(guildId): Promise { return await this.redis.get(this.id(guildId)); } async set(guildId, stringifiedQueueData): Promise { return await this.redis.set(this.id(guildId), stringifiedQueueData); } async delete(guildId): Promise { return await this.redis.del(this.id(guildId)); } async parse(stringifiedQueueData): Promise> { return JSON.parse(stringifiedQueueData); } async stringify(parsedQueueData): Promise { return JSON.stringify(parsedQueueData); } private id(guildId) { return `lavalinkqueue_${guildId}`; // transform the id to your belikings } } // Custom Queue Watcher Functions export class myCustomWatcher implements QueueChangesWatcher { constructor() { } 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}`); } } ```
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
});

All Events:

On Lavalink-Manager:

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

On Node-Manager:

How to log queue logs?

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

How to use flowertts with custom options

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

UpdateLog

Version 1.2.0

Version 1.2.1

Version 2.0.0

How to load tracks / stop playing has changed for the node.updatePlayer rest endpoint the Client handles it automatically

To satisfy the changes from lavalink updatePlayer endpoint, player play also got adjusted for that (Most users won't need this feature!)

Node Resuming got supported

First enable it by doing:

Node Options got adjusted # It's a property not a method should be treated readonly

Player function got added to stop playing without disconnecting

Node functions for sponsorBlock Plugin (https://github.com/topi314/Sponsorblock-Plugin) got added

Lavalink track.userData got added (basically same feature as my custom pluginInfo.clientData system)

You only get the track.userData data through playerUpdate object

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

Version 2.2.2

Version 2.3.0

Version 2.3.1

Version 2.3.2 / Version 2.3.3

Version 2.3.4

Version 2.3.5

Version 2.3.6

Version 2.4.0