riffy-extended
Version:
Riffy is a pro lavalink client. It is designed to be simple and easy to use, with a focus on stability and more features.
236 lines (196 loc) • 8.6 kB
JavaScript
const { EventEmitter } = require("events");
const { Node } = require("./Node");
const { Player } = require("./Player");
const { Track } = require("./Track");
const { version: pkgVersion } = require("../../package.json")
const versions = ["v3", "v4"];
class Riffy extends EventEmitter {
constructor(client, nodes, options) {
super();
if (!client) throw new Error("Client is required to initialize Riffy");
if (!nodes || !Array.isArray(nodes)) throw new Error(`Nodes are required & Must Be an Array(Received ${typeof nodes}) for to initialize Riffy`);
if (!options.send || typeof options.send !== "function") throw new Error("Send function is required to initialize Riffy");
this.client = client;
this.nodes = nodes;
this.nodeMap = new Map();
this.players = new Map();
this.options = options;
this.clientId = null;
this.initiated = false;
this.send = options.send || null;
this.defaultSearchPlatform = options.defaultSearchPlatform || "ytmsearch";
this.restVersion = options.restVersion || "v3";
this.tracks = [];
this.loadType = null;
this.playlistInfo = null;
this.pluginInfo = null;
this.plugins = options.plugins;
/**
* @description Package Version Of Riffy
*/
this.version = pkgVersion;
if (this.restVersion && !versions.includes(this.restVersion)) throw new RangeError(`${this.restVersion} is not a valid version`);
}
get leastUsedNodes() {
return [...this.nodeMap.values()]
.filter((node) => node.connected)
.sort((a, b) => a.rest.calls - b.rest.calls);
}
init(clientId) {
if (this.initiated) return this;
this.clientId = clientId;
this.nodes.forEach((node) => this.createNode(node));
this.initiated = true;
if (this.plugins) {
this.plugins.forEach((plugin) => {
plugin.load(this);
});
}
}
createNode(options) {
const node = new Node(this, options, this.options);
this.nodeMap.set(options.name || options.host, node);
node.connect();
this.emit("nodeCreate", node);
return node;
}
destroyNode(identifier) {
const node = this.nodeMap.get(identifier);
if (!node) return;
node.disconnect();
this.nodeMap.delete(identifier);
this.emit("nodeDestroy", node);
}
updateVoiceState(packet) {
if (!["VOICE_STATE_UPDATE", "VOICE_SERVER_UPDATE"].includes(packet.t)) return;
const player = this.players.get(packet.d.guild_id);
if (!player) return;
if (packet.t === "VOICE_SERVER_UPDATE") {
player.connection.setServerUpdate(packet.d);
} else if (packet.t === "VOICE_STATE_UPDATE") {
if (packet.d.user_id !== this.clientId) return;
player.connection.setStateUpdate(packet.d);
}
}
fetchRegion(region) {
const nodesByRegion = [...this.nodeMap.values()]
.filter((node) => node.connected && node.regions?.includes(region?.toLowerCase()))
.sort((a, b) => {
const aLoad = a.stats.cpu
? (a.stats.cpu.systemLoad / a.stats.cpu.cores) * 100
: 0;
const bLoad = b.stats.cpu
? (b.stats.cpu.systemLoad / b.stats.cpu.cores) * 100
: 0;
return aLoad - bLoad;
});
return nodesByRegion;
}
/**
* Creates a connection based on the provided options.
*
* @param {Object} options - The options for creating the connection.
* @param {string} options.guildId - The ID of the guild.
* @param {string} [options.region] - The region for the connection.
* @param {number} [options.defaultVolume] - The default volume of the player. **By-Default**: **100**
* @param {LoopOption} [options.loop] - The loop mode of the player.
* @throws {Error} Throws an error if Riffy is not initialized or no nodes are available.
* @return {Player} The created player.
*/
createConnection(options) {
if (!this.initiated) throw new Error("You have to initialize Riffy in your ready event");
const player = this.players.get(options.guildId);
if (player) return player;
if (this.leastUsedNodes.length === 0) throw new Error("No nodes are available");
let node;
if (options.region) {
const region = this.fetchRegion(options.region)[0];
node = this.nodeMap.get(region.name || this.leastUsedNodes[0].name);
} else {
node = this.nodeMap.get(this.leastUsedNodes[0].name);
}
if (!node) throw new Error("No nodes are available");
return this.createPlayer(node, options);
}
createPlayer(node, options) {
const player = new Player(this, node, options);
this.players.set(options.guildId, player);
player.connect(options);
this.emit("playerCreate", player);
return player;
}
destroyPlayer(guildId) {
const player = this.players.get(guildId);
if (!player) return;
player.destroy();
this.players.delete(guildId);
this.emit("playerDestroy", player);
}
removeConnection(guildId) {
this.players.get(guildId)?.destroy();
this.players.delete(guildId);
}
/**
* @param {object} param0
* @param {string} param0.query used for searching as a search Query
* @param {*} param0.source A source to search the query on example:ytmsearch for youtube music
* @param {*} param0.requester the requester who's requesting
* @param {(string | Node)?} param0.node the node to request the query on either use node identifier/name or the node class itself
* @returns {import("..").nodeResponse} returned properties values are nullable if lavalink doesn't give them
* */
async resolve({ query, source, requester, node }) {
try {
if (!this.initiated) throw new Error("You have to initialize Riffy in your ready event");
if(node && (typeof node !== "string" && !(node instanceof Node))) throw new Error(`'node' property must either be an node identifier/name('string') or an Node/Node Class, But Received: ${typeof node}`)
const sources = source || this.defaultSearchPlatform;
const requestNode = (node && typeof node === 'string' ? this.nodeMap.get(node) : node) || this.leastUsedNodes[0];
if (!requestNode) throw new Error("No nodes are available.");
const regex = /^https?:\/\//;
const identifier = regex.test(query) ? query : `${sources}:${query}`;
let response = await requestNode.rest.makeRequest(`GET`, `/${requestNode.rest.version}/loadtracks?identifier=${encodeURIComponent(identifier)}`);
// for resolving identifiers - Only works in Spotify and Youtube
if (response.loadType === "empty" || response.loadType === "NO_MATCHES") {
response = await requestNode.rest.makeRequest(`GET`, `/${requestNode.rest.version}/loadtracks?identifier=https://open.spotify.com/track/${query}`);
if (response.loadType === "empty" || response.loadType === "NO_MATCHES") {
response = await requestNode.rest.makeRequest(`GET`, `/${requestNode.rest.version}/loadtracks?identifier=https://www.youtube.com/watch?v=${query}`);
}
}
if (requestNode.rest.version === "v4") {
if (response.loadType === "track") {
this.tracks = response.data ? [new Track(response.data, requester, requestNode)] : [];
} else if (response.loadType === "playlist") {
this.tracks = response.data?.tracks ? response.data.tracks.map((track) => new Track(track, requester, requestNode)) : [];
} else {
this.tracks = response.loadType === "search" && response.data ? response.data.map((track) => new Track(track, requester, requestNode)) : [];
}
} else {
this.tracks = response?.tracks ? response.tracks.map((track) => new Track(track, requester, requestNode)) : [];
}
if (
requestNode.rest.version === "v4" &&
response.loadType === "playlist"
) {
this.playlistInfo = response.data?.info ?? null;
} else {
this.playlistInfo = response.playlistInfo ?? null;
}
this.loadType = response.loadType ?? null
this.pluginInfo = response.pluginInfo ?? {};
return {
loadType: this.loadType,
exception: this.loadType == "error" ? response.data : this.loadType == "LOAD_FAILED" ? response.exception : null,
playlistInfo: this.playlistInfo,
pluginInfo: this.pluginInfo,
tracks: this.tracks,
};
} catch (error) {
throw new Error(error);
}
}
get(guildId) {
const player = this.players.get(guildId);
if (!player) throw new Error(`Player not found for ${guildId} guildId`);
return player;
}
}
module.exports = { Riffy };