LavalinkManager

The main Base-Manager of this Package

Type: class extends node:EventEmitter

Constructor

new LavalinkManager(options:ManagerOptions)

Import

index.ts
import { LavalinkManager } from "lavalink-client";

Overview

Properties
Methods
Event-Listeners

Check out Example Creations down below


Properties

.initated

If the Manager was initated

Type: Boolean

.useable

If the Manager is useable (If at least 1 Node is connected)

Type: Boolean

.options

The options from the Manager

Type: ManagerOptions

.players

All the Players of the Manager

Type: MiniMap<guildId:string, Player>

.nodeManager

The Node Manager of the Manager

Type: NodeManager

.utils

The Manager's Utils

Type: ManagerUtils


Methods

.init(clientData: BotClientOptions) IMPORTANT!

Initializes the Manager and connects all Nodes

Returns: Promise<LavalinkManager>

botClient.on("ready", async () => { // only init it when the Bot is ready...
  await botClient.lavalink.init({ 
    id: botClient.user.id, 
    username: botclient.user.username
  });
});

.createPlayer(options: PlayerOptions)

Create or get a Player

Returns: Player

const newPlayer = await client.lavalink.createPlayer({
  guildId: interaction.guildId, 
  voiceChannelId: interaction.member.voice?.channelId, 
  textChannelId: interaction.channelId, // (optional)
  selfDeaf: true, // (optional)
  selfMute: false, // (optional)
  volume: client.defaultVolume,  // (optional) default volume
  instaUpdateFiltersFix: true, // (optional)
  applyVolumeAsFilter: false, // (optional)
  // node: "YOUR_NODE_ID", // (optional)
  // vcRegion: interaction.member.voice?.channel?.rtcRegion // (optional)
});

.getPlayer(guildId:string)

Create a Player

Returns: Player | undefined

const player = client.lavalink.getPlayer(interaction.guildId);

Important Conditions to check:

  • player is not undefined

  • player is connected to a VoiceChannel

  • user in a VoiceChannel && player in same VoiceChannel as user

  • player.node is Connected

  • player is playing / there is a current song in player.queue

.deletePlayer(guildId:string)

Removes a Player from the saved MiniMap, needs to be destroyed first

Returns: Boolean

client.lavalink.deletePlayer(oldPlayer?.guildId || interaction.guildId);

.sendRawData(data : VoicePacket | VoiceServer | VoiceState | any) IMPORTANT!

Sends Raw Discord's Clients Event Data to the Manager

Returns: void

botClient.on("raw", (data) => client.lavalink.sendRawData(data));

Event-Listeners

All Events you can listen to on the LavalinkManager Class

trackStart

Emitted whenever a Track plays

Parameter
Type
Description

player

The Player for this Event

track

The current playing track (player.queue.current)

payload

The Payload Lavalink sent

client.lavalink.on("trackStart", (player, track, payload) => { });

trackEnd

Emitted whenever a Track finished playing.

Parameter
Type
Description

player

The Player for this Event

track

The Track that finished Playing

payload

The Payload Lavalink sent

client.lavalink.on("trackEnd", (player, track, payload) => { });

trackStuck

Emitted whenever a Track got stuck while playing

Parameter
Type
Description

player

The Player for this Event

track

The Track that got stuck

payload

The Payload Lavalink sent

client.lavalink.on("trackStuck", (player, track, payload) => { });

trackError

Emitted whenever a Track errored

Parameter
Type
Description

player

The Player for this Event

track

The Track that Errored

payload

The Payload Lavalink sent

client.lavalink.on("trackError", (player, track, payload) => { });

queueEnd

Emitted when the track Ended, but there are no more tracks in the queue

(trackEnd, does NOT get exexcuted)

Parameter
Type
Description

player

The Player for this Event

track

The last played track

payload

The Payload Lavalink sent

client.lavalink.on("queueEnd", (player, track, payload) => { });

playerCreate

Emitted whenver a Player gets created

Parameter
Type
Description

player

The created Player

client.lavalink.on("playerCreate", (player) => { });

playerMove

Emitted whenever a Player gets moved between Voice Channels

Parameter
Type
Description

player

The Player for this Event

oldVoiceChannelId

old Voice Channel Id

newVoiceChannelId

new Voice Channel Id

client.lavalink.on("playerMove", (player, oldVCId, newVCId) => { });

playerDisconnect

Emitted whenever a player is disconnected from a channel

Parameter
Type
Description

