erela.js
Version:
An easy-to-use Lavalink client for NodeJS.
208 lines (207 loc) • 7.65 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.Plugin = exports.Structure = exports.TrackUtils = void 0;
/** @hidden */
const TRACK_SYMBOL = Symbol("track"),
/** @hidden */
UNRESOLVED_TRACK_SYMBOL = Symbol("unresolved"), SIZES = [
"0",
"1",
"2",
"3",
"default",
"mqdefault",
"hqdefault",
"maxresdefault",
];
/** @hidden */
const escapeRegExp = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
class TrackUtils {
static trackPartial = null;
static manager;
/** @hidden */
static init(manager) {
this.manager = manager;
}
static setTrackPartial(partial) {
if (!Array.isArray(partial) || !partial.every(str => typeof str === "string"))
throw new Error("Provided partial is not an array or not a string array.");
if (!partial.includes("track"))
partial.unshift("track");
this.trackPartial = partial;
}
/**
* Checks if the provided argument is a valid Track or UnresolvedTrack, if provided an array then every element will be checked.
* @param trackOrTracks
*/
static validate(trackOrTracks) {
if (typeof trackOrTracks === "undefined")
throw new RangeError("Provided argument must be present.");
if (Array.isArray(trackOrTracks) && trackOrTracks.length) {
for (const track of trackOrTracks) {
if (!(track[TRACK_SYMBOL] || track[UNRESOLVED_TRACK_SYMBOL]))
return false;
}
return true;
}
return (trackOrTracks[TRACK_SYMBOL] ||
trackOrTracks[UNRESOLVED_TRACK_SYMBOL]) === true;
}
/**
* Checks if the provided argument is a valid UnresolvedTrack.
* @param track
*/
static isUnresolvedTrack(track) {
if (typeof track === "undefined")
throw new RangeError("Provided argument must be present.");
return track[UNRESOLVED_TRACK_SYMBOL] === true;
}
/**
* Checks if the provided argument is a valid Track.
* @param track
*/
static isTrack(track) {
if (typeof track === "undefined")
throw new RangeError("Provided argument must be present.");
return track[TRACK_SYMBOL] === true;
}
/**
* Builds a Track from the raw data from Lavalink and a optional requester.
* @param data
* @param requester
*/
static build(data, requester) {
if (typeof data === "undefined")
throw new RangeError('Argument "data" must be present.');
try {
const track = {
track: data.track,
title: data.info.title,
identifier: data.info.identifier,
author: data.info.author,
duration: data.info.length,
isSeekable: data.info.isSeekable,
isStream: data.info.isStream,
uri: data.info.uri,
thumbnail: data.info.uri.includes("youtube")
? `https://img.youtube.com/vi/${data.info.identifier}/default.jpg`
: null,
displayThumbnail(size = "default") {
const finalSize = SIZES.find((s) => s === size) ?? "default";
return this.uri.includes("youtube")
? `https://img.youtube.com/vi/${data.info.identifier}/${finalSize}.jpg`
: null;
},
requester,
};
track.displayThumbnail = track.displayThumbnail.bind(track);
if (this.trackPartial) {
for (const key of Object.keys(track)) {
if (this.trackPartial.includes(key))
continue;
delete track[key];
}
}
Object.defineProperty(track, TRACK_SYMBOL, {
configurable: true,
value: true
});
return track;
}
catch (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
*/
static buildUnresolved(query, requester) {
if (typeof query === "undefined")
throw new RangeError('Argument "query" must be present.');
let unresolvedTrack = {
requester,
async resolve() {
const resolved = await TrackUtils.getClosestTrack(this);
Object.getOwnPropertyNames(this).forEach(prop => delete this[prop]);
Object.assign(this, resolved);
}
};
if (typeof query === "string")
unresolvedTrack.title = query;
else
unresolvedTrack = { ...unresolvedTrack, ...query };
Object.defineProperty(unresolvedTrack, UNRESOLVED_TRACK_SYMBOL, {
configurable: true,
value: true
});
return unresolvedTrack;
}
static async getClosestTrack(unresolvedTrack) {
if (!TrackUtils.manager)
throw new RangeError("Manager has not been initiated.");
if (!TrackUtils.isUnresolvedTrack(unresolvedTrack))
throw new RangeError("Provided track is not a UnresolvedTrack.");
const query = [unresolvedTrack.author, unresolvedTrack.title].filter(str => !!str).join(" - ");
const res = await TrackUtils.manager.search(query, unresolvedTrack.requester);
if (res.loadType !== "SEARCH_RESULT")
throw res.exception ?? {
message: "No tracks found.",
severity: "COMMON",
};
if (unresolvedTrack.author) {
const channelNames = [unresolvedTrack.author, `${unresolvedTrack.author} - Topic`];
const originalAudio = res.tracks.find(track => {
return (channelNames.some(name => new RegExp(`^${escapeRegExp(name)}$`, "i").test(track.author)) ||
new RegExp(`^${escapeRegExp(unresolvedTrack.title)}$`, "i").test(track.title));
});
if (originalAudio)
return originalAudio;
}
if (unresolvedTrack.duration) {
const sameDuration = res.tracks.find(track => (track.duration >= (unresolvedTrack.duration - 1500)) &&
(track.duration <= (unresolvedTrack.duration + 1500)));
if (sameDuration)
return sameDuration;
}
return res.tracks[0];
}
}
exports.TrackUtils = TrackUtils;
/** Gets or extends structures to extend the built in, or already extended, classes to add more functionality. */
class Structure {
/**
* Extends a class.
* @param name
* @param extender
*/
static extend(name, extender) {
if (!structures[name])
throw new TypeError(`"${name} is not a valid structure`);
const extended = extender(structures[name]);
structures[name] = extended;
return extended;
}
/**
* Get a structure from available structures by name.
* @param name
*/
static get(name) {
const structure = structures[name];
if (!structure)
throw new TypeError('"structure" must be provided.');
return structure;
}
}
exports.Structure = Structure;
class Plugin {
load(manager) { }
unload(manager) { }
}
exports.Plugin = Plugin;
const structures = {
Player: require("./Player").Player,
Queue: require("./Queue").Queue,
Node: require("./Node").Node,
};