lavalink-client
Version:
Easy, flexible and feature-rich lavalink@v4 Client. Both for Beginners and Proficients.
762 lines (761 loc) • 35.8 kB
JavaScript
import { DebugEvents } from "./Constants.js";
import { bandCampSearch } from "./CustomSearches/BandCampSearch.js";
import { FilterManager } from "./Filters.js";
import { Queue, QueueSaver } from "./Queue.js";
import { queueTrackEnd } from "./Utils.js";
export class Player {
/** Filter Manager per player */
filterManager;
/** circular reference to the lavalink Manager from the Player for easier use */
LavalinkManager;
/** Player options currently used, mutation doesn't affect player's state */
options;
/** The lavalink node assigned the the player, don't change it manually */
node;
/** The queue from the player */
queue;
/** The Guild Id of the Player */
guildId;
/** The Voice Channel Id of the Player */
voiceChannelId = null;
/** The Text Channel Id of the Player */
textChannelId = null;
/** States if the Bot is supposed to be outputting audio */
playing = false;
/** States if the Bot is paused or not */
paused = false;
/** Repeat Mode of the Player */
repeatMode = "off";
/** Player's ping */
ping = {
/* Response time for rest actions with Lavalink Server */
lavalink: 0,
/* Latency of the Discord's Websocket Voice Server */
ws: 0
};
/** The Display Volume */
volume = 100;
/** The Volume Lavalink actually is outputting */
lavalinkVolume = 100;
/** The current Positin of the player (Calculated) */
get position() {
return this.lastPosition + (this.lastPositionChange ? Date.now() - this.lastPositionChange : 0);
}
/** The timestamp when the last position change update happened */
lastPositionChange = null;
/** The current Positin of the player (from Lavalink) */
lastPosition = 0;
lastSavedPosition = 0;
/** When the player was created [Timestamp in Ms] (from lavalink) */
createdTimeStamp;
/** The Player Connection's State (from Lavalink) */
connected = false;
/** Voice Server Data (from Lavalink) */
voice = {
endpoint: null,
sessionId: null,
token: null
};
voiceState = {
selfDeaf: false,
selfMute: false,
serverDeaf: false,
serverMute: false,
suppress: false,
};
/** Custom data for the player */
data = {};
/**
* Create a new Player
* @param options
* @param LavalinkManager
*/
constructor(options, LavalinkManager) {
if (typeof options?.customData === "object")
for (const [key, value] of Object.entries(options.customData))
this.set(key, value);
this.options = options;
this.filterManager = new FilterManager(this);
this.LavalinkManager = LavalinkManager;
this.guildId = this.options.guildId;
this.voiceChannelId = this.options.voiceChannelId;
this.textChannelId = this.options.textChannelId || null;
this.node = typeof this.options.node === "string"
? this.LavalinkManager.nodeManager.nodes.get(this.options.node)
: this.options.node;
if (!this.node || typeof this.node.request !== "function") {
if (typeof this.options.node === "string" && this.LavalinkManager.options?.advancedOptions?.enableDebugEvents) {
this.LavalinkManager.emit("debug", DebugEvents.PlayerCreateNodeNotFound, {
state: "warn",
message: `Player was created with provided node Id: ${this.options.node}, but no node with that Id was found.`,
functionLayer: "Player > constructor()",
});
}
const least = this.LavalinkManager.nodeManager.leastUsedNodes();
this.node = least.filter(v => options.vcRegion ? v.options?.regions?.includes(options.vcRegion) : true)[0] || least[0] || null;
}
if (!this.node)
throw new Error("No available Node was found, please add a LavalinkNode to the Manager via Manager.NodeManager#createNode");
if (typeof options.volume === "number" && !isNaN(options.volume))
this.volume = Number(options.volume);
this.volume = Math.round(Math.max(Math.min(this.volume, 1000), 0));
this.lavalinkVolume = Math.round(Math.max(Math.min(Math.round(this.LavalinkManager.options.playerOptions.volumeDecrementer
? this.volume * this.LavalinkManager.options.playerOptions.volumeDecrementer
: this.volume), 1000), 0));
this.LavalinkManager.emit("playerCreate", this);
this.queue = new Queue(this.guildId, {}, new QueueSaver(this.LavalinkManager.options.queueOptions), this.LavalinkManager.options.queueOptions);
}
/**
* Set custom data.
* @param key
* @param value
*/
set(key, value) {
this.data[key] = value;
return this;
}
/**
* Get custom data.
* @param key
*/
get(key) {
return this.data[key];
}
/**
* CLears all the custom data.
*/
clearData() {
const toKeep = Object.keys(this.data).filter(v => v.startsWith("internal_"));
for (const key in this.data) {
if (toKeep.includes(key))
continue;
delete this.data[key];
}
return this;
}
/**
* Get all custom Data
*/
getAllData() {
return Object.fromEntries(Object.entries(this.data).filter(v => !v[0].startsWith("internal_")));
}
/**
* Play the next track from the queue / a specific track, with playoptions for Lavalink
* @param options
*/
async play(options = {}) {
if (this.get("internal_queueempty")) {
if (this.LavalinkManager.options?.advancedOptions?.enableDebugEvents) {
this.LavalinkManager.emit("debug", DebugEvents.PlayerPlayQueueEmptyTimeoutClear, {
state: "log",
message: `Player was called to play something, while there was a queueEmpty Timeout set, clearing the timeout.`,
functionLayer: "Player > play()",
});
}
this.LavalinkManager.emit("playerQueueEmptyCancel", this);
clearTimeout(this.get("internal_queueempty"));
this.set("internal_queueempty", undefined);
}
// if clientTrack provided, override options.track object
if (options?.clientTrack && (this.LavalinkManager.utils.isTrack(options?.clientTrack) || this.LavalinkManager.utils.isUnresolvedTrack(options.clientTrack))) {
if (this.LavalinkManager.utils.isUnresolvedTrack(options.clientTrack)) {
try {
// resolve the unresolved track
await options.clientTrack.resolve(this);
}
catch (error) {
if (this.LavalinkManager.options?.advancedOptions?.enableDebugEvents) {
this.LavalinkManager.emit("debug", DebugEvents.PlayerPlayUnresolvedTrackFailed, {
state: "error",
error: error,
message: `Player Play was called with clientTrack, Song is unresolved, but couldn't resolve it`,
functionLayer: "Player > play() > resolve currentTrack",
});
}
this.LavalinkManager.emit("trackError", this, this.queue.current, error);
if (options && "clientTrack" in options)
delete options.clientTrack;
if (options && "track" in options)
delete options.track;
// try to play the next track if possible
if (this.LavalinkManager.options?.autoSkipOnResolveError === true && this.queue.tracks[0])
return this.play(options);
return this;
}
}
if ((typeof options.track?.userData === "object" || typeof options.clientTrack?.userData === "object") && options.clientTrack)
options.clientTrack.userData = { ...(options?.clientTrack.userData || {}), ...(options.track?.userData || {}) };
options.track = {
encoded: options.clientTrack?.encoded,
requester: options.clientTrack?.requester,
userData: options.clientTrack?.userData,
};
}
// if either encoded or identifier is provided generate the data to play them
if (options?.track?.encoded || options?.track?.identifier) {
this.queue.current = options.clientTrack || null;
this.queue.utils.save();
if (typeof options?.volume === "number" && !isNaN(options?.volume)) {
this.volume = Math.max(Math.min(options?.volume, 500), 0);
let vol = Number(this.volume);
if (this.LavalinkManager.options.playerOptions.volumeDecrementer)
vol *= this.LavalinkManager.options.playerOptions.volumeDecrementer;
this.lavalinkVolume = Math.round(vol);
options.volume = this.lavalinkVolume;
}
const track = Object.fromEntries(Object.entries({
encoded: options.track.encoded,
identifier: options.track.identifier,
}).filter(v => typeof v[1] !== "undefined"));
if (typeof options.track.userData === "object")
track.userData = {
...(options.track.userData || {})
};
if (typeof options?.track?.requester === "object")
track.userData = {
...(track.userData || {}),
requester: this.LavalinkManager.utils.getTransformedRequester(options?.track?.requester || {})
};
if (this.LavalinkManager.options?.advancedOptions?.enableDebugEvents) {
this.LavalinkManager.emit("debug", DebugEvents.PlayerPlayWithTrackReplace, {
state: "log",
message: `Player was called to play something, with a specific track provided. Replacing the current Track and resolving the track on trackStart Event.`,
functionLayer: "Player > play()",
});
}
return this.node.updatePlayer({
guildId: this.guildId,
noReplace: false,
playerOptions: Object.fromEntries(Object.entries({
track,
position: options.position ?? undefined,
paused: options.paused ?? undefined,
endTime: options?.endTime ?? undefined,
filters: options?.filters ?? undefined,
volume: options.volume ?? this.lavalinkVolume ?? undefined,
voice: options.voice ?? undefined,
}).filter(v => typeof v[1] !== "undefined")),
});
}
if (!this.queue.current && this.queue.tracks.length)
await queueTrackEnd(this);
if (this.queue.current && this.LavalinkManager.utils.isUnresolvedTrack(this.queue.current)) {
if (this.LavalinkManager.options?.advancedOptions?.enableDebugEvents) {
this.LavalinkManager.emit("debug", DebugEvents.PlayerPlayUnresolvedTrack, {
state: "log",
message: `Player Play was called, current Queue Song is unresolved, resolving the track.`,
functionLayer: "Player > play()",
});
}
try {
// resolve the unresolved track
await this.queue.current.resolve(this);
if (typeof options.track?.userData === "object" && this.queue.current)
this.queue.current.userData = { ...(this.queue.current?.userData || {}), ...(options.track?.userData || {}) };
}
catch (error) {
if (this.LavalinkManager.options?.advancedOptions?.enableDebugEvents) {
this.LavalinkManager.emit("debug", DebugEvents.PlayerPlayUnresolvedTrackFailed, {
state: "error",
error: error,
message: `Player Play was called, current Queue Song is unresolved, but couldn't resolve it`,
functionLayer: "Player > play() > resolve currentTrack",
});
}
this.LavalinkManager.emit("trackError", this, this.queue.current, error);
if (options && "clientTrack" in options)
delete options.clientTrack;
if (options && "track" in options)
delete options.track;
// try to play the next track if possible
if (this.LavalinkManager.options?.autoSkipOnResolveError === true && this.queue.tracks[0])
return this.play(options);
return this;
}
}
if (!this.queue.current)
throw new Error(`There is no Track in the Queue, nor provided in the PlayOptions`);
if (typeof options?.volume === "number" && !isNaN(options?.volume)) {
this.volume = Math.max(Math.min(options?.volume, 500), 0);
let vol = Number(this.volume);
if (this.LavalinkManager.options.playerOptions.volumeDecrementer)
vol *= this.LavalinkManager.options.playerOptions.volumeDecrementer;
this.lavalinkVolume = Math.round(vol);
options.volume = this.lavalinkVolume;
}
const finalOptions = Object.fromEntries(Object.entries({
track: {
encoded: this.queue.current?.encoded || null,
// identifier: options.identifier,
userData: options?.track?.userData || {},
},
volume: this.lavalinkVolume,
position: options?.position ?? 0,
endTime: options?.endTime ?? undefined,
filters: options?.filters ?? undefined,
paused: options?.paused ?? undefined,
voice: options?.voice ?? undefined
}).filter(v => typeof v[1] !== "undefined"));
if ((typeof finalOptions.position !== "undefined" && isNaN(finalOptions.position)) || (typeof finalOptions.position === "number" && (finalOptions.position < 0 || finalOptions.position >= this.queue.current.info.duration)))
throw new Error("PlayerOption#position must be a positive number, less than track's duration");
if ((typeof finalOptions.volume !== "undefined" && isNaN(finalOptions.volume) || (typeof finalOptions.volume === "number" && finalOptions.volume < 0)))
throw new Error("PlayerOption#volume must be a positive number");
if ((typeof finalOptions.endTime !== "undefined" && isNaN(finalOptions.endTime)) || (typeof finalOptions.endTime === "number" && (finalOptions.endTime < 0 || finalOptions.endTime >= this.queue.current.info.duration)))
throw new Error("PlayerOption#endTime must be a positive number, less than track's duration");
if (typeof finalOptions.position === "number" && typeof finalOptions.endTime === "number" && finalOptions.endTime < finalOptions.position)
throw new Error("PlayerOption#endTime must be bigger than PlayerOption#position");
const now = performance.now();
await this.node.updatePlayer({
guildId: this.guildId,
noReplace: (options?.noReplace ?? false),
playerOptions: finalOptions,
});
this.ping.lavalink = Math.round((performance.now() - now) / 10) / 100;
return this;
}
/**
* Set the Volume for the Player
* @param volume The Volume in percent
* @param ignoreVolumeDecrementer If it should ignore the volumedecrementer option
*/
async setVolume(volume, ignoreVolumeDecrementer = false) {
volume = Number(volume);
if (isNaN(volume))
throw new TypeError("Volume must be a number.");
this.volume = Math.round(Math.max(Math.min(volume, 1000), 0));
this.lavalinkVolume = Math.round(Math.max(Math.min(Math.round(this.LavalinkManager.options.playerOptions.volumeDecrementer && !ignoreVolumeDecrementer
? this.volume * this.LavalinkManager.options.playerOptions.volumeDecrementer
: this.volume), 1000), 0));
const now = performance.now();
if (this.LavalinkManager.options.playerOptions.applyVolumeAsFilter) {
if (this.LavalinkManager.options?.advancedOptions?.enableDebugEvents) {
this.LavalinkManager.emit("debug", DebugEvents.PlayerVolumeAsFilter, {
state: "log",
message: `Player Volume was set as a Filter, because LavalinkManager option "playerOptions.applyVolumeAsFilter" is true`,
functionLayer: "Player > setVolume()",
});
}
await this.node.updatePlayer({ guildId: this.guildId, playerOptions: { filters: { volume: this.lavalinkVolume / 100 } } });
}
else {
await this.node.updatePlayer({ guildId: this.guildId, playerOptions: { volume: this.lavalinkVolume } });
}
this.ping.lavalink = Math.round((performance.now() - now) / 10) / 100;
return this;
}
/**
* Search for a track
* @param query The query to search for
* @param requestUser The user that requested the track
* @param throwOnEmpty If an error should be thrown if no track is found
* @returns The search result
*/
async lavaSearch(query, requestUser, throwOnEmpty = false) {
return this.node.lavaSearch(query, requestUser, throwOnEmpty);
}
/**
* Set the SponsorBlock
* @param segments The segments to set
*/
async setSponsorBlock(segments = ["sponsor", "selfpromo"]) {
return this.node.setSponsorBlock(this, segments);
}
/**
* Get the SponsorBlock
*/
async getSponsorBlock() {
return this.node.getSponsorBlock(this);
}
/**
* Delete the SponsorBlock
*/
async deleteSponsorBlock() {
return this.node.deleteSponsorBlock(this);
}
/**
*
* @param query Query for your data
* @param requestUser
*/
async search(query, requestUser, throwOnEmpty = false) {
const Query = this.LavalinkManager.utils.transformQuery(query);
if (["bcsearch", "bandcamp"].includes(Query.source) && !this.node.info.sourceManagers.includes("bandcamp")) {
if (this.LavalinkManager.options?.advancedOptions?.enableDebugEvents) {
this.LavalinkManager.emit("debug", DebugEvents.BandcampSearchLokalEngine, {
state: "log",
message: `Player.search was called with a Bandcamp Query, but no bandcamp search was enabled on lavalink, searching with the custom Search Engine.`,
functionLayer: "Player > search()",
});
}
return await bandCampSearch(this, Query.query, requestUser);
}
return this.node.search(Query, requestUser, throwOnEmpty);
}
/**
* Pause the player
*/
async pause() {
if (this.paused && !this.playing)
throw new Error("Player is already paused - not able to pause.");
this.paused = true;
this.lastPositionChange = null; // needs to removed to not cause issues
const now = performance.now();
await this.node.updatePlayer({ guildId: this.guildId, playerOptions: { paused: true } });
this.ping.lavalink = Math.round((performance.now() - now) / 10) / 100;
return this;
}
/**
* Resume the Player
*/
async resume() {
if (!this.paused)
throw new Error("Player isn't paused - not able to resume.");
this.paused = false;
const now = performance.now();
await this.node.updatePlayer({ guildId: this.guildId, playerOptions: { paused: false } });
this.ping.lavalink = Math.round((performance.now() - now) / 10) / 100;
return this;
}
/**
* Seek to a specific Position
* @param position
*/
async seek(position) {
if (!this.queue.current)
return undefined;
position = Number(position);
if (isNaN(position))
throw new RangeError("Position must be a number.");
if (!this.queue.current.info.isSeekable || this.queue.current.info.isStream)
throw new RangeError("Current Track is not seekable / a stream");
if (position < 0 || position > this.queue.current.info.duration)
position = Math.max(Math.min(position, this.queue.current.info.duration), 0);
this.lastPositionChange = Date.now();
this.lastPosition = position;
const now = performance.now();
await this.node.updatePlayer({ guildId: this.guildId, playerOptions: { position } });
this.ping.lavalink = Math.round((performance.now() - now) / 10) / 100;
return this;
}
/**
* Set the Repeatmode of the Player
* @param repeatMode
*/
async setRepeatMode(repeatMode) {
if (!["off", "track", "queue"].includes(repeatMode))
throw new RangeError("Repeatmode must be either 'off', 'track', or 'queue'");
this.repeatMode = repeatMode;
return this;
}
/**
* Skip the current song, or a specific amount of songs
* @param amount provide the index of the next track to skip to
*/
async skip(skipTo = 0, throwError = true) {
if (!this.queue.tracks.length && (throwError || (typeof skipTo === "boolean" && skipTo === true)))
throw new RangeError("Can't skip more than the queue size");
if (typeof skipTo === "number" && skipTo > 1) {
if (skipTo > this.queue.tracks.length)
throw new RangeError("Can't skip more than the queue size");
await this.queue.splice(0, skipTo - 1);
}
if (!this.playing && !this.queue.current)
return (this.play(), this);
const now = performance.now();
this.set("internal_skipped", true);
await this.node.updatePlayer({ guildId: this.guildId, playerOptions: { track: { encoded: null }, paused: false } });
this.ping.lavalink = Math.round((performance.now() - now) / 10) / 100;
return this;
}
/**
* Clears the queue and stops playing. Does not destroy the Player and not leave the channel
* @returns
*/
async stopPlaying(clearQueue = true, executeAutoplay = false) {
// use internal_stopPlaying on true, so that it doesn't utilize current loop states. on trackEnd event
this.set("internal_stopPlaying", true);
// remove tracks from the queue
if (this.queue.tracks.length && clearQueue === true)
await this.queue.splice(0, this.queue.tracks.length);
if (executeAutoplay === false)
this.set("internal_autoplayStopPlaying", true);
else
this.set("internal_autoplayStopPlaying", undefined);
const now = performance.now();
// send to lavalink, that it should stop playing
await this.node.updatePlayer({ guildId: this.guildId, playerOptions: { track: { encoded: null } } });
this.ping.lavalink = Math.round((performance.now() - now) / 10) / 100;
return this;
}
/**
* Connects the Player to the Voice Channel
* @returns
*/
async connect() {
if (!this.options.voiceChannelId)
throw new RangeError("No Voice Channel id has been set. (player.options.voiceChannelId)");
await this.LavalinkManager.options.sendToShard(this.guildId, {
op: 4,
d: {
guild_id: this.guildId,
channel_id: this.options.voiceChannelId,
self_mute: this.options.selfMute ?? false,
self_deaf: this.options.selfDeaf ?? true,
}
});
this.voiceChannelId = this.options.voiceChannelId;
return this;
}
async changeVoiceState(data) {
if (this.options.voiceChannelId === data.voiceChannelId)
throw new RangeError("New Channel can't be equal to the old Channel.");
await this.LavalinkManager.options.sendToShard(this.guildId, {
op: 4,
d: {
guild_id: this.guildId,
channel_id: data.voiceChannelId,
self_mute: data.selfMute ?? this.options.selfMute ?? false,
self_deaf: data.selfDeaf ?? this.options.selfDeaf ?? true,
}
});
// override the options
this.options.voiceChannelId = data.voiceChannelId;
this.options.selfMute = data.selfMute;
this.options.selfDeaf = data.selfDeaf;
this.voiceChannelId = data.voiceChannelId;
return this;
}
/**
* Disconnects the Player from the Voice Channel, but keeps the player in the cache
* @param force If false it throws an error, if player thinks it's already disconnected
* @returns
*/
async disconnect(force = false) {
if (!force && !this.options.voiceChannelId)
throw new RangeError("No Voice Channel id has been set. (player.options.voiceChannelId)");
await this.LavalinkManager.options.sendToShard(this.guildId, {
op: 4,
d: {
guild_id: this.guildId,
channel_id: null,
self_mute: false,
self_deaf: false,
}
});
this.voiceChannelId = null;
return this;
}
/**
* Destroy the player and disconnect from the voice channel
*/
async destroy(reason, disconnect = true) {
if (this.LavalinkManager.options.advancedOptions?.debugOptions.playerDestroy.debugLog)
console.log(`Lavalink-Client-Debug | PlayerDestroy [::] destroy Function, [guildId ${this.guildId}] - Destroy-Reason: ${String(reason)}`);
if (this.get("internal_queueempty")) {
clearTimeout(this.get("internal_queueempty"));
this.set("internal_queueempty", undefined);
}
if (this.get("internal_destroystatus") === true) {
if (this.LavalinkManager.options?.advancedOptions?.enableDebugEvents) {
this.LavalinkManager.emit("debug", DebugEvents.PlayerDestroyingSomewhereElse, {
state: "warn",
message: `Player is already destroying somewhere else..`,
functionLayer: "Player > destroy()",
});
}
if (this.LavalinkManager.options.advancedOptions?.debugOptions.playerDestroy.debugLog)
console.log(`Lavalink-Client-Debug | PlayerDestroy [::] destroy Function, [guildId ${this.guildId}] - Already destroying somewhere else..`);
return;
}
this.set("internal_destroystatus", true);
// disconnect player and set VoiceChannel to Null
if (disconnect)
await this.disconnect(true);
else
this.set("internal_destroywithoutdisconnect", true);
// Destroy the queue
await this.queue.utils.destroy();
// delete the player from cache
this.LavalinkManager.deletePlayer(this.guildId);
// destroy the player on lavalink side
await this.node.destroyPlayer(this.guildId);
if (this.LavalinkManager.options.advancedOptions?.debugOptions.playerDestroy.debugLog)
console.log(`Lavalink-Client-Debug | PlayerDestroy [::] destroy Function, [guildId ${this.guildId}] - Player got destroyed successfully`);
// emit the event
this.LavalinkManager.emit("playerDestroy", this, reason);
// return smt
return this;
}
/**
* Get the current lyrics of the track currently playing on the guild
* @param guildId The guild id to get the current lyrics for
* @param skipTrackSource If true, it will not try to get the lyrics from the track source
* @returns The current lyrics
* @example
* ```ts
* const lyrics = await player.getCurrentLyrics();
* ```
*/
async getCurrentLyrics(skipTrackSource) {
return await this.node.lyrics.getCurrent(this.guildId, skipTrackSource);
}
/**
* Get the lyrics of a specific track
* @param track The track to get the lyrics for
* @param skipTrackSource If true, it will not try to get the lyrics from the track source
* @returns The lyrics of the track
* @example
* ```ts
* const lyrics = await player.getLyrics(player.queue.tracks[0], true);
* ```
*/
async getLyrics(track, skipTrackSource) {
return await this.node.lyrics.get(track, skipTrackSource);
}
/**
* Subscribe to the lyrics event on a specific guild to active live lyrics events
* @param guildId The guild id to subscribe to
* @returns The unsubscribe function
* @example
* ```ts
* const lyrics = await player.subscribeLyrics();
* ```
*/
subscribeLyrics() {
return this.node.lyrics.subscribe(this.guildId);
}
/**
* Unsubscribe from the lyrics event on a specific guild to disable live lyrics events
* @param guildId The guild id to unsubscribe from
* @returns The unsubscribe function
* @example
* ```ts
* const lyrics = await player.unsubscribeLyrics();
* ```
*/
unsubscribeLyrics(guildId) {
return this.node.lyrics.unsubscribe(guildId);
}
/**
* Move the player on a different Audio-Node
* @param newNode New Node / New Node Id
* @param checkSources If it should check if the sources are supported by the new node
*/
async changeNode(newNode, checkSources = true) {
const updateNode = typeof newNode === "string" ? this.LavalinkManager.nodeManager.nodes.get(newNode) : newNode;
if (!updateNode)
throw new Error("Could not find the new Node");
if (!updateNode.connected)
throw new Error("The provided Node is not active or disconnected");
if (this.node.id === updateNode.id)
throw new Error("Player is already on the provided Node");
if (this.get("internal_nodeChanging") === true)
throw new Error("Player is already changing the node please wait");
if (checkSources) {
const isDefaultSource = () => {
try {
this.LavalinkManager.utils.validateSourceString(updateNode, this.LavalinkManager.options.playerOptions.defaultSearchPlatform);
return true;
}
catch {
return false;
}
};
if (!isDefaultSource())
throw new RangeError(`defaultSearchPlatform "${this.LavalinkManager.options.playerOptions.defaultSearchPlatform}" is not supported by the newNode`);
if (this.queue.current || this.queue.tracks.length) { // Check if all queued track sources are supported by the new node
const trackSources = new Set([this.queue.current, ...this.queue.tracks].map(track => track.info.sourceName));
const missingSources = [...trackSources].filter(source => !updateNode.info.sourceManagers.includes(source));
if (missingSources.length)
throw new RangeError(`Sources missing for Node ${updateNode.id}: ${missingSources.join(', ')}`);
}
}
if (this.LavalinkManager.options?.advancedOptions?.enableDebugEvents) {
this.LavalinkManager.emit("debug", DebugEvents.PlayerChangeNode, {
state: "log",
message: `Player.changeNode() was executed, trying to change from "${this.node.id}" to "${updateNode.id}"`,
functionLayer: "Player > changeNode()",
});
}
const data = this.toJSON();
const currentTrack = this.queue.current;
const voiceData = this.voice;
if (!voiceData.endpoint ||
!voiceData.sessionId ||
!voiceData.token)
throw new Error("Voice Data is missing, can't change the node");
this.set("internal_nodeChanging", true); // This will stop execution of trackEnd or queueEnd event while changing the node
if (this.node.connected)
await this.node.destroyPlayer(this.guildId); // destroy the player on the currentNode if it's connected
this.node = updateNode;
const now = performance.now();
try {
await this.connect();
const endpoint = `/sessions/${this.node.sessionId}/players/${this.guildId}`; //Send the VoiceData to the newly connected node.
await this.node.request(endpoint, r => {
r.method = "PATCH";
r.headers["Content-Type"] = "application/json";
r.body = JSON.stringify({
voice: {
token: voiceData.token,
endpoint: voiceData.endpoint,
sessionId: voiceData.sessionId
}
});
});
if (currentTrack) { // If there is a current track, send it to the new node.
await this.node.updatePlayer({
guildId: this.guildId,
noReplace: false,
playerOptions: {
track: currentTrack ?? null,
position: currentTrack ? data.position : 0,
volume: data.lavalinkVolume,
paused: data.paused,
//filters: { ...data.filters, equalizer: data.equalizer }, Sending filters on nodeChange causes issues (player gets dicsonnected)
}
});
}
this.ping.lavalink = Math.round((performance.now() - now) / 10) / 100;
return this.node.id;
}
catch (error) {
if (this.LavalinkManager.options?.advancedOptions?.enableDebugEvents) {
this.LavalinkManager.emit("debug", DebugEvents.PlayerChangeNode, {
state: "error",
error: error,
message: `Player.changeNode() execution failed`,
functionLayer: "Player > changeNode()",
});
}
throw new Error(`Failed to change the node: ${error}`);
}
finally {
this.set("internal_nodeChanging", undefined);
}
}
/** Converts the Player including Queue to a Json state */
toJSON() {
return {
guildId: this.guildId,
options: this.options,
voiceChannelId: this.voiceChannelId,
textChannelId: this.textChannelId,
position: this.position,
lastPosition: this.lastPosition,
lastPositionChange: this.lastPositionChange,
volume: this.volume,
lavalinkVolume: this.lavalinkVolume,
repeatMode: this.repeatMode,
paused: this.paused,
playing: this.playing,
createdTimeStamp: this.createdTimeStamp,
filters: this.filterManager?.data || {},
equalizer: this.filterManager?.equalizerBands || [],
nodeId: this.node?.id,
nodeSessionId: this.node?.sessionId,
ping: this.ping,
queue: this.queue.utils.toJSON(),
};
}
}