player

The Player for this Event

voiceChannelId

The disconnected voice Channel

client.lavalink.on("playerDisconnect", (player, voiceChannelId) => { });

playerSocketClose

Emitted when a Node-Socket got closed for a specific Player

Parameter
Type
Description

player

The Player for this Event

payload

The Payload Lavalink sent

client.lavalink.on("playerSocketClose", (player, track, payload) => { });

playerDestroy

Emitted whenever a Player got destroyed

Parameter
Type
Description

player

The Destroyed Player

destroyReason

The Destroy Reason (if provided)

client.lavalink.on("playerDestroy", (player, destroyReason) => { });

playerUpdate

Emitted whenever a Player gets an update from Lavalink's playerUpdate Event

Parameter
Type
Description

oldPlayerJson

Player Data before it was udpated

newPlayer

Afterwards the Player got updated

client.lavalink.on("playerUpdate", (oldPlayerJson, newPlayer) => { });

Example-Creations

Example Creation
import { Client, GatewayIntentBits } from "discord.js";
import { createClient, RedisClientType } from 'redis';
import { LavalinkManager } from "../src";
// The Bot Client, here a discord.js one
const client = new Client({
    intents: [ GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates ]
}) as Client & { redis: RedisClientType, lavalink: LavalinkManager };
client.lavalink = new LavalinkManager({
    nodes: [
        {
            authorization: "yourverystrongpassword",
            host: "localhost",
            port: 2333,
            id: "testnode",
            requestTimeout: 10000,
        }
    ],
    sendToShard: (guildId, payload) => client.guilds.cache.get(guildId)?.shard?.send(payload),
    autoSkip: true,
    client: {
        id: envConfig.clientId, // REQUIRED! (at least after the .init)
        username: "TESTBOT"
    },
    playerOptions: {
        applyVolumeAsFilter: false,
        clientBasedPositionUpdateInterval: 50,
        defaultSearchPlatform: "ytmsearch",
        volumeDecrementer: 0.75, // on client 100% == on lavalink 75%
        onDisconnect: {
            autoReconnect: true, // automatically attempts a reconnect, if the bot disconnects from the voice channel, if it fails, it gets 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
        },
        useUnresolvedData: true
    },
    queueOptions: {
        maxPreviousTracks: 10
    },
});

Finally, initatlize the Manager and send Raw Data!

client.on("ready", async () => {
  await client.lavalink.init({
    id: client.user.id,
    username: client.user.username
  });
});

client.on("raw", data => client.lavalink.sendRawData(data));

Advanced Example Creation

Example with Redis-Queue, Request-Transformer, Autoplay Function, ...
import { Client, GatewayIntentBits } from "discord.js";
import { createClient, RedisClientType } from 'redis';
import { LavalinkManager, QueueStoreManager , QueueChangesWatcher } from "lavalink-client";
// The Bot Client, here a discord.js one
const client = new Client({
    intents: [ GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates ]
}) as Client & { redis: RedisClientType, lavalink: LavalinkManager };
// The Custom Redis Server
client.redis = createClient({ 
  url: "redis://localhost:6379", 
  password: "your_very_strong_password"
});
client.redis.connect();
client.redis.on("error", (err) => console.log('Redis Client Error', err));
// Create the Lavalink Server
client.lavalink = new LavalinkManager({
    nodes: [
        {
            authorization: "yourverystrongpassword",
            host: "localhost",
            port: 2333,
            id: "testnode",
            requestTimeout: 10000,
        }
    ],
    sendToShard: (guildId, payload) => client.guilds.cache.get(guildId)?.shard?.send(payload),
    autoSkip: true,
    client: {
        id: envConfig.clientId, // REQUIRED! (at least after the .init)
        username: "TESTBOT"
    },
    playerOptions: {
        applyVolumeAsFilter: false,
        clientBasedPositionUpdateInterval: 50,
        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 gets 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,
        queueStore: new myCustomStore(client.redis),
        queueChangesWatcher: new myCustomWatcher(client)
    },
});

Custom Queue Store Class (Saving the queue on a redis server)

