larakazagumo
Version:
A shoukaku wrapper with built-in queue support.
249 lines (248 loc) • 12 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Kazagumo = void 0;
const events_1 = require("events");
const Interfaces_1 = require("./Modules/Interfaces");
const shoukaku_1 = require("shoukaku");
const { State, VoiceState } = shoukaku_1.Constants;
const KazagumoPlayer_1 = require("./Managers/KazagumoPlayer");
const KazagumoTrack_1 = require("./Managers/Supports/KazagumoTrack");
class Kazagumo extends events_1.EventEmitter {
/**
* Initialize a Kazagumo instance.
* @param KazagumoOptions KazagumoOptions
* @param connector Connector
* @param nodes NodeOption[]
* @param options ShoukakuOptions
*/
constructor(KazagumoOptions, connector, nodes, options = {}) {
super();
this.KazagumoOptions = KazagumoOptions;
/** Kazagumo players */
this.players = new Map();
this.shoukaku = new shoukaku_1.Shoukaku(connector, nodes, options);
if (this.KazagumoOptions.plugins) {
for (const [, plugin] of this.KazagumoOptions.plugins.entries()) {
if (plugin.constructor.name !== 'KazagumoPlugin')
throw new Interfaces_1.KazagumoError(1, 'Plugin must be an instance of KazagumoPlugin');
plugin.load(this);
}
}
this.players = new Map();
}
// Modified version of Shoukaku#joinVoiceChannel
// Credit to @deivu
createVoiceConnection(newPlayerOptions, kazagumoPlayerOptions) {
return __awaiter(this, void 0, void 0, function* () {
var _a;
if (this.shoukaku.connections.has(newPlayerOptions.guildId) && this.shoukaku.players.has(newPlayerOptions.guildId))
return this.shoukaku.players.get(newPlayerOptions.guildId);
if (this.shoukaku.connections.has(newPlayerOptions.guildId) &&
!this.shoukaku.players.has(newPlayerOptions.guildId)) {
this.shoukaku.connections.get(newPlayerOptions.guildId).disconnect();
// tslint:disable-next-line:no-console
console.log('[KazagumoError; l220 Kazagumo.ts] -> Connection exist but player not found. Destroying connection...');
}
const connection = new shoukaku_1.Connection(this.shoukaku, newPlayerOptions);
this.shoukaku.connections.set(connection.guildId, connection);
try {
yield connection.connect();
}
catch (error) {
this.shoukaku.connections.delete(newPlayerOptions.guildId);
throw error;
}
try {
let node;
if (kazagumoPlayerOptions.loadBalancer)
node = yield this.getLeastUsedNode();
else if (kazagumoPlayerOptions.nodeName)
node = (_a = this.shoukaku.nodes.get(kazagumoPlayerOptions.nodeName)) !== null && _a !== void 0 ? _a : (yield this.getLeastUsedNode());
else
node = this.shoukaku.options.nodeResolver(this.shoukaku.nodes);
if (!node)
throw new Interfaces_1.KazagumoError(3, 'No node found');
const player = this.shoukaku.options.structures.player
? new this.shoukaku.options.structures.player(connection.guildId, node)
: new shoukaku_1.Player(connection.guildId, node);
const onUpdate = (state) => {
if (state !== VoiceState.SESSION_READY)
return;
player.sendServerUpdate(connection);
};
yield player.sendServerUpdate(connection);
connection.on('connectionUpdate', onUpdate);
this.shoukaku.players.set(player.guildId, player);
return player;
}
catch (error) {
connection.disconnect();
this.shoukaku.connections.delete(newPlayerOptions.guildId);
throw error;
}
});
}
/**
* Create a player.
* @param options CreatePlayerOptions
* @returns Promise<KazagumoPlayer>
*/
createPlayer(options) {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b, _c;
const exist = this.players.get(options.guildId);
if (exist)
return exist;
let node;
if (options.loadBalancer)
node = this.getLeastUsedNode();
else if (options.nodeName)
node = (_a = this.shoukaku.nodes.get(options.nodeName)) !== null && _a !== void 0 ? _a : this.getLeastUsedNode();
else
node = this.shoukaku.options.nodeResolver(this.shoukaku.nodes);
if (!options.deaf)
options.deaf = false;
if (!options.mute)
options.mute = false;
if (!node)
throw new Interfaces_1.KazagumoError(3, 'No node found');
const shoukakuPlayer = yield this.createVoiceConnection({
guildId: options.guildId,
channelId: options.voiceId,
deaf: options.deaf,
mute: options.mute,
shardId: options.shardId && !isNaN(options.shardId) ? options.shardId : 0,
}, options);
const kazagumoPlayer = new ((_c = (_b = this.KazagumoOptions.extends) === null || _b === void 0 ? void 0 : _b.player) !== null && _c !== void 0 ? _c : KazagumoPlayer_1.KazagumoPlayer)(this, shoukakuPlayer, {
guildId: options.guildId,
voiceId: options.voiceId,
textId: options.textId,
deaf: options.deaf,
volume: isNaN(Number(options.volume)) ? 100 : options.volume,
}, options.data);
this.players.set(options.guildId, kazagumoPlayer);
this.emit(Interfaces_1.Events.PlayerCreate, kazagumoPlayer);
return kazagumoPlayer;
});
}
/**
* Get a player by guildId.
* @param guildId Guild ID
* @returns KazagumoPlayer | undefined
*/
getPlayer(guildId) {
return this.players.get(guildId);
}
/**
* Destroy a player.
* @param guildId Guild ID
* @returns void
*/
destroyPlayer(guildId) {
const player = this.getPlayer(guildId);
if (!player)
return;
player.destroy();
this.players.delete(guildId);
}
/**
* Get the least used node.
* @param group The group where you want to get the least used nodes there. Case-sensitive, catch the error when there is no such group
* @returns Node
*/
getLeastUsedNode(group) {
return __awaiter(this, void 0, void 0, function* () {
const nodes = [...this.shoukaku.nodes.values()];
const onlineNodes = nodes.filter((node) => node.state === State.CONNECTED && (!group || node.group === group));
if (!onlineNodes.length && group && !nodes.find((x) => x.group === group))
throw new Interfaces_1.KazagumoError(2, `There is no such group: ${group}`);
if (!onlineNodes.length)
throw new Interfaces_1.KazagumoError(2, !!group ? `No nodes are online in ${group}` : 'No nodes are online');
const temp = yield Promise.all(onlineNodes.map((node) => __awaiter(this, void 0, void 0, function* () {
return ({
node,
players: (yield node.rest.getPlayers())
.filter((x) => this.players.get(x.guildId))
.map((x) => this.players.get(x.guildId))
.filter((x) => x.shoukaku.node.name === node.name).length,
});
})));
return temp.reduce((a, b) => (a.players < b.players ? a : b)).node;
});
}
/**
* Search a track by query or uri.
* @param query Query
* @param options KazagumoOptions
* @returns Promise<KazagumoSearchResult>
*/
search(query, options) {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b, _c, _d;
const node = (options === null || options === void 0 ? void 0 : options.nodeName)
? ((_a = this.shoukaku.nodes.get(options.nodeName)) !== null && _a !== void 0 ? _a : (yield this.getLeastUsedNode()))
: yield this.getLeastUsedNode();
if (!node)
throw new Interfaces_1.KazagumoError(3, 'No node is available');
const source = Interfaces_1.SourceIDs[((options === null || options === void 0 ? void 0 : options.engine) && ['youtube', 'youtube_music', 'soundcloud'].includes(options.engine)
? options.engine
: null) ||
(!!this.KazagumoOptions.defaultSearchEngine &&
['youtube', 'youtube_music', 'soundcloud'].includes(this.KazagumoOptions.defaultSearchEngine)
? this.KazagumoOptions.defaultSearchEngine
: null) ||
'youtube'];
const isUrl = /^https?:\/\/.*/.test(query);
const customSource = (_c = (_b = options === null || options === void 0 ? void 0 : options.source) !== null && _b !== void 0 ? _b : this.KazagumoOptions.defaultSource) !== null && _c !== void 0 ? _c : `${source}search:`;
const result = yield node.rest.resolve(!isUrl ? `${customSource}${query}` : query).catch((_) => null);
if (!result || result.loadType === shoukaku_1.LoadType.EMPTY)
return this.buildSearch(undefined, [], 'SEARCH');
let loadType;
let normalizedData = { tracks: [] };
switch (result.loadType) {
case shoukaku_1.LoadType.TRACK: {
loadType = 'TRACK';
normalizedData.tracks = [result.data];
break;
}
case shoukaku_1.LoadType.PLAYLIST: {
loadType = 'PLAYLIST';
normalizedData = {
playlistName: result.data.info.name,
tracks: result.data.tracks,
};
break;
}
case shoukaku_1.LoadType.SEARCH: {
loadType = 'SEARCH';
normalizedData.tracks = result.data;
break;
}
default: {
loadType = 'SEARCH';
normalizedData.tracks = [];
break;
}
}
this.emit(Interfaces_1.Events.Debug, `Searched ${query}; Track results: ${normalizedData.tracks.length}`);
return this.buildSearch((_d = normalizedData.playlistName) !== null && _d !== void 0 ? _d : undefined, normalizedData.tracks.map((track) => new KazagumoTrack_1.KazagumoTrack(track, options === null || options === void 0 ? void 0 : options.requester)), loadType);
});
}
buildSearch(playlistName, tracks = [], type) {
return {
playlistName,
tracks,
type: type !== null && type !== void 0 ? type : 'SEARCH',
};
}
}
exports.Kazagumo = Kazagumo;