From b8eec6dbd48f2bc608c91c26dc7fc68aaf075155 Mon Sep 17 00:00:00 2001 From: Snowflake107 Date: Mon, 10 May 2021 10:43:37 +0545 Subject: [PATCH] rough --- package.json | 2 +- src/Player.ts | 173 +++++++++++++++++++++++++++++++++++++-- src/Structures/Queue.ts | 35 +++++++- src/Structures/Track.ts | 6 +- src/utils/Constants.ts | 1 + src/utils/PlayerError.ts | 11 ++- yarn.lock | 24 +++--- 7 files changed, 226 insertions(+), 26 deletions(-) diff --git a/package.json b/package.json index 6fd2029..9726515 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "@discordjs/opus": "^0.5.0", "@types/node": "^14.14.41", "@types/ws": "^7.4.1", - "discord.js": "^12.5.3", + "discord.js": "discordjs/discord.js", "discord.js-docgen": "discordjs/docgen#ts-patch", "jsdoc-babel": "^0.5.0", "prettier": "^2.2.1", diff --git a/src/Player.ts b/src/Player.ts index b732e1c..9601b2e 100644 --- a/src/Player.ts +++ b/src/Player.ts @@ -1,13 +1,19 @@ import { EventEmitter } from 'events'; -import { Client, Collection, Snowflake, Message } from 'discord.js'; +import { Client, Collection, Snowflake, Message, Collector } from 'discord.js'; import Util from './utils/Util'; import Queue from './Structures/Queue'; +import Track from './Structures/Track'; +import PlayerError from './utils/PlayerError'; import { ExtractorModel } from './Structures/ExtractorModel'; +import ytdl from 'discord-ytdl-core'; +import { PlayerEvents, PlayerErrorEventCodes } from './utils/Constants'; export class Player extends EventEmitter { public client: Client; public queues = new Collection(); public Extractors = new Collection(); + private _cooldownsTimeout = new Collection(); + private _resultsCollectors = new Collection>(); constructor(client: Client) { super(); @@ -21,18 +27,171 @@ export class Player extends EventEmitter { } public createQueue(message: Message) { - if (this.queues.has(message.guild.id)) return this.queues.get(message.guild.id); - - const queue = new Queue(this, message); - void this.queues.set(message.guild.id, queue); - return queue; + return new Promise((resolve) => { + if (this.queues.has(message.guild.id)) return this.queues.get(message.guild.id); + const channel = message.member.voice?.channel; + if (!channel) return void this.emit( + PlayerEvents.ERROR, + new PlayerError('Voice connection is not available in this server!', PlayerErrorEventCodes.NOT_CONNECTED, message) + ); + + const queue = new Queue(this, message.guild); + void this.queues.set(message.guild.id, queue); + + channel + .join() + .then((connection) => { + this.emit(PlayerEvents.CONNECTION_CREATE, message, connection); + + queue.voiceConnection = connection; + if (queue.options.setSelfDeaf) connection.voice.setSelfDeaf(true); + this.emit(PlayerEvents.QUEUE_CREATE, message, queue); + resolve(queue); + }) + .catch((err) => { + this.queues.delete(message.guild.id); + this.emit( + PlayerEvents.ERROR, + new PlayerError(err.message ?? err, PlayerErrorEventCodes.UNABLE_TO_JOIN, message) + ); + }); + + return queue; + }) } public getQueue(message: Message) { return this.queues.get(message.guild.id) ?? null; } - + async play(message: Message, query: string | Track, firstResult?: boolean): Promise { + 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; + + const queue = this.getQueue(message); + + if (query instanceof Track) track = query; + else { + if (ytdl.validateURL(query)) { + const info = await ytdl.getBasicInfo(query).catch(() => { }); + if (!info) return void this.emit(PlayerEvents.NO_RESULTS, message, query); + if (info.videoDetails.isLiveContent && !queue.options.enableLive) + return void this.emit( + PlayerEvents.ERROR, + new PlayerError('Live video is not enabled!', PlayerErrorEventCodes.LIVE_VIDEO, message) + ); + 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.buildTimeCode(Util.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 = await extractor.handle(query); + if (data) { + track = new Track(this, { + title: data.title, + description: data.description, + duration: Util.buildTimeCode(Util.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 = await this.searchTracks(message, query, firstResult); + } + } + + if (track) { + if (queue) { + const q = queue.addTrack(track); + this.emit(PlayerEvents.TRACK_ADD, message, q, q.tracks[q.tracks.length - 1]); + } else { + const q = queue.addTrack(track); + if (q) this.emit(PlayerEvents.TRACK_START, message, q.tracks[0], q); + + // todo: start playing + } + } + } + + private searchTracks(message: Message, query: string, firstResult?: boolean): Promise { + return new Promise(async (resolve) => { + let tracks: Track[] = []; + const queryType = Util.getQueryType(query); + + switch (queryType) { + default: + tracks = await Util.ytSearch(query, { user: message.author, player: this }); + } + + if (tracks.length < 1) return void this.emit(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(PlayerEvents.SEARCH_RESULTS, message, query, tracks, collector); + + collector.on('collect', ({ content }) => { + if (content === 'cancel') { + collector.stop(); + return this.emit(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(PlayerEvents.SEARCH_INVALID_RESPONSE, message, query, tracks, content, collector); + } + }); + + collector.on('end', (_, reason) => { + if (reason === 'time') { + this.emit(PlayerEvents.SEARCH_CANCEL, message, query, tracks); + } + }); + }); + } } export default Player; \ No newline at end of file diff --git a/src/Structures/Queue.ts b/src/Structures/Queue.ts index d83a369..6ed849d 100644 --- a/src/Structures/Queue.ts +++ b/src/Structures/Queue.ts @@ -1,10 +1,41 @@ +import { Guild, Message, VoiceConnection } from 'discord.js'; import { Player } from '../Player'; +import { PlayerOptions } from '../types/types'; +import Track from './Track'; +import { PlayerError } from '../utils/PlayerError'; export class Queue { - public readonly player: Player; + player: Player; + guild: Guild; + firstMessage: Message; + options: PlayerOptions = {}; + tracks: Track[] = []; + voiceConnection: VoiceConnection = null; - constructor(player: Player, data: any) { + constructor(player: Player, guild: Guild) { Object.defineProperty(this, 'player', { value: player, enumerable: false }); + + this.guild = guild; + } + + get playing() { + return this.tracks[0]; + } + + async play(message: Message, query: string | Track, firstResult?: boolean) { + return await this.player.play(message, query, firstResult); + } + + addTrack(track: Track) { + if (!track || !(track instanceof Track)) throw new PlayerError('No track specified to add to the queue'); + this.tracks.push(track); + return this; + } + + addTracks(tracks: Track[]) { + this.tracks.push(...tracks); + + return this; } } diff --git a/src/Structures/Track.ts b/src/Structures/Track.ts index 253dffa..4142061 100644 --- a/src/Structures/Track.ts +++ b/src/Structures/Track.ts @@ -1,8 +1,10 @@ +import { Message } from 'discord.js'; import { Player } from '../Player'; export class Track { - public readonly player: Player; - + readonly player: Player; + readonly message: Message; + constructor(player: Player, data: any) { Object.defineProperty(this, 'player', { value: player, enumerable: false }); } diff --git a/src/utils/Constants.ts b/src/utils/Constants.ts index 1c4bf3d..dd36a0f 100644 --- a/src/utils/Constants.ts +++ b/src/utils/Constants.ts @@ -20,6 +20,7 @@ export enum PlayerEvents { }; export enum PlayerErrorEventCodes { + DEFAULT = 'PlayerError', LIVE_VIDEO = 'LiveVideo', NOT_CONNECTED = 'NotConnected', UNABLE_TO_JOIN = 'UnableToJoin', diff --git a/src/utils/PlayerError.ts b/src/utils/PlayerError.ts index c87e1f6..5185fb1 100644 --- a/src/utils/PlayerError.ts +++ b/src/utils/PlayerError.ts @@ -1,10 +1,19 @@ +import { Message } from "discord.js"; + export default class PlayerError extends Error { - constructor(msg: string, name?: string) { + discordMessage: Message; + + constructor(msg: string, name?: string, message?: Message) { super(); this.name = name ?? 'PlayerError'; this.message = msg; + this.discordMessage = message; Error.captureStackTrace(this); } + + get code() { + return this.name; + } } export { PlayerError }; diff --git a/yarn.lock b/yarn.lock index 8b2d9c3..2ddb0ed 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1802,19 +1802,17 @@ discord.js-docgen@discordjs/docgen#ts-patch: tsubaki "^1.3.2" yargs "^14.0.0" -discord.js@^12.5.3: - version "12.5.3" - resolved "https://registry.yarnpkg.com/discord.js/-/discord.js-12.5.3.tgz#56820d473c24320871df9ea0bbc6b462f21cf85c" - integrity sha512-D3nkOa/pCkNyn6jLZnAiJApw2N9XrIsXUAdThf01i7yrEuqUmDGc7/CexVWwEcgbQR97XQ+mcnqJpmJ/92B4Aw== +discord.js@discordjs/discord.js: + version "12.5.0" + resolved "https://codeload.github.com/discordjs/discord.js/tar.gz/0e40f9b86826ba50aa3840807fb86e1bce6b1c3d" dependencies: "@discordjs/collection" "^0.1.6" "@discordjs/form-data" "^3.0.1" abort-controller "^3.0.0" node-fetch "^2.6.1" - prism-media "^1.2.9" - setimmediate "^1.0.5" + prism-media "^1.2.2" tweetnacl "^1.0.3" - ws "^7.4.4" + ws "^7.3.1" dmd@^4.0.5: version "4.0.6" @@ -3476,7 +3474,7 @@ prettier@^2.2.1: resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.2.1.tgz#795a1a78dd52f073da0cd42b21f9c91381923ff5" integrity sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q== -prism-media@^1.2.7, prism-media@^1.2.9: +prism-media@^1.2.2, prism-media@^1.2.7: version "1.2.9" resolved "https://registry.yarnpkg.com/prism-media/-/prism-media-1.2.9.tgz#8d4f97b36efdfc82483eb8d3db64020767866f36" integrity sha512-UHCYuqHipbTR1ZsXr5eg4JUmHER8Ss4YEb9Azn+9zzJ7/jlTtD1h0lc4g6tNx3eMlB8Mp6bfll0LPMAV4R6r3Q== @@ -3834,11 +3832,6 @@ set-value@^2.0.0, set-value@^2.0.1: is-plain-object "^2.0.3" split-string "^3.0.1" -setimmediate@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" - integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU= - shebang-command@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" @@ -4572,6 +4565,11 @@ write@1.0.3: dependencies: mkdirp "^0.5.1" +ws@^7.3.1: + version "7.4.5" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.5.tgz#a484dd851e9beb6fdb420027e3885e8ce48986c1" + integrity sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g== + ws@^7.4.4: version "7.4.4" resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.4.tgz#383bc9742cb202292c9077ceab6f6047b17f2d59"