export class myCustomStore implements QueueStoreManager {
    private redis:RedisClientType;
    constructor(redisClient:RedisClientType) {
        this.redis = redisClient;
    }
    async get(guildId): Promise<any> {
        return await this.redis.get(this.id(guildId));
    }
    async set(guildId, stringifiedQueueData): Promise<any> {
        // await this.delete(guildId); // redis requires you to delete it first;
        return await this.redis.set(this.id(guildId), stringifiedQueueData);
    }
    async delete(guildId): Promise<any> {
        return await this.redis.del(this.id(guildId));
    }
    async parse(stringifiedQueueData): Promise<Partial<StoredQueue>> {
        return JSON.parse(stringifiedQueueData);
    }
    async stringify(parsedQueueData): Promise<any> {
        return JSON.stringify(parsedQueueData);
    }
    // you can add more utils if you need to...
    private id(guildId) {
        return `lavalinkqueue_${guildId}`; // transform the id
    }
}

Custom Queue Changes Watcher Class, for seeing changes within the Queue

export class myCustomWatcher implements QueueChangesWatcher {
    private client:BotClient;
    constructor(client:BotClient) {
        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}`);
    }
}

Custom Autoplay function, which gets executed, before the "queueEnd" Event fires, and if there is a new track added in the queue, then it plays it. else "queueEnd" will fire

export const autoPlayFunction = async (player, lastPlayedTrack) => {
    if(lastPlayedTrack.info.sourceName === "spotify") {
        const filtered = player.queue.previous.filter(v => v.info.sourceName === "spotify").slice(0, 5);
        const ids = filtered.map(v => v.info.identifier || v.info.uri.split("/")?.reverse()?.[0] || v.info.uri.split("/")?.reverse()?.[1]);
        if(ids.length >= 2) {
            const res = await player.search({
                query: `seed_tracks=${ids.join(",")}`, //`seed_artists=${artistIds.join(",")}&seed_genres=${genre.join(",")}&seed_tracks=${trackIds.join(",")}`;
                source: "sprec"
            }, lastPlayedTrack.requester).then(response => {
                response.tracks = response.tracks.filter(v => v.info.identifier !== lastPlayedTrack.info.identifier); // remove the lastPlayed track if it's in there..
                return response;
            }).catch(console.warn);
            if(res && res.tracks.length) await player.queue.add(res.tracks.slice(0, 5).map(track => { 
                // transform the track plugininfo so you can figure out if the track is from autoplay or not. 
                track.pluginInfo.clientData = { ...(track.pluginInfo.clientData||{}), fromAutoplay: true }; 
                return track;
            })); else console.log("Spotify - NOTHING GOT ADDED");
        }
        return;
    }
    if(lastPlayedTrack.info.sourceName === "youtube" || lastPlayedTrack.info.sourceName === "youtubemusic") {
        const res = await player.search({
            query:`https://www.youtube.com/watch?v=${lastPlayedTrack.info.identifier}&list=RD${lastPlayedTrack.info.identifier}`,
            source: "youtube"
        }, lastPlayedTrack.requester).then(response => {
            response.tracks = response.tracks.filter(v => v.info.identifier !== lastPlayedTrack.info.identifier); // remove the lastPlayed track if it's in there..
            return response;
        }).catch(console.warn);
        if(res && res.tracks.length) await player.queue.add(res.tracks.slice(0, 5).map(track => { 
            // transform the track plugininfo so you can figure out if the track is from autoplay or not. 
            track.pluginInfo.clientData = { ...(track.pluginInfo.clientData||{}), fromAutoplay: true }; 
            return track;
        })); else console.log("YT - NOTHING GOT ADDED");
        return;
    }
    return
}

Custom requestTransformer Function, which allows you to provide just a User Object into the "requester" parameter(s) and then transform it to save memory! Attention: This function might get executed on already transformed requesters, if you re-use it.

export const requesterTransformer = (requester:any):CustomRequester => {
    // if it's already the transformed requester
    if(typeof requester === "object" && "avatar" in requester && Object.keys(requester).length === 3) return requester as CustomRequester; 
    // if it's still a discord.js User
    if(typeof requester === "object" && "displayAvatarURL" in requester) { // it's a user
        return {
            id: requester.id,
            username: requester.username,
            avatar: requester.displayAvatarURL(),
        }
    }
    // if it's non of the above
    return { id: requester!.toString(), username: "unknown" }; // reteurn something that makes sense for you!
}

export interface CustomRequester {
    id: string,
    username: string,
    avatar?: string,
}

Finally, initatlize the Manager and send Raw Data!

client.on("ready", async () => {
  await client.lavalink.init({
    id: client.user.id,
    username: client.user.username
  });
});

client.on("raw", data => client.lavalink.sendRawData(data));

Last updated