From c7e6962219dac8d8dbb071bc310496cdc472c1d0 Mon Sep 17 00:00:00 2001 From: Snowflake107 Date: Tue, 6 Apr 2021 19:51:18 +0545 Subject: [PATCH] base --- src/Player.ts | 202 ++++++++++++++++++++++++++++++++++++++- src/utils/Constants.ts | 13 ++- src/utils/PlayerError.ts | 8 ++ tsconfig.json | 3 +- 4 files changed, 223 insertions(+), 3 deletions(-) create mode 100644 src/utils/PlayerError.ts diff --git a/src/Player.ts b/src/Player.ts index 01228e7..f4f69df 100644 --- a/src/Player.ts +++ b/src/Player.ts @@ -6,7 +6,9 @@ import Util from './utils/Util'; import AudioFilters from './utils/AudioFilters'; import Queue from './Structures/Queue'; import Track from './Structures/Track'; -import { PlayerEvents } from './utils/Constants'; +import { PlayerErrorEventCodes, PlayerEvents } from './utils/Constants'; +import PlayerError from "./utils/PlayerError"; +import ytdl from "discord-ytdl-core"; // @ts-ignore import spotify from 'spotify-url-info'; @@ -147,4 +149,202 @@ export default class Player extends EventEmitter { }); }); } + + async play(message: Message, query: string | Track, firstResult?: boolean) { + if (!message) throw new PlayerError("Play function needs message"); + if (!query) throw new PlayerError("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) track = query; + else { + if (ytdl.validateURL(query)) { + const info = await ytdl.getBasicInfo(query).catch(() => {}); + if (!info) return this.emit(PlayerEvents.NO_RESULTS, message, query); + if (info.videoDetails.isLiveContent && !this.options.enableLive) return this.emit(PlayerEvents.ERROR, PlayerErrorEventCodes.LIVE_VIDEO, message, new PlayerError("Live video is not enabled!")); + const lastThumbnail = info.videoDetails.thumbnails[info.videoDetails.thumbnails.length - 1]; + + track = new 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.durationString(Util.parseMS(parseInt(info.videoDetails.lengthSeconds) * 1000)), + views: parseInt(info.videoDetails.viewCount), + requestedBy: message.author, + fromPlaylist: false, + source: "youtube" + }); + } else { + track = await this._searchTracks(message, query, firstResult); + } + } + + if (track) { + if (this.isPlaying(message)) { + const queue = this._addTrackToQueue(message, track); + this.emit(PlayerEvents.TRACK_ADD, message, queue, queue.tracks[queue.tracks.length - 1]); + } else { + const queue = await this._createQueue(message, track); + if (queue) this.emit(PlayerEvents.TRACK_START, message, queue.tracks[0], queue); + } + } + } + + isPlaying(message: Message) { + return this.queues.some(g => g.guildID === message.guild.id); + } + + getQueue(message: Message) { + return this.queues.find(g => g.guildID === message.guild.id); + } + + private _addTrackToQueue(message: Message, track: Track) { + const queue = this.getQueue(message); + if (!queue) this.emit(PlayerEvents.ERROR, PlayerErrorEventCodes.NOT_PLAYING, message, new PlayerError("Player is not available in this server")); + if (!track || !(track instanceof Track)) throw new PlayerError('No track specified to add to the queue'); + queue.tracks.push(track); + return queue; + } + + private _createQueue(message: Message, track: Track): Promise { + return new Promise((resolve) => { + const channel = message.member.voice ? message.member.voice.channel : null + if (!channel) return this.emit(PlayerEvents.ERROR, PlayerErrorEventCodes.NOT_CONNECTED, message, new PlayerError("Voice connection is not available in this server!")); + + const queue = new Queue(this, message, this.filters); + this.queues.set(message.guild.id, queue); + + channel.join().then((connection) => { + this.emit(PlayerEvents.CONNECTION_CREATE, message, connection); + + queue.voiceConnection = connection; + if (this.options.autoSelfDeaf) connection.voice.setSelfDeaf(true); + queue.tracks.push(track); + this.emit(PlayerEvents.QUEUE_CREATE, message, queue); + resolve(queue); + // this._playTrack(queue, true) + }).catch((err) => { + this.queues.delete(message.guild.id); + this.emit(PlayerEvents.ERROR, PlayerErrorEventCodes.UNABLE_TO_JOIN, message, new PlayerError(err.message ?? err)); + }); + }); + } + + private async _playTrack(queue: Queue, firstPlay: boolean) { + if (queue.stopped) return; + + if (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 this.emit(PlayerEvents.MUSIC_STOP, queue.firstMessage); + } + + return this.emit(PlayerEvents.QUEUE_END, queue.firstMessage, queue); + } + + if (!queue.repeatMode && !firstPlay) { + const oldTrack = queue.tracks.shift(); + if (queue.loopMode) queue.tracks.push(oldTrack); + queue.previousTracks.push(oldTrack); + } + + const track = queue.playing; + + queue.lastSkipped = false; + this._playStream(queue, false).then(() => { + if (!firstPlay) this.emit(PlayerEvents.TRACK_START, queue.firstMessage, track, queue); + }); + } + + private _playStream(queue: Queue, updateFilter: boolean, seek?: number): Promise { + return new Promise(async (resolve) => { + const ffmeg = Util.checkFFmpeg(); + if (!ffmeg) return; + + const seekTime = typeof seek === "number" ? seek : updateFilter ? queue.voiceConnection.dispatcher.streamTime + queue.additionalStreamTime : undefined; + const encoderArgsFilters: string[] = []; + + Object.keys(queue.filters).forEach((filterName) => { + // @ts-ignore + if (queue.filters[filterName]) { + // @ts-ignore + encoderArgsFilters.push(this.filters[filterName]); + } + }); + + let encoderArgs: string[]; + if (encoderArgsFilters.length < 1) { + encoderArgs = [] + } else { + encoderArgs = ['-af', encoderArgsFilters.join(',')] + } + + let newStream: any; + if (queue.playing.raw.source === "youtube") { + newStream = ytdl(queue.playing.url, { + filter: 'audioonly', + opusEncoded: true, + encoderArgs, + seek: seekTime / 1000, + highWaterMark: 1 << 25, + ...this.options.ytdlDownloadOptions + }); + } else { + newStream = ytdl.arbitraryStream(queue.playing.raw.source === "soundcloud" ? await queue.playing.raw.engine.downloadProgressive() : queue.playing.raw.engine, { + opusEncoded: true, + encoderArgs, + seek: seekTime / 1000 + }); + } + + setTimeout(() => { + if (queue.stream) queue.stream.destroy(); + queue.stream = newStream; + queue.voiceConnection.play(newStream, { + type: 'opus', + bitrate: 'auto' + }); + + if (seekTime) { + queue.additionalStreamTime = seekTime; + } + queue.voiceConnection.dispatcher.setVolumeLogarithmic(queue.calculatedVolume / 200); + queue.voiceConnection.dispatcher.on('start', () => { + resolve(); + }); + + queue.voiceConnection.dispatcher.on('finish', () => { + queue.additionalStreamTime = 0; + return this._playTrack(queue, false); + }); + + newStream.on('error', (error: Error) => { + if (error.message.toLowerCase().includes('video unavailable')) { + this.emit(PlayerEvents.ERROR, PlayerErrorEventCodes.VIDEO_UNAVAILABLE, queue.firstMessage, queue.playing, error); + this._playTrack(queue, false); + } else { + this.emit(PlayerEvents.ERROR, error, queue.firstMessage, error); + } + }); + }, 1000); + }); + } + } diff --git a/src/utils/Constants.ts b/src/utils/Constants.ts index 1b501e7..e44ff01 100644 --- a/src/utils/Constants.ts +++ b/src/utils/Constants.ts @@ -1,7 +1,9 @@ export const PlayerEvents = { BOT_DISCONNECT: 'botDisconnect', CHANNEL_EMPTY: 'channelEmpty', + CONNECTION_CREATE: 'connectionCreate', ERROR: 'error', + MUSIC_STOP: 'musicStop', NO_RESULTS: 'noResults', PLAYLIST_ADD: 'playlistAdd', PLAYLIST_PARSE_END: 'playlistParseEnd', @@ -12,5 +14,14 @@ export const PlayerEvents = { SEARCH_INVALID_RESPONSE: 'searchInvalidResponse', SEARCH_RESULTS: 'searchResults', TRACK_ADD: 'trackAdd', - TRACK_START: 'trackStart' + TRACK_START: 'trackStart', +}; + +export const PlayerErrorEventCodes = { + LIVE_VIDEO: "LiveVideo", + NOT_CONNECTED: "NotConnected", + UNABLE_TO_JOIN: "UnableToJoin", + NOT_PLAYING: "NotPlaying", + PARSE_ERROR: "ParseError", + VIDEO_UNAVAILABLE: "VideoUnavailable" }; diff --git a/src/utils/PlayerError.ts b/src/utils/PlayerError.ts new file mode 100644 index 0000000..2c362eb --- /dev/null +++ b/src/utils/PlayerError.ts @@ -0,0 +1,8 @@ +export default class PlayerError extends Error { + constructor(msg: string, name?: string) { + super(); + this.name = name ?? "PlayerError"; + this.message = msg; + Error.captureStackTrace(this); + } +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 441108e..d75c6d9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,8 @@ "outDir": "./lib", "strict": true, "strictNullChecks": false, - "resolveJsonModule": true + "resolveJsonModule": true, + "esModuleInterop": true }, "include": [ "src/**/*"