lavalink-client
Version:
Easy, flexible and feature-rich lavalink@v4 Client. Both for Beginners and Proficients.
526 lines (525 loc) • 29.9 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.MiniMap = exports.ManagerUtils = exports.NodeSymbol = exports.QueueSymbol = exports.UnresolvedTrackSymbol = exports.TrackSymbol = void 0;
exports.parseLavalinkConnUrl = parseLavalinkConnUrl;
exports.queueTrackEnd = queueTrackEnd;
const node_url_1 = require("node:url");
const types_1 = require("node:util/types");
const Constants_1 = require("./Constants.js");
const LavalinkManagerStatics_1 = require("./LavalinkManagerStatics.js");
exports.TrackSymbol = Symbol("LC-Track");
exports.UnresolvedTrackSymbol = Symbol("LC-Track-Unresolved");
exports.QueueSymbol = Symbol("LC-Queue");
exports.NodeSymbol = Symbol("LC-Node");
/** @hidden */
const escapeRegExp = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
/**
* Parses Node Connection Url: "lavalink://<nodeId>:<nodeAuthorization(Password)>@<NodeHost>:<NodePort>"
* @param connectionUrl
* @returns
*/
function parseLavalinkConnUrl(connectionUrl) {
if (!connectionUrl.startsWith("lavalink://"))
throw new Error(`ConnectionUrl (${connectionUrl}) must start with 'lavalink://'`);
const parsed = new node_url_1.URL(connectionUrl);
return {
authorization: parsed.password,
id: parsed.username,
host: parsed.hostname,
port: Number(parsed.port),
};
}
class ManagerUtils {
LavalinkManager = undefined;
constructor(LavalinkManager) {
this.LavalinkManager = LavalinkManager;
}
buildPluginInfo(data, clientData = {}) {
return {
clientData: clientData,
...(data.pluginInfo || data.plugin),
};
}
buildTrack(data, requester) {
if (!data?.encoded || typeof data.encoded !== "string")
throw new RangeError("Argument 'data.encoded' must be present.");
if (!data.info)
throw new RangeError("Argument 'data.info' must be present.");
try {
let transformedRequester = typeof requester === "object"
? this.getTransformedRequester(requester)
: undefined;
if (!transformedRequester && typeof data?.userData?.requester === "object" && data.userData.requester !== null) {
transformedRequester = this.getTransformedRequester(data.userData.requester);
}
const r = {
encoded: data.encoded,
info: {
identifier: data.info.identifier,
title: data.info.title,
author: data.info.author,
duration: data.info?.duration || data.info?.length,
artworkUrl: data.info.artworkUrl || data.pluginInfo?.artworkUrl || data.plugin?.artworkUrl,
uri: data.info.uri,
sourceName: data.info.sourceName,
isSeekable: data.info.isSeekable,
isStream: data.info.isStream,
isrc: data.info.isrc,
},
userData: {
...data.userData,
requester: transformedRequester
},
pluginInfo: this.buildPluginInfo(data, "clientData" in data ? data.clientData : {}),
requester: transformedRequester || this.getTransformedRequester(this.LavalinkManager?.options?.client),
};
Object.defineProperty(r, exports.TrackSymbol, { configurable: true, value: true });
return r;
}
catch (error) {
if (this.LavalinkManager?.options?.advancedOptions?.enableDebugEvents) {
this.LavalinkManager?.emit("debug", Constants_1.DebugEvents.BuildTrackError, {
error: error,
functionLayer: "ManagerUtils > buildTrack()",
message: "Error while building track",
state: "error",
});
}
throw new RangeError(`Argument "data" is not a valid track: ${error.message}`);
}
}
/**
* Builds a UnresolvedTrack to be resolved before being played .
* @param query
* @param requester
*/
buildUnresolvedTrack(query, requester) {
if (typeof query === "undefined")
throw new RangeError('Argument "query" must be present.');
const unresolvedTrack = {
encoded: query.encoded || undefined,
info: query.info ? query.info : query.title ? query : undefined,
pluginInfo: this.buildPluginInfo(query),
requester: this.getTransformedRequester(requester),
async resolve(player) {
const closest = await getClosestTrack(this, player);
if (!closest)
throw new SyntaxError("No closest Track found");
for (const prop of Object.getOwnPropertyNames(this))
delete this[prop];
// delete symbol
delete this[exports.UnresolvedTrackSymbol];
// assign new symbol
Object.defineProperty(this, exports.TrackSymbol, { configurable: true, value: true });
return Object.assign(this, closest);
}
};
if (!this.isUnresolvedTrack(unresolvedTrack))
throw SyntaxError("Could not build Unresolved Track");
Object.defineProperty(unresolvedTrack, exports.UnresolvedTrackSymbol, { configurable: true, value: true });
return unresolvedTrack;
}
/**
* Validate if a data is equal to a node
* @param data
*/
isNode(data) {
if (!data)
return false;
const keys = Object.getOwnPropertyNames(Object.getPrototypeOf(data));
if (!keys.includes("constructor"))
return false;
if (!keys.length)
return false;
// all required functions
if (!["connect", "destroy", "destroyPlayer", "fetchAllPlayers", "fetchInfo", "fetchPlayer", "fetchStats", "fetchVersion", "request", "updatePlayer", "updateSession"].every(v => keys.includes(v)))
return false;
return true;
}
getTransformedRequester(requester) {
try {
return typeof this.LavalinkManager?.options?.playerOptions?.requesterTransformer === "function"
? this.LavalinkManager?.options?.playerOptions?.requesterTransformer(requester)
: requester;
}
catch (e) {
if (this.LavalinkManager?.options?.advancedOptions?.enableDebugEvents) {
this.LavalinkManager?.emit("debug", Constants_1.DebugEvents.TransformRequesterFunctionFailed, {
error: e,
functionLayer: "ManagerUtils > getTransformedRequester()",
message: "Your custom transformRequesterFunction failed to execute, please check your function for errors.",
state: "error",
});
}
return requester;
}
}
/**
* Validate if a data is equal to node options
* @param data
*/
isNodeOptions(data) {
if (!data || typeof data !== "object" || Array.isArray(data))
return false;
if (typeof data.host !== "string" || !data.host.length)
return false;
if (typeof data.port !== "number" || isNaN(data.port) || data.port < 0 || data.port > 65535)
return false;
if (typeof data.authorization !== "string" || !data.authorization.length)
return false;
if ("secure" in data && typeof data.secure !== "boolean" && data.secure !== undefined)
return false;
if ("sessionId" in data && typeof data.sessionId !== "string" && data.sessionId !== undefined)
return false;
if ("id" in data && typeof data.id !== "string" && data.id !== undefined)
return false;
if ("regions" in data && (!Array.isArray(data.regions) || !data.regions.every(v => typeof v === "string") && data.regions !== undefined))
return false;
if ("poolOptions" in data && typeof data.poolOptions !== "object" && data.poolOptions !== undefined)
return false;
if ("retryAmount" in data && (typeof data.retryAmount !== "number" || isNaN(data.retryAmount) || data.retryAmount <= 0 && data.retryAmount !== undefined))
return false;
if ("retryDelay" in data && (typeof data.retryDelay !== "number" || isNaN(data.retryDelay) || data.retryDelay <= 0 && data.retryDelay !== undefined))
return false;
if ("requestTimeout" in data && (typeof data.requestTimeout !== "number" || isNaN(data.requestTimeout) || data.requestTimeout <= 0 && data.requestTimeout !== undefined))
return false;
return true;
}
/**
* Validate if a data is equal to a track
* @param data the Track to validate
* @returns
*/
isTrack(data) {
if (!data)
return false;
if (data[exports.TrackSymbol] === true)
return true;
return typeof data?.encoded === "string" && typeof data?.info === "object" && !("resolve" in data);
}
/**
* Checks if the provided argument is a valid UnresolvedTrack.
* @param track
*/
isUnresolvedTrack(data) {
if (!data)
return false;
if (data[exports.UnresolvedTrackSymbol] === true)
return true;
return typeof data === "object" && (("info" in data && typeof data.info.title === "string") || typeof data.encoded === "string") && "resolve" in data && typeof data.resolve === "function";
}
/**
* Checks if the provided argument is a valid UnresolvedTrack.
* @param track
*/
isUnresolvedTrackQuery(data) {
return typeof data === "object" && !("info" in data) && typeof data.title === "string";
}
async getClosestTrack(data, player) {
try {
return getClosestTrack(data, player);
}
catch (e) {
if (this.LavalinkManager?.options?.advancedOptions?.enableDebugEvents) {
this.LavalinkManager?.emit("debug", Constants_1.DebugEvents.GetClosestTrackFailed, {
error: e,
functionLayer: "ManagerUtils > getClosestTrack()",
message: "Failed to resolve track because the getClosestTrack function failed.",
state: "error",
});
}
throw e;
}
}
validateQueryString(node, queryString, sourceString) {
if (!node.info)
throw new Error("No Lavalink Node was provided");
if (!node.info.sourceManagers?.length)
throw new Error("Lavalink Node, has no sourceManagers enabled");
if (!queryString.trim().length)
throw new Error(`Query string is empty, please provide a valid query string.`);
if (sourceString === "speak" && queryString.length > 100)
throw new Error(`Query is speak, which is limited to 100 characters.`);
// checks for blacklisted links / domains / queries
if (this.LavalinkManager.options?.linksBlacklist?.length > 0) {
if (this.LavalinkManager.options?.advancedOptions?.enableDebugEvents) {
this.LavalinkManager.emit("debug", Constants_1.DebugEvents.ValidatingBlacklistLinks, {
state: "log",
message: `Validating Query against LavalinkManager.options.linksBlacklist, query: "${queryString}"`,
functionLayer: "(LavalinkNode > node | player) > search() > validateQueryString()",
});
}
if (this.LavalinkManager.options?.linksBlacklist.some(v => (typeof v === "string" && (queryString.toLowerCase().includes(v.toLowerCase()) || v.toLowerCase().includes(queryString.toLowerCase()))) || (0, types_1.isRegExp)(v) && v.test(queryString))) {
throw new Error(`Query string contains a link / word which is blacklisted.`);
}
}
if (!/^https?:\/\//.test(queryString))
return;
else if (this.LavalinkManager.options?.linksAllowed === false)
throw new Error("Using links to make a request is not allowed.");
// checks for if the query is whitelisted (should only work for links, so it skips the check for no link queries)
if (this.LavalinkManager.options?.linksWhitelist?.length > 0) {
if (this.LavalinkManager.options?.advancedOptions?.enableDebugEvents) {
this.LavalinkManager.emit("debug", Constants_1.DebugEvents.ValidatingWhitelistLinks, {
state: "log",
message: `Link was provided to the Query, validating against LavalinkManager.options.linksWhitelist, query: "${queryString}"`,
functionLayer: "(LavalinkNode > node | player) > search() > validateQueryString()",
});
}
if (!this.LavalinkManager.options?.linksWhitelist.some(v => (typeof v === "string" && (queryString.toLowerCase().includes(v.toLowerCase()) || v.toLowerCase().includes(queryString.toLowerCase()))) || (0, types_1.isRegExp)(v) && v.test(queryString))) {
throw new Error(`Query string contains a link / word which isn't whitelisted.`);
}
}
// missing links: beam.pro local getyarn.io clypit pornhub reddit ocreamix soundgasm
if ((LavalinkManagerStatics_1.SourceLinksRegexes.YoutubeMusicRegex.test(queryString) || LavalinkManagerStatics_1.SourceLinksRegexes.YoutubeRegex.test(queryString)) && !node.info?.sourceManagers?.includes("youtube")) {
throw new Error("Query / Link Provided for this Source but Lavalink Node has not 'youtube' enabled");
}
if ((LavalinkManagerStatics_1.SourceLinksRegexes.SoundCloudMobileRegex.test(queryString) || LavalinkManagerStatics_1.SourceLinksRegexes.SoundCloudRegex.test(queryString)) && !node.info?.sourceManagers?.includes("soundcloud")) {
throw new Error("Query / Link Provided for this Source but Lavalink Node has not 'soundcloud' enabled");
}
if (LavalinkManagerStatics_1.SourceLinksRegexes.bandcamp.test(queryString) && !node.info?.sourceManagers?.includes("bandcamp")) {
throw new Error("Query / Link Provided for this Source but Lavalink Node has not 'bandcamp' enabled (introduced with lavaplayer 2.2.0 or lavalink 4.0.6)");
}
if (LavalinkManagerStatics_1.SourceLinksRegexes.TwitchTv.test(queryString) && !node.info?.sourceManagers?.includes("twitch")) {
throw new Error("Query / Link Provided for this Source but Lavalink Node has not 'twitch' enabled");
}
if (LavalinkManagerStatics_1.SourceLinksRegexes.vimeo.test(queryString) && !node.info?.sourceManagers?.includes("vimeo")) {
throw new Error("Query / Link Provided for this Source but Lavalink Node has not 'vimeo' enabled");
}
if (LavalinkManagerStatics_1.SourceLinksRegexes.tiktok.test(queryString) && !node.info?.sourceManagers?.includes("tiktok")) {
throw new Error("Query / Link Provided for this Source but Lavalink Node has not 'tiktok' enabled");
}
if (LavalinkManagerStatics_1.SourceLinksRegexes.mixcloud.test(queryString) && !node.info?.sourceManagers?.includes("mixcloud")) {
throw new Error("Query / Link Provided for this Source but Lavalink Node has not 'mixcloud' enabled");
}
if (LavalinkManagerStatics_1.SourceLinksRegexes.AllSpotifyRegex.test(queryString) && !node.info?.sourceManagers?.includes("spotify")) {
throw new Error("Query / Link Provided for this Source but Lavalink Node has not 'spotify' enabled");
}
if (LavalinkManagerStatics_1.SourceLinksRegexes.appleMusic.test(queryString) && !node.info?.sourceManagers?.includes("applemusic")) {
throw new Error("Query / Link Provided for this Source but Lavalink Node has not 'applemusic' enabled");
}
if (LavalinkManagerStatics_1.SourceLinksRegexes.AllDeezerRegex.test(queryString) && !node.info?.sourceManagers?.includes("deezer")) {
throw new Error("Query / Link Provided for this Source but Lavalink Node has not 'deezer' enabled");
}
if (LavalinkManagerStatics_1.SourceLinksRegexes.musicYandex.test(queryString) && !node.info?.sourceManagers?.includes("yandexmusic")) {
throw new Error("Query / Link Provided for this Source but Lavalink Node has not 'yandexmusic' enabled");
}
if (LavalinkManagerStatics_1.SourceLinksRegexes.jiosaavn.test(queryString) && !node.info?.sourceManagers?.includes("jiosaavn")) {
throw new Error("Query / Link Provided for this Source but Lavalink Node has not 'jiosaavn' (via jiosaavn-plugin) enabled");
}
return;
}
transformQuery(query) {
const sourceOfQuery = typeof query === "string" ? undefined : (LavalinkManagerStatics_1.DefaultSources[(query.source?.trim?.()?.toLowerCase?.()) ?? this.LavalinkManager?.options?.playerOptions?.defaultSearchPlatform?.toLowerCase?.()] ?? (query.source?.trim?.()?.toLowerCase?.()));
const Query = {
query: typeof query === "string" ? query : query.query,
extraQueryUrlParams: typeof query !== "string" ? query.extraQueryUrlParams : undefined,
source: sourceOfQuery ?? this.LavalinkManager?.options?.playerOptions?.defaultSearchPlatform?.toLowerCase?.()
};
const foundSource = Object.keys(LavalinkManagerStatics_1.DefaultSources).find(source => Query.query?.toLowerCase?.()?.startsWith(`${source}:`.toLowerCase()))?.trim?.()?.toLowerCase?.();
// ignore links...
if (foundSource && !["https", "http"].includes(foundSource) && LavalinkManagerStatics_1.DefaultSources[foundSource]) {
Query.source = LavalinkManagerStatics_1.DefaultSources[foundSource]; // set the source to ytsearch:
Query.query = Query.query.slice(`${foundSource}:`.length, Query.query.length); // remove ytsearch: from the query
}
return Query;
}
transformLavaSearchQuery(query) {
// transform the query object
const sourceOfQuery = typeof query === "string" ? undefined : (LavalinkManagerStatics_1.DefaultSources[(query.source?.trim?.()?.toLowerCase?.()) ?? this.LavalinkManager?.options?.playerOptions?.defaultSearchPlatform?.toLowerCase?.()] ?? (query.source?.trim?.()?.toLowerCase?.()));
const Query = {
query: typeof query === "string" ? query : query.query,
types: query.types ? ["track", "playlist", "artist", "album", "text"].filter(v => query.types?.find(x => x.toLowerCase().startsWith(v))) : ["track", "playlist", "artist", "album", /*"text"*/],
source: sourceOfQuery ?? this.LavalinkManager?.options?.playerOptions?.defaultSearchPlatform?.toLowerCase?.()
};
const foundSource = Object.keys(LavalinkManagerStatics_1.DefaultSources).find(source => Query.query.toLowerCase().startsWith(`${source}:`.toLowerCase()))?.trim?.()?.toLowerCase?.();
if (foundSource && LavalinkManagerStatics_1.DefaultSources[foundSource]) {
Query.source = LavalinkManagerStatics_1.DefaultSources[foundSource]; // set the source to ytsearch:
Query.query = Query.query.slice(`${foundSource}:`.length, Query.query.length); // remove ytsearch: from the query
}
return Query;
}
validateSourceString(node, sourceString) {
if (!sourceString)
throw new Error(`No SourceString was provided`);
const source = LavalinkManagerStatics_1.DefaultSources[sourceString.toLowerCase().trim()];
if (!source)
throw new Error(`Lavalink Node SearchQuerySource: '${sourceString}' is not available`);
if (!node.info)
throw new Error("Lavalink Node does not have any info cached yet, not ready yet!");
if (source === "amsearch" && !node.info?.sourceManagers?.includes("applemusic")) {
throw new Error("Lavalink Node has not 'applemusic' enabled, which is required to have 'amsearch' work");
}
if (source === "dzisrc" && !node.info?.sourceManagers?.includes("deezer")) {
throw new Error("Lavalink Node has not 'deezer' enabled, which is required to have 'dzisrc' work");
}
if (source === "dzsearch" && !node.info?.sourceManagers?.includes("deezer")) {
throw new Error("Lavalink Node has not 'deezer' enabled, which is required to have 'dzsearch' work");
}
if (source === "dzisrc" && node.info?.sourceManagers?.includes("deezer") && !node.info?.sourceManagers?.includes("http")) {
throw new Error("Lavalink Node has not 'http' enabled, which is required to have 'dzisrc' to work");
}
if (source === "jsrec" && !node.info?.sourceManagers?.includes("jiosaavn")) {
throw new Error("Lavalink Node has not 'jiosaavn' (via jiosaavn-plugin) enabled, which is required to have 'jsrec' to work");
}
if (source === "jssearch" && !node.info?.sourceManagers?.includes("jiosaavn")) {
throw new Error("Lavalink Node has not 'jiosaavn' (via jiosaavn-plugin) enabled, which is required to have 'jssearch' to work");
}
if (source === "scsearch" && !node.info?.sourceManagers?.includes("soundcloud")) {
throw new Error("Lavalink Node has not 'soundcloud' enabled, which is required to have 'scsearch' work");
}
if (source === "speak" && !node.info?.plugins?.find(c => c.name.toLowerCase().includes(LavalinkManagerStatics_1.LavalinkPlugins.DuncteBot_Plugin.toLowerCase()))) {
throw new Error("Lavalink Node has not 'speak' enabled, which is required to have 'speak' work");
}
if (source === "tts" && !node.info?.plugins?.find(c => c.name.toLowerCase().includes(LavalinkManagerStatics_1.LavalinkPlugins.GoogleCloudTTS.toLowerCase()))) {
throw new Error("Lavalink Node has not 'tts' enabled, which is required to have 'tts' work");
}
if (source === "ftts" && !(node.info?.sourceManagers?.includes("ftts") || node.info?.sourceManagers?.includes("flowery-tts") || node.info?.sourceManagers?.includes("flowerytts"))) {
throw new Error("Lavalink Node has not 'flowery-tts' enabled, which is required to have 'ftts' work");
}
if (source === "ymsearch" && !node.info?.sourceManagers?.includes("yandexmusic")) {
throw new Error("Lavalink Node has not 'yandexmusic' enabled, which is required to have 'ymsearch' work");
}
if (source === "ytmsearch" && !node.info.sourceManagers?.includes("youtube")) {
throw new Error("Lavalink Node has not 'youtube' enabled, which is required to have 'ytmsearch' work");
}
if (source === "ytsearch" && !node.info?.sourceManagers?.includes("youtube")) {
throw new Error("Lavalink Node has not 'youtube' enabled, which is required to have 'ytsearch' work");
}
return;
}
}
exports.ManagerUtils = ManagerUtils;
class MiniMap extends Map {
constructor(data = []) {
super(data);
}
filter(fn, thisArg) {
if (typeof thisArg !== 'undefined')
fn = fn.bind(thisArg);
const results = new this.constructor[Symbol.species]();
for (const [key, val] of this) {
if (fn(val, key, this))
results.set(key, val);
}
return results;
}
toJSON() {
return [...this.entries()];
}
map(fn, thisArg) {
if (typeof thisArg !== 'undefined')
fn = fn.bind(thisArg);
const iter = this.entries();
return Array.from({ length: this.size }, () => {
const [key, value] = iter.next().value;
return fn(value, key, this);
});
}
}
exports.MiniMap = MiniMap;
async function queueTrackEnd(player) {
if (player.queue.current && !player.queue.current?.pluginInfo?.clientData?.previousTrack) { // If there was a current Track already and repeatmode === true, add it to the queue.
player.queue.previous.unshift(player.queue.current);
if (player.queue.previous.length > player.queue.options.maxPreviousTracks)
player.queue.previous.splice(player.queue.options.maxPreviousTracks, player.queue.previous.length);
await player.queue.utils.save();
}
// and if repeatMode == queue, add it back to the queue!
if (player.repeatMode === "queue" && player.queue.current)
player.queue.tracks.push(player.queue.current);
// change the current Track to the next upcoming one
const nextSong = player.queue.tracks.shift();
try {
if (player.LavalinkManager.utils.isUnresolvedTrack(nextSong))
await nextSong.resolve(player);
player.queue.current = nextSong || null;
// save it in the DB
await player.queue.utils.save();
}
catch (error) {
if (player.LavalinkManager.options?.advancedOptions?.enableDebugEvents) {
player.LavalinkManager.emit("debug", Constants_1.DebugEvents.PlayerPlayUnresolvedTrackFailed, {
state: "error",
error: error,
message: `queueTrackEnd Util was called, tried to resolve the next track, but failed to find the closest matching song`,
functionLayer: "Player > play() > resolve currentTrack",
});
}
player.LavalinkManager.emit("trackError", player, player.queue.current, error);
// try to play the next track if possible
if (player.LavalinkManager.options?.autoSkipOnResolveError === true && player.queue.tracks[0])
return queueTrackEnd(player);
}
// return the new current Track
return player.queue.current;
}
async function applyUnresolvedData(resTrack, data, utils) {
if (!resTrack?.info || !data?.info)
return;
if (data.info.uri)
resTrack.info.uri = data.info.uri;
if (utils?.LavalinkManager?.options?.playerOptions?.useUnresolvedData === true) { // overwrite values
if (data.info.artworkUrl?.length)
resTrack.info.artworkUrl = data.info.artworkUrl;
if (data.info.title?.length)
resTrack.info.title = data.info.title;
if (data.info.author?.length)
resTrack.info.author = data.info.author;
}
else { // only overwrite if undefined / invalid
if ((resTrack.info.title === 'Unknown title' || resTrack.info.title === "Unspecified description") && resTrack.info.title != data.info.title)
resTrack.info.title = data.info.title;
if (resTrack.info.author !== data.info.author)
resTrack.info.author = data.info.author;
if (resTrack.info.artworkUrl !== data.info.artworkUrl)
resTrack.info.artworkUrl = data.info.artworkUrl;
}
for (const key of Object.keys(data.info))
if (typeof resTrack.info[key] === "undefined" && key !== "resolve" && data.info[key])
resTrack.info[key] = data.info[key]; // add non-existing values
return resTrack;
}
async function getClosestTrack(data, player) {
if (!player || !player.node)
throw new RangeError("No player with a lavalink node was provided");
if (player.LavalinkManager.utils.isTrack(data))
return player.LavalinkManager.utils.buildTrack(data, data.requester);
if (!player.LavalinkManager.utils.isUnresolvedTrack(data))
throw new RangeError("Track is not an unresolved Track");
if (!data?.info?.title && typeof data.encoded !== "string" && !data.info.uri)
throw new SyntaxError("the track uri / title / encoded Base64 string is required for unresolved tracks");
if (!data.requester)
throw new SyntaxError("The requester is required");
// try to decode the track, if possible
if (typeof data.encoded === "string") {
const r = await player.node.decode.singleTrack(data.encoded, data.requester);
if (r)
return applyUnresolvedData(r, data, player.LavalinkManager.utils);
}
// try to fetch the track via a uri if possible
if (typeof data.info.uri === "string") {
const r = await player.search({ query: data?.info?.uri }, data.requester).then(v => v.tracks?.[0]);
if (r)
return applyUnresolvedData(r, data, player.LavalinkManager.utils);
}
// search the track as closely as possible
const query = [data.info?.title, data.info?.author].filter(str => !!str).join(" by ");
const sourceName = data.info?.sourceName;
return await player.search({
query, source: sourceName !== "twitch" && sourceName !== "flowery-tts" ? sourceName : player.LavalinkManager.options?.playerOptions?.defaultSearchPlatform,
}, data.requester).then((res) => {
let trackToUse = null;
// try to find via author name
if (data.info.author && !trackToUse)
trackToUse = res.tracks.find(track => [data.info?.author || "", `${data.info?.author} - Topic`].some(name => new RegExp(`^${escapeRegExp(name)}$`, "i").test(track.info?.author)) || new RegExp(`^${escapeRegExp(data.info?.title)}$`, "i").test(track.info?.title));
// try to find via duration
if (data.info.duration && !trackToUse)
trackToUse = res.tracks.find(track => (track.info?.duration >= (data.info?.duration - 1500)) && (track?.info.duration <= (data.info?.duration + 1500)));
// try to find via isrc
if (data.info.isrc && !trackToUse)
trackToUse = res.tracks.find(track => track.info?.isrc === data.info?.isrc);
// apply unresolved data and return the track
return applyUnresolvedData(trackToUse || res.tracks[0], data, player.LavalinkManager.utils);
});
}