UNPKG

larakazagumo

Version:

A shoukaku wrapper with built-in queue support.

249 lines (248 loc) 12 kB
"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;