UNPKG

kosa-discord-player

Version:

edit discord-player by Androz2091 package using discord.js v12

1,059 lines 62.7 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()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Player = void 0; const events_1 = require("events"); const discord_js_1 = require("discord.js"); const Util_1 = __importDefault(require("./utils/Util")); const AudioFilters_1 = __importDefault(require("./utils/AudioFilters")); const Queue_1 = require("./Structures/Queue"); const Track_1 = require("./Structures/Track"); const Constants_1 = require("./utils/Constants"); const PlayerError_1 = __importDefault(require("./utils/PlayerError")); const discord_ytdl_core_1 = __importDefault(require("discord-ytdl-core")); const ExtractorModel_1 = require("./Structures/ExtractorModel"); const os_1 = __importDefault(require("os")); // @ts-ignore const spotify_url_info_1 = __importDefault(require("spotify-url-info")); // @ts-ignore const soundcloud_scraper_1 = require("soundcloud-scraper"); const youtube_sr_1 = __importDefault(require("youtube-sr")); const SoundCloud = new soundcloud_scraper_1.Client(); /** * The Player class * @extends {EventEmitter} */ class Player extends events_1.EventEmitter { /** * Creates new Player instance * @param {DiscordClient} client The discord.js client * @param {PlayerOptions} options Player options */ constructor(client, options) { super(); /** * The collection of queues in this player * @type {DiscordCollection<Queue>} */ this.queues = new discord_js_1.Collection(); /** * Collection of results collectors * @type {DiscordCollection<DiscordCollector<DiscordSnowflake, DiscordMessage>>} * @private */ this._resultsCollectors = new discord_js_1.Collection(); /** * Collection of cooldowns timeout * @type {DiscordCollection<Timeout>} * @private */ this._cooldownsTimeout = new discord_js_1.Collection(); /** * The extractor model collection * @type {DiscordCollection<ExtractorModel>} */ this.Extractors = new discord_js_1.Collection(); /** * The discord client that instantiated this player * @name Player#client * @type {DiscordClient} * @readonly */ Object.defineProperty(this, 'client', { value: client, enumerable: false }); /** * The player options * @type {PlayerOptions} */ this.options = Object.assign({}, Constants_1.PlayerOptions, options !== null && options !== void 0 ? options : {}); // check FFmpeg void Util_1.default.alertFFmpeg(); /** * Audio filters * @type {Object} */ this.filters = AudioFilters_1.default; this.client.on('voiceStateUpdate', this._handleVoiceStateUpdate.bind(this)); // auto detect @discord-player/extractor if (!this.options.disableAutoRegister) { let nv; // tslint:disable:no-conditional-assignment if ((nv = Util_1.default.require('@discord-player/extractor'))) { ['Attachment', 'Facebook', 'Reverbnation', 'Vimeo'].forEach((ext) => void this.use(ext, nv[ext])); } } } static get AudioFilters() { return AudioFilters_1.default; } /** * Define custom extractor in this player * @param {String} extractorName The extractor name * @param {any} extractor The extractor itself * @returns {Player} */ use(extractorName, extractor) { if (!extractorName) throw new PlayerError_1.default('Missing extractor name!', 'PlayerExtractorError'); const methods = ['validate', 'getInfo']; for (const method of methods) { if (typeof extractor[method] !== 'function') throw new PlayerError_1.default('Invalid extractor supplied!', 'PlayerExtractorError'); } this.Extractors.set(extractorName, new ExtractorModel_1.ExtractorModel(extractorName, extractor)); return this; } /** * Remove existing extractor from this player * @param {String} extractorName The extractor name * @returns {Boolean} */ unuse(extractorName) { if (!extractorName) throw new PlayerError_1.default('Missing extractor name!', 'PlayerExtractorError'); return this.Extractors.delete(extractorName); } /** * Internal method to search tracks * @param {DiscordMessage} message The message * @param {string} query The query * @param {boolean} [firstResult=false] If it should return the first result * @returns {Promise<Track>} * @private */ _searchTracks(message, query, firstResult) { return new Promise((resolve) => __awaiter(this, void 0, void 0, function* () { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k; let tracks = []; const queryType = Util_1.default.getQueryType(query); switch (queryType) { case 'soundcloud_track': { const data = yield SoundCloud.getSongInfo(query).catch(() => { }); if (data) { const track = new Track_1.Track(this, { title: data.title, url: data.url, duration: Util_1.default.buildTimeCode(Util_1.default.parseMS(data.duration)), description: data.description, thumbnail: data.thumbnail, views: data.playCount, author: data.author.name, requestedBy: message.author, fromPlaylist: false, source: 'soundcloud', engine: data }); tracks.push(track); } } break; case 'spotify_song': { const matchSpotifyURL = query.match(/https?:\/\/(?:embed\.|open\.)(?:spotify\.com\/)(?:track\/|\?uri=spotify:track:)((\w|-){22})/); if (matchSpotifyURL) { const spotifyData = yield spotify_url_info_1.default.getPreview(query).catch(() => { }); if (spotifyData) { const searchString = this.options.disableArtistSearch ? spotifyData.title : `${spotifyData.artist} - ${spotifyData.title}`; tracks = yield Util_1.default.ytSearch(searchString, { user: message.author, player: this, limit: 1 }); } } } break; case 'spotify_album': case 'spotify_playlist': { this.emit(Constants_1.PlayerEvents.PLAYLIST_PARSE_START, null, message); const playlist = yield spotify_url_info_1.default.getData(query); if (!playlist) return void this.emit(Constants_1.PlayerEvents.NO_RESULTS, message, query); // tslint:disable-next-line:no-shadowed-variable let tracks = yield Promise.all(playlist.tracks.items.map((item) => __awaiter(this, void 0, void 0, function* () { var _l, _m; const sq = queryType === 'spotify_album' ? `${this.options.disableArtistSearch ? item.artists[0].name : `${item.artists[0].name} - `}${(_l = item.name) !== null && _l !== void 0 ? _l : item.track.name}` : `${this.options.disableArtistSearch ? item.track.artists[0].name : `${item.track.artists[0].name} - `}${(_m = item.name) !== null && _m !== void 0 ? _m : item.track.name}`; const data = yield Util_1.default.ytSearch(sq, { limit: 1, player: this, user: message.author, pl: true }); if (data.length) return data[0]; }))); tracks = tracks.filter((f) => !!f); if (!tracks.length) return void this.emit(Constants_1.PlayerEvents.NO_RESULTS, message, query); const pl = Object.assign(Object.assign({}, playlist), { tracks, duration: (_a = tracks === null || tracks === void 0 ? void 0 : tracks.reduce((a, c) => { var _a; return a + ((_a = c === null || c === void 0 ? void 0 : c.durationMS) !== null && _a !== void 0 ? _a : 0); }, 0)) !== null && _a !== void 0 ? _a : 0, thumbnail: (_c = (_b = playlist.images[0]) === null || _b === void 0 ? void 0 : _b.url) !== null && _c !== void 0 ? _c : tracks[0].thumbnail, title: (_e = (_d = playlist.title) !== null && _d !== void 0 ? _d : playlist.name) !== null && _e !== void 0 ? _e : '' }); this.emit(Constants_1.PlayerEvents.PLAYLIST_PARSE_END, pl, message); if (this.isPlaying(message)) { const queue = this._addTracksToQueue(message, tracks); this.emit(Constants_1.PlayerEvents.PLAYLIST_ADD, message, queue, pl); } else { const track = tracks.shift(); const queue = (yield this._createQueue(message, track).catch((e) => void this.emit(Constants_1.PlayerEvents.ERROR, e, message))); this.emit(Constants_1.PlayerEvents.PLAYLIST_ADD, message, queue, pl); this.emit(Constants_1.PlayerEvents.TRACK_START, message, queue.tracks[0], queue); this._addTracksToQueue(message, tracks); } return; } case 'youtube_playlist': { this.emit(Constants_1.PlayerEvents.PLAYLIST_PARSE_START, null, message); const playlist = yield youtube_sr_1.default.getPlaylist(query); if (!playlist) return void this.emit(Constants_1.PlayerEvents.NO_RESULTS, message, query); // @ts-ignore playlist.videos = playlist.videos.map((data) => { var _a; return new Track_1.Track(this, { title: data.title, url: data.url, duration: Util_1.default.buildTimeCode(Util_1.default.parseMS(data.duration)), description: data.description, thumbnail: (_a = data.thumbnail) === null || _a === void 0 ? void 0 : _a.displayThumbnailURL(), views: data.views, author: data.channel.name, requestedBy: message.author, fromPlaylist: true, source: 'youtube' }); }); // @ts-ignore playlist.duration = playlist.videos.reduce((a, c) => a + c.durationMS, 0); // @ts-ignore playlist.thumbnail = (_g = (_f = playlist.thumbnail) === null || _f === void 0 ? void 0 : _f.url) !== null && _g !== void 0 ? _g : playlist.videos[0].thumbnail; // @ts-ignore playlist.requestedBy = message.author; Object.defineProperty(playlist, 'tracks', { get: () => { var _a; return (_a = playlist.videos) !== null && _a !== void 0 ? _a : []; } }); this.emit(Constants_1.PlayerEvents.PLAYLIST_PARSE_END, playlist, message); // @ts-ignore // tslint:disable-next-line:no-shadowed-variable const tracks = playlist.videos; if (this.isPlaying(message)) { const queue = this._addTracksToQueue(message, tracks); this.emit(Constants_1.PlayerEvents.PLAYLIST_ADD, message, queue, playlist); } else { const track = tracks.shift(); const queue = (yield this._createQueue(message, track).catch((e) => void this.emit(Constants_1.PlayerEvents.ERROR, e, message))); this.emit(Constants_1.PlayerEvents.PLAYLIST_ADD, message, queue, playlist); this.emit(Constants_1.PlayerEvents.TRACK_START, message, queue.tracks[0], queue); this._addTracksToQueue(message, tracks); } return; } case 'soundcloud_playlist': { this.emit(Constants_1.PlayerEvents.PLAYLIST_PARSE_START, null, message); const data = yield SoundCloud.getPlaylist(query).catch(() => { }); if (!data) return void this.emit(Constants_1.PlayerEvents.NO_RESULTS, message, query); const res = { id: data.id, title: data.title, tracks: [], author: data.author, duration: 0, thumbnail: data.thumbnail, requestedBy: message.author }; for (const song of data.tracks) { const r = new Track_1.Track(this, { title: song.title, url: song.url, duration: Util_1.default.buildTimeCode(Util_1.default.parseMS(song.duration)), description: song.description, thumbnail: (_h = song.thumbnail) !== null && _h !== void 0 ? _h : 'https://soundcloud.com/pwa-icon-192.png', views: (_j = song.playCount) !== null && _j !== void 0 ? _j : 0, author: (_k = song.author) !== null && _k !== void 0 ? _k : data.author, requestedBy: message.author, fromPlaylist: true, source: 'soundcloud', engine: song }); res.tracks.push(r); } if (!res.tracks.length) return this.emit(Constants_1.PlayerEvents.ERROR, Constants_1.PlayerErrorEventCodes.PARSE_ERROR, message); res.duration = res.tracks.reduce((a, c) => a + c.durationMS, 0); this.emit(Constants_1.PlayerEvents.PLAYLIST_PARSE_END, res, message); if (this.isPlaying(message)) { const queue = this._addTracksToQueue(message, res.tracks); this.emit(Constants_1.PlayerEvents.PLAYLIST_ADD, message, queue, res); } else { const track = res.tracks.shift(); const queue = (yield this._createQueue(message, track).catch((e) => void this.emit(Constants_1.PlayerEvents.ERROR, e, message))); this.emit(Constants_1.PlayerEvents.PLAYLIST_ADD, message, queue, res); this.emit(Constants_1.PlayerEvents.TRACK_START, message, queue.tracks[0], queue); this._addTracksToQueue(message, res.tracks); } return; } default: tracks = yield Util_1.default.ytSearch(query, { user: message.author, player: this }); } if (tracks.length < 1) return void this.emit(Constants_1.PlayerEvents.NO_RESULTS, message, query); if (firstResult || tracks.length === 1) return resolve(tracks[0]); const collectorString = `${message.author.id}-${message.channel.id}`; const currentCollector = this._resultsCollectors.get(collectorString); if (currentCollector) currentCollector.stop(); const collector = message.channel.createMessageCollector((m) => m.author.id === message.author.id, { time: 60000 }); this._resultsCollectors.set(collectorString, collector); this.emit(Constants_1.PlayerEvents.SEARCH_RESULTS, message, query, tracks, collector); collector.on('collect', ({ content }) => { if (content === 'cancel') { collector.stop(); return this.emit(Constants_1.PlayerEvents.SEARCH_CANCEL, message, query, tracks); } if (!isNaN(content) && parseInt(content) >= 1 && parseInt(content) <= tracks.length) { const index = parseInt(content, 10); const track = tracks[index - 1]; collector.stop(); resolve(track); } else { this.emit(Constants_1.PlayerEvents.SEARCH_INVALID_RESPONSE, message, query, tracks, content, collector); } }); collector.on('end', (_, reason) => { if (reason === 'time') { this.emit(Constants_1.PlayerEvents.SEARCH_CANCEL, message, query, tracks); } }); })); } /** * Play a song * @param {DiscordMessage} message The discord.js message object * @param {string|Track} query Search query, can be `Player.Track` instance * @param {Boolean} [firstResult=false] If it should play the first result * @example await player.play(message, "never gonna give you up", true) * @returns {Promise<void>} */ play(message, query, firstResult) { return __awaiter(this, void 0, void 0, function* () { if (!message) throw new PlayerError_1.default('Play function needs message'); if (!query) throw new PlayerError_1.default('Play function needs search query as a string or Player.Track object'); if (this._cooldownsTimeout.has(`end_${message.guild.id}`)) { clearTimeout(this._cooldownsTimeout.get(`end_${message.guild.id}`)); this._cooldownsTimeout.delete(`end_${message.guild.id}`); } if (typeof query === 'string') query = query.replace(/<(.+)>/g, '$1'); let track; if (query instanceof Track_1.Track) track = query; else { if (discord_ytdl_core_1.default.validateURL(query)) { const info = yield discord_ytdl_core_1.default.getBasicInfo(query).catch(() => { }); if (!info) return void this.emit(Constants_1.PlayerEvents.NO_RESULTS, message, query); if (info.videoDetails.isLiveContent && !this.options.enableLive) return void this.emit(Constants_1.PlayerEvents.ERROR, Constants_1.PlayerErrorEventCodes.LIVE_VIDEO, message, new PlayerError_1.default('Live video is not enabled!')); const lastThumbnail = info.videoDetails.thumbnails[info.videoDetails.thumbnails.length - 1]; track = new Track_1.Track(this, { title: info.videoDetails.title, description: info.videoDetails.description, author: info.videoDetails.author.name, url: info.videoDetails.video_url, thumbnail: lastThumbnail.url, duration: Util_1.default.buildTimeCode(Util_1.default.parseMS(parseInt(info.videoDetails.lengthSeconds) * 1000)), views: parseInt(info.videoDetails.viewCount), requestedBy: message.author, fromPlaylist: false, source: 'youtube', live: Boolean(info.videoDetails.isLiveContent) }); } else { for (const [_, extractor] of this.Extractors) { if (extractor.validate(query)) { const data = yield extractor.handle(query); if (data) { track = new Track_1.Track(this, { title: data.title, description: data.description, duration: Util_1.default.buildTimeCode(Util_1.default.parseMS(data.duration)), thumbnail: data.thumbnail, author: data.author, views: data.views, engine: data.engine, source: 'arbitrary', fromPlaylist: false, requestedBy: message.author, url: data.url }); if (extractor.important) break; } } } if (!track) track = yield this._searchTracks(message, query, firstResult); } } if (track) { if (this.isPlaying(message)) { const queue = this._addTrackToQueue(message, track); this.emit(Constants_1.PlayerEvents.TRACK_ADD, message, queue, queue.tracks[queue.tracks.length - 1]); } else { const queue = yield this._createQueue(message, track); if (queue) this.emit(Constants_1.PlayerEvents.TRACK_START, message, queue.tracks[0], queue); } } }); } /** * Checks if this player is playing in a server * @param {DiscordMessage} message The message object * @returns {Boolean} */ isPlaying(message) { return this.queues.some((g) => g.guildID === message.guild.id); } /** * Returns guild queue object * @param {DiscordMessage} message The message object * @returns {Queue} */ getQueue(message) { return this.queues.find((g) => g.guildID === message.guild.id); } /** * Sets audio filters in this player * @param {DiscordMessage} message The message object * @param {QueueFilters} newFilters Audio filters object * @returns {Promise<void>} */ setFilters(message, newFilters) { return new Promise((resolve) => { const queue = this.queues.find((g) => g.guildID === message.guild.id); if (!queue) this.emit(Constants_1.PlayerEvents.ERROR, Constants_1.PlayerErrorEventCodes.NOT_PLAYING, message, new PlayerError_1.default('Not playing')); if (queue.playing.raw.live) return void this.emit(Constants_1.PlayerEvents.ERROR, Constants_1.PlayerErrorEventCodes.LIVE_VIDEO, message, new PlayerError_1.default('Cannot use setFilters on livestream')); Object.keys(newFilters).forEach((filterName) => { // @ts-ignore queue.filters[filterName] = newFilters[filterName]; }); this._playStream(queue, true).then(() => { resolve(); }); }); } /** * Sets track position * @param {DiscordMessage} message The message object * @param {Number} time Time in ms to set * @returns {Promise<void>} */ setPosition(message, time) { return new Promise((resolve) => { const queue = this.queues.find((g) => g.guildID === message.guild.id); if (!queue) return this.emit(Constants_1.PlayerEvents.ERROR, Constants_1.PlayerErrorEventCodes.NOT_PLAYING, message); if (typeof time !== 'number' && !isNaN(time)) time = parseInt(time); if (queue.playing.durationMS <= time) return this.skip(message); if (queue.voiceConnection.dispatcher.streamTime === time || queue.voiceConnection.dispatcher.streamTime + queue.additionalStreamTime === time) return resolve(); if (time < 0) this._playStream(queue, false).then(() => resolve()); this._playStream(queue, false, time).then(() => resolve()); }); } /** * Sets track position * @param {DiscordMessage} message The message object * @param {Number} time Time in ms to set * @returns {Promise<void>} */ seek(message, time) { return this.setPosition(message, time); } /** * Skips current track * @param {DiscordMessage} message The message object * @returns {Boolean} */ skip(message) { const queue = this.getQueue(message); if (!queue) { this.emit(Constants_1.PlayerEvents.ERROR, Constants_1.PlayerErrorEventCodes.NOT_PLAYING, message); return false; } if (!queue.voiceConnection || !queue.voiceConnection.dispatcher) { this.emit(Constants_1.PlayerEvents.ERROR, Constants_1.PlayerErrorEventCodes.MUSIC_STARTING, message); return false; } queue.voiceConnection.dispatcher.end(); queue.lastSkipped = true; return true; } /** * Moves to a new voice channel * @param {DiscordMessage} message The message object * @param {DiscordVoiceChannel} channel New voice channel to move to * @returns {Boolean} */ moveTo(message, channel) { if (!channel || channel.type !== 'voice') return; const queue = this.queues.find((g) => g.guildID === message.guild.id); if (!queue) { this.emit(Constants_1.PlayerEvents.ERROR, Constants_1.PlayerErrorEventCodes.NOT_PLAYING, message); return false; } if (!queue.voiceConnection || !queue.voiceConnection.dispatcher) { this.emit(Constants_1.PlayerEvents.ERROR, Constants_1.PlayerErrorEventCodes.MUSIC_STARTING, message); return false; } if (queue.voiceConnection.channel.id === channel.id) return; queue.voiceConnection.dispatcher.pause(); channel .join() .then(() => queue.voiceConnection.dispatcher.resume()) .catch(() => this.emit(Constants_1.PlayerEvents.ERROR, Constants_1.PlayerErrorEventCodes.UNABLE_TO_JOIN, message)); return true; } /** * Pause the playback * @param {DiscordMessage} message The message object * @returns {Boolean} */ pause(message) { const queue = this.getQueue(message); if (!queue) { this.emit(Constants_1.PlayerEvents.ERROR, Constants_1.PlayerErrorEventCodes.NOT_PLAYING, message); return false; } if (!queue.voiceConnection || !queue.voiceConnection.dispatcher) { this.emit(Constants_1.PlayerEvents.ERROR, Constants_1.PlayerErrorEventCodes.MUSIC_STARTING, message); return false; } queue.voiceConnection.dispatcher.pause(); queue.paused = true; return true; } /** * Resume the playback * @param {DiscordMessage} message The message object * @returns {Boolean} */ resume(message) { const queue = this.getQueue(message); if (!queue) { this.emit(Constants_1.PlayerEvents.ERROR, Constants_1.PlayerErrorEventCodes.NOT_PLAYING, message); return false; } if (!queue.voiceConnection || !queue.voiceConnection.dispatcher) { this.emit(Constants_1.PlayerEvents.ERROR, Constants_1.PlayerErrorEventCodes.MUSIC_STARTING, message); return false; } queue.voiceConnection.dispatcher.resume(); queue.paused = false; return true; } /** * Stops the player * @param {DiscordMessage} message The message object * @returns {Boolean} */ stop(message) { const queue = this.getQueue(message); if (!queue) { this.emit(Constants_1.PlayerEvents.ERROR, Constants_1.PlayerErrorEventCodes.NOT_PLAYING, message); return false; } if (!queue.voiceConnection || !queue.voiceConnection.dispatcher) { this.emit(Constants_1.PlayerEvents.ERROR, Constants_1.PlayerErrorEventCodes.MUSIC_STARTING, message); return false; } queue.stopped = true; queue.tracks = []; if (queue.stream) queue.stream.destroy(); queue.voiceConnection.dispatcher.end(); if (this.options.leaveOnStop) queue.voiceConnection.channel.leave(); this.queues.delete(message.guild.id); return true; } /** * Sets music volume * @param {DiscordMessage} message The message object * @param {Number} percent The volume percentage/amount to set * @returns {Boolean} */ setVolume(message, percent) { const queue = this.getQueue(message); if (!queue) { this.emit(Constants_1.PlayerEvents.ERROR, Constants_1.PlayerErrorEventCodes.NOT_PLAYING, message); return false; } if (!queue.voiceConnection || !queue.voiceConnection.dispatcher) { this.emit(Constants_1.PlayerEvents.ERROR, Constants_1.PlayerErrorEventCodes.MUSIC_STARTING, message); return false; } queue.volume = percent; queue.voiceConnection.dispatcher.setVolumeLogarithmic(queue.calculatedVolume / 200); return true; } /** * Clears the queue * @param {DiscordMessage} message The message object * @returns {Boolean} */ clearQueue(message) { const queue = this.getQueue(message); if (!queue) { this.emit(Constants_1.PlayerEvents.ERROR, Constants_1.PlayerErrorEventCodes.NOT_PLAYING, message); return false; } queue.tracks = queue.playing ? [queue.playing] : []; return true; } /** * Plays previous track * @param {DiscordMessage} message The message object * @returns {Boolean} */ back(message) { const queue = this.getQueue(message); if (!queue) { this.emit(Constants_1.PlayerEvents.ERROR, Constants_1.PlayerErrorEventCodes.NOT_PLAYING, message); return false; } if (!queue.voiceConnection || !queue.voiceConnection.dispatcher) { this.emit(Constants_1.PlayerEvents.ERROR, Constants_1.PlayerErrorEventCodes.MUSIC_STARTING, message); return false; } queue.tracks.splice(1, 0, queue.previousTracks.shift()); queue.voiceConnection.dispatcher.end(); queue.lastSkipped = true; return true; } /** * Sets repeat mode * @param {DiscordMessage} message The message object * @param {Boolean} enabled If it should enable the repeat mode */ setRepeatMode(message, enabled) { const queue = this.getQueue(message); if (!queue) { this.emit(Constants_1.PlayerEvents.ERROR, Constants_1.PlayerErrorEventCodes.NOT_PLAYING, message); return; } queue.repeatMode = Boolean(enabled); return queue.repeatMode; } /** * Sets loop mode * @param {DiscordMessage} message The message object * @param {Boolean} enabled If it should enable the loop mode * @returns {Boolean} */ setLoopMode(message, enabled) { const queue = this.getQueue(message); if (!queue) { this.emit(Constants_1.PlayerEvents.ERROR, Constants_1.PlayerErrorEventCodes.NOT_PLAYING, message); return; } queue.loopMode = Boolean(enabled); return queue.loopMode; } /** * Returns currently playing track * @param {DiscordMessage} message The message object * @returns {Track} */ nowPlaying(message) { const queue = this.getQueue(message); if (!queue) { this.emit(Constants_1.PlayerEvents.ERROR, Constants_1.PlayerErrorEventCodes.NOT_PLAYING, message); return; } return queue.tracks[0]; } /** * Shuffles the queue * @param {DiscordMessage} message The message object * @returns {Queue} */ shuffle(message) { const queue = this.getQueue(message); if (!queue) { this.emit(Constants_1.PlayerEvents.ERROR, Constants_1.PlayerErrorEventCodes.NOT_PLAYING, message); return; } const currentTrack = queue.tracks.shift(); for (let i = queue.tracks.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [queue.tracks[i], queue.tracks[j]] = [queue.tracks[j], queue.tracks[i]]; } queue.tracks.unshift(currentTrack); return queue; } /** * Removes specified track * @param {DiscordMessage} message The message object * @param {Track|number} track The track object/id to remove * @returns {Track} */ remove(message, track) { const queue = this.getQueue(message); if (!queue) { this.emit(Constants_1.PlayerEvents.ERROR, Constants_1.PlayerErrorEventCodes.NOT_PLAYING, message); return; } let trackFound = null; if (typeof track === 'number') { trackFound = queue.tracks[track]; if (trackFound) { queue.tracks = queue.tracks.filter((t) => t !== trackFound); } } else { trackFound = queue.tracks.find((s) => s.url === track.url); if (trackFound) { queue.tracks = queue.tracks.filter((s) => s.url !== trackFound.url); } } return trackFound; } /** * Returns time code of currently playing song * @param {DiscordMessage} message The message object * @param {Boolean} [queueTime] If it should make the time code of the whole queue * @returns {Object} */ getTimeCode(message, queueTime) { const queue = this.getQueue(message); if (!queue) return; const previousTracksTime = queue.previousTracks.length > 0 ? queue.previousTracks.reduce((p, c) => p + c.durationMS, 0) : 0; const currentStreamTime = queueTime ? previousTracksTime + queue.currentStreamTime : queue.currentStreamTime; const totalTracksTime = queue.totalTime; const totalTime = queueTime ? previousTracksTime + totalTracksTime : queue.playing.durationMS; const currentTimecode = Util_1.default.buildTimeCode(Util_1.default.parseMS(currentStreamTime)); const endTimecode = Util_1.default.buildTimeCode(Util_1.default.parseMS(totalTime)); return { current: currentTimecode, end: endTimecode }; } /** * Creates progressbar * @param {DiscordMessage} message The message object * @param {PlayerProgressbarOptions} [options] Progressbar options * @returns {String} */ createProgressBar(message, options) { const queue = this.getQueue(message); if (!queue) return; const previousTracksTime = queue.previousTracks.length > 0 ? queue.previousTracks.reduce((p, c) => p + c.durationMS, 0) : 0; const currentStreamTime = (options === null || options === void 0 ? void 0 : options.queue) ? previousTracksTime + queue.currentStreamTime : queue.currentStreamTime; const totalTracksTime = queue.totalTime; const totalTime = (options === null || options === void 0 ? void 0 : options.queue) ? previousTracksTime + totalTracksTime : queue.playing.durationMS; const length = typeof (options === null || options === void 0 ? void 0 : options.length) === 'number' ? (options === null || options === void 0 ? void 0 : options.length) <= 0 || (options === null || options === void 0 ? void 0 : options.length) === Infinity ? 15 : options === null || options === void 0 ? void 0 : options.length : 15; const index = Math.round((currentStreamTime / totalTime) * length); const indicator = typeof (options === null || options === void 0 ? void 0 : options.indicator) === 'string' && (options === null || options === void 0 ? void 0 : options.indicator.length) > 0 ? options === null || options === void 0 ? void 0 : options.indicator : '🔘'; const line = typeof (options === null || options === void 0 ? void 0 : options.line) === 'string' && (options === null || options === void 0 ? void 0 : options.line.length) > 0 ? options === null || options === void 0 ? void 0 : options.line : '▬'; if (index >= 1 && index <= length) { const bar = line.repeat(length - 1).split(''); bar.splice(index, 0, indicator); if (Boolean(options === null || options === void 0 ? void 0 : options.timecodes)) { const currentTimecode = Util_1.default.buildTimeCode(Util_1.default.parseMS(currentStreamTime)); const endTimecode = Util_1.default.buildTimeCode(Util_1.default.parseMS(totalTime)); return `${currentTimecode} ┃ ${bar.join('')} ┃ ${endTimecode}`; } else { return `${bar.join('')}`; } } else { if (Boolean(options === null || options === void 0 ? void 0 : options.timecodes)) { const currentTimecode = Util_1.default.buildTimeCode(Util_1.default.parseMS(currentStreamTime)); const endTimecode = Util_1.default.buildTimeCode(Util_1.default.parseMS(totalTime)); return `${currentTimecode} ┃ ${indicator}${line.repeat(length - 1)} ┃ ${endTimecode}`; } else { return `${indicator}${line.repeat(length - 1)}`; } } } /** * Gets lyrics of a song * <warn>You need to have `@discord-player/extractor` installed in order to use this method!</warn> * @param {String} query Search query * @example const lyrics = await player.lyrics("alan walker faded") * message.channel.send(lyrics.lyrics); * @returns {Promise<LyricsData>} */ lyrics(query) { return __awaiter(this, void 0, void 0, function* () { const extractor = Util_1.default.require('@discord-player/extractor'); if (!extractor) throw new PlayerError_1.default("Cannot call 'Player.lyrics()' without '@discord-player/extractor'"); const data = yield extractor.Lyrics(query); if (Array.isArray(data)) return null; return data; }); } /** * Toggle autoplay for youtube streams * @param {DiscordMessage} message The message object * @param {Boolean} enable Enable/Disable autoplay * @returns {boolean} */ setAutoPlay(message, enable) { const queue = this.getQueue(message); if (!queue) return void this.emit(Constants_1.PlayerEvents.ERROR, Constants_1.PlayerErrorEventCodes.NOT_PLAYING, message); queue.autoPlay = Boolean(enable); return queue.autoPlay; } /** * Player stats * @returns {PlayerStats} */ getStats() { return { uptime: this.client.uptime, connections: this.client.voice.connections.size, // tslint:disable:no-shadowed-variable users: this.client.voice.connections.reduce((a, c) => a + c.channel.members.filter((a) => a.user.id !== this.client.user.id).size, 0), queues: this.queues.size, extractors: this.Extractors.size, versions: { ffmpeg: Util_1.default.getFFmpegVersion(), node: process.version, v8: process.versions.v8 }, system: { arch: process.arch, platform: process.platform, cpu: os_1.default.cpus().length, memory: { total: (process.memoryUsage().heapTotal / 1024 / 1024).toFixed(2), usage: (process.memoryUsage().heapUsed / 1024 / 1024).toFixed(2), rss: (process.memoryUsage().rss / 1024 / 1024).toFixed(2), arrayBuffers: (process.memoryUsage().arrayBuffers / 1024 / 1024).toFixed(2) }, uptime: process.uptime() } }; } /** * Jumps to particular track * @param {DiscordMessage} message The message * @param {Track|number} track The track to jump to * @returns {boolean} */ jump(message, track) { const toJUMP = this.remove(message, track); const queue = this.getQueue(message); if (!toJUMP || !queue) throw new PlayerError_1.default('Specified Track not found'); queue.tracks.splice(1, 0, toJUMP); return this.skip(message); } /** * Internal method to handle VoiceStateUpdate events * @param {DiscordVoiceState} oldState The old voice state * @param {DiscordVoiceState} newState The new voice state * @returns {void} * @private */ _handleVoiceStateUpdate(oldState, newState) { const queue = this.queues.find((g) => g.guildID === oldState.guild.id); if (!queue) return; if (newState.member.id === this.client.user.id && !newState.channelID) { queue.stream.destroy(); this.queues.delete(newState.guild.id); this.emit(Constants_1.PlayerEvents.BOT_DISCONNECT, queue.firstMessage); } if (!queue.voiceConnection || !queue.voiceConnection.channel) return; if (!this.options.leaveOnEmpty) return; if (!oldState.channelID || newState.channelID) { const emptyTimeout = this._cooldownsTimeout.get(`empty_${oldState.guild.id}`); // @todo: stage channels const channelEmpty = Util_1.default.isVoiceEmpty(queue.voiceConnection.channel); if (!channelEmpty && emptyTimeout) { clearTimeout(emptyTimeout); this._cooldownsTimeout.delete(`empty_${oldState.guild.id}`); } } else { if (!Util_1.default.isVoiceEmpty(queue.voiceConnection.channel)) return; const timeout = setTimeout(() => { if (!Util_1.default.isVoiceEmpty(queue.voiceConnection.channel)) return; if (!this.queues.has(queue.guildID)) return; queue.voiceConnection.channel.leave(); this.queues.delete(queue.guildID); this.emit(Constants_1.PlayerEvents.CHANNEL_EMPTY, queue.firstMessage, queue); }, this.options.leaveOnEmptyCooldown || 0); this._cooldownsTimeout.set(`empty_${oldState.guild.id}`, timeout); } } /** * Internal method used to add tracks to the queue * @param {DiscordMessage} message The discord message * @param {Track} track The track * @returns {Queue} * @private */ _addTrackToQueue(message, track) { const queue = this.getQueue(message); if (!queue) this.emit(Constants_1.PlayerEvents.ERROR, Constants_1.PlayerErrorEventCodes.NOT_PLAYING, message, new PlayerError_1.default('Player is not available in this server')); if (!track || !(track instanceof Track_1.Track)) throw new PlayerError_1.default('No track specified to add to the queue'); queue.tracks.push(track); return queue; } /** * Same as `_addTrackToQueue` but used for multiple tracks * @param {DiscordMessage} message Discord message * @param {Track[]} tracks The tracks * @returns {Queue} * @private */ _addTracksToQueue(message, tracks) { const queue = this.getQueue(message); if (!queue) throw new PlayerError_1.default('Cannot add tracks to queue because no song is currently being played on the server.'); queue.tracks.push(...tracks); return queue; } /** * Internal method used to create queue * @param {DiscordMessage} message The message * @param {Track} track The track * @returns {Promise<Queue>} * @private */ _createQueue(message, track) { return new Promise((resolve) => { const channel = message.member.voice ? message.member.voice.channel : null; if (!channel) return void this.emit(Constants_1.PlayerEvents.ERROR, Constants_1.PlayerErrorEventCodes.NOT_CONNECTED, message, new PlayerError_1.default('Voice connection is not available in this server!')); const queue = new Queue_1.Queue(this, message); this.queues.set(message.guild.id, queue); channel .join() .then((connection) => { this.emit(Constants_1.PlayerEvents.CONNECTION_CREATE, message, connection); queue.voiceConnection = connection; if (this.options.autoSelfDeaf) connection.voice.setSelfDeaf(true); queue.tracks.push(track); this.emit(Constants_1.PlayerEvents.QUEUE_CREATE, message, queue); resolve(queue); this._playTrack(queue, true); }) .catch((err) => { var _a; this.queues.delete(message.guild.id); this.emit(Constants_1.PlayerEvents.ERROR, Constants_1.PlayerErrorEventCodes.UNABLE_TO_JOIN, message, new PlayerError_1.default((_a = err.message) !== null && _a !== void 0 ? _a : err)); }); }); } /** * Internal method used to init stream playing * @param {Queue} queue The queue * @param {boolean} firstPlay If this is a first play * @returns {Promise<void>} * @private */ _playTrack(queue, firstPlay) { return __awaiter(this, void 0, void 0, function* () { if (queue.stopped) return; if (!queue.autoPlay && queue.tracks.length === 1 && !queue.loopMode && !queue.repeatMode && !firstPlay) { if (this.options.leaveOnEnd && !queue.stopped) { this.queues.delete(queue.guildID); const timeout = setTimeout(() => { queue.voiceConnection.channel.leave(); }, this.options.leaveOnEndCooldown || 0); this._cooldownsTimeout.set(`end_${queue.guildID}`, timeout); } this.queues.delete(queue.guildID); if (queue.stopped) { return void this.emit(Constants_1.PlayerEvents.MUSIC_STOP, queue.firstMessage); } return void this.emit(Constants_1.PlayerEvents.QUEUE_END, queue.firstMessage, queue); } if (queue.autoPlay && !queue.repeatMode && !firstPlay) { const oldTrack = queue.tracks.shift(); const info = oldTrack.raw.source === 'youtube' ? yield discord_ytdl_core_1.default.getInfo(oldTrack.url).catch((e) => { }) : null; if (info) { const res = yield Util_1.default.ytSearch(info.related_videos[0].title, { player: this, limit: 1, user: oldTrack.requestedBy }) .then((v) => v[0]) .catch((e) => { }); if (res) { queue.tracks.push(res); if (queue.loopMode) queue.tracks.push(oldTrack); queue.previousTracks.push(oldTrack); } } } else if