From 4acd25a6abba96359a6559b3f5acc56e10051ed4 Mon Sep 17 00:00:00 2001 From: Snowflake107 Date: Tue, 22 Jun 2021 11:18:20 +0545 Subject: [PATCH] re-implement voice --- src/Structures/Queue.ts | 202 +++++++++++++------- src/VoiceInterface/BasicStreamDispatcher.ts | 1 + src/types/types.ts | 8 + 3 files changed, 147 insertions(+), 64 deletions(-) diff --git a/src/Structures/Queue.ts b/src/Structures/Queue.ts index d2bda36..243cc0b 100644 --- a/src/Structures/Queue.ts +++ b/src/Structures/Queue.ts @@ -22,6 +22,8 @@ class Queue { private _streamTime: number = 0; public _cooldownsTimeout = new Collection(); private _activeFilters: any[] = []; + private _filtersUpdate = false; + public destroyed = false; /** * Queue constructor @@ -50,6 +52,42 @@ class Queue { */ this.options = {}; + /** + * If this queue is destroyed + * @type {boolean} + * @name Queue#destroyed + */ + + /** + * Queue repeat mode + * @type {QueueRepeatMode} + * @name Queue#repeatMode + */ + + /** + * Queue metadata + * @type {any} + * @name Queue#metadata + */ + + /** + * Previous tracks + * @type {Track[]} + * @name Queue#previousTracks + */ + + /** + * Regular tracks + * @type {Track[]} + * @name Queue#tracks + */ + + /** + * The connection + * @type {StreamDispatcher} + * @name Queue#connection + */ + Object.assign( this.options, { @@ -70,6 +108,7 @@ class Queue { * @type {Track} */ get current() { + this.#watchDestroyed(); return this.connection.audioResource?.metadata ?? this.tracks[0]; } @@ -78,6 +117,7 @@ class Queue { * @returns {Track} */ nowPlaying() { + this.#watchDestroyed(); return this.current; } @@ -87,22 +127,47 @@ class Queue { * @returns {Promise} */ async connect(channel: StageChannel | VoiceChannel) { + this.#watchDestroyed(); if (!["stage", "voice"].includes(channel?.type)) throw new TypeError(`Channel type must be voice or stage, got ${channel?.type}!`); const connection = await this.player.voiceUtils.connect(channel, { deaf: this.options.autoSelfDeaf }); this.connection = connection; - // it's ok to use this here since Queue listens to the events 1 time per play and destroys the listener - this.connection.setMaxListeners(Infinity); - if (channel.type === "stage") await channel.guild.me.voice.setRequestToSpeak(true).catch(() => {}); - this.connection.on("error", (err) => this.player.emit("error", this, err)); + this.connection.on("error", (err) => this.player.emit("connectionError", this, err)); this.connection.on("debug", (msg) => this.player.emit("debug", this, msg)); this.player.emit("connectionCreate", this, this.connection); + this.connection.on("start", () => { + this.playing = true; + if (!this._filtersUpdate) this.player.emit("trackStart", this, this.current); + this._filtersUpdate = false; + }); + + this.connection.on("finish", async () => { + this.playing = false; + if (this._filtersUpdate) return; + this._streamTime = 0; + + if (!this.tracks.length && this.repeatMode === QueueRepeatMode.OFF) { + if (this.options.leaveOnEnd) this.destroy(); + this.player.emit("queueEnd", this); + } else { + if (this.repeatMode !== QueueRepeatMode.AUTOPLAY) { + if (this.repeatMode === QueueRepeatMode.TRACK) return void this.play(Util.last(this.previousTracks), { immediate: true }); + if (this.repeatMode === QueueRepeatMode.QUEUE) this.tracks.push(Util.last(this.previousTracks)); + const nextTrack = this.tracks.shift(); + this.play(nextTrack, { immediate: true }); + return; + } else { + this._handleAutoplay(Util.last(this.previousTracks)); + } + } + }); + return this; } @@ -115,6 +180,8 @@ class Queue { this.connection.end(); if (disconnect) this.connection.disconnect(); this.player.queues.delete(this.guild.id); + this.player.voiceUtils.cache.delete(this.guild.id); + this.destroyed = true; } /** @@ -122,7 +189,9 @@ class Queue { * @returns {boolean} */ skip() { + this.#watchDestroyed(); if (!this.connection) return false; + this._filtersUpdate = false; this.connection.end(); return true; } @@ -133,6 +202,7 @@ class Queue { * @returns {void} */ addTrack(track: Track) { + this.#watchDestroyed(); this.tracks.push(track); this.player.emit("trackAdd", this, track); } @@ -142,6 +212,7 @@ class Queue { * @param {Track[]} tracks Array of tracks to add */ addTracks(tracks: Track[]) { + this.#watchDestroyed(); this.tracks.push(...tracks); this.player.emit("tracksAdd", this, tracks); } @@ -152,6 +223,7 @@ class Queue { * @returns {boolean} */ setPaused(paused?: boolean) { + this.#watchDestroyed(); if (!this.connection) return false; return paused ? this.connection.pause(true) : this.connection.resume(); } @@ -162,6 +234,7 @@ class Queue { * @returns {void} */ setBitrate(bitrate: number | "auto") { + this.#watchDestroyed(); if (!this.connection?.audioResource?.encoder) return; if (bitrate === "auto") bitrate = this.connection.channel?.bitrate ?? 64000; this.connection.audioResource.encoder.setBitrate(bitrate); @@ -173,6 +246,7 @@ class Queue { * @returns {boolean} */ setVolume(amount: number) { + this.#watchDestroyed(); if (!this.connection) return false; this.options.initialVolume = amount; return this.connection.setVolume(amount); @@ -183,6 +257,7 @@ class Queue { * @returns {boolean} */ setRepeatMode(mode: QueueRepeatMode) { + this.#watchDestroyed(); if (![QueueRepeatMode.OFF, QueueRepeatMode.QUEUE, QueueRepeatMode.TRACK, QueueRepeatMode.AUTOPLAY].includes(mode)) throw new Error(`Unknown repeat mode "${mode}"!`); if (mode === this.repeatMode) return false; this.repeatMode = mode; @@ -194,6 +269,7 @@ class Queue { * @type {number} */ get volume() { + this.#watchDestroyed(); if (!this.connection) return 100; return this.connection.volume; } @@ -207,6 +283,7 @@ class Queue { * @type {number} */ get streamTime() { + this.#watchDestroyed(); if (!this.connection) return 0; const playbackTime = this._streamTime + this.connection.streamTime; const NC = this._activeFilters.includes("nightcore") ? 1.25 : null; @@ -221,6 +298,7 @@ class Queue { * @returns {AudioFilters} */ getFiltersEnabled() { + this.#watchDestroyed(); return AudioFilters.names.filter((x) => this._activeFilters.includes(x)); } @@ -229,6 +307,7 @@ class Queue { * @returns {AudioFilters} */ getFiltersDisabled() { + this.#watchDestroyed(); return AudioFilters.names.filter((x) => !this._activeFilters.includes(x)); } @@ -238,6 +317,7 @@ class Queue { * @returns {Promise} */ async setFilters(filters?: QueueFilters) { + this.#watchDestroyed(); if (!filters || !Object.keys(filters).length) { // reset filters const streamTime = this.streamTime; @@ -276,6 +356,7 @@ class Queue { * @returns {boolean} */ async seek(position: number) { + this.#watchDestroyed(); if (!this.playing || !this.current) return false; if (position < 1) position = 0; if (position >= this.current.durationMS) return this.skip(); @@ -294,7 +375,17 @@ class Queue { * @returns {Promise} */ async back() { - return await this.play(Util.last(this.previousTracks), { immediate: true }); + this.#watchDestroyed(); + const prev = this.previousTracks[this.previousTracks.length - 2]; // because last item is the current track + if (!prev) throw new Error("Could not find previous track"); + + return await this.play(prev, { immediate: true }); + } + + clear() { + this.#watchDestroyed(); + this.tracks = []; + this.previousTracks = []; } /** @@ -304,6 +395,7 @@ class Queue { * @returns {Promise} */ async play(src?: Track, options: PlayOptions = {}): Promise { + this.#watchDestroyed(); if (!this.connection || !this.connection.voiceConnection) throw new Error("Voice connection is not available, use .connect()!"); if (src && (this.playing || this.tracks.length) && !options.immediate) return this.addTrack(src); const track = options.filtersUpdate && !options.immediate ? src || this.current : src ?? this.tracks.shift(); @@ -314,8 +406,7 @@ class Queue { this.previousTracks.push(track); } - let stream, - pauseEvent = false; + let stream; if (["youtube", "spotify"].includes(track.raw.source)) { if (track.raw.source === "spotify" && !track.raw.engine) { track.raw.engine = await YouTube.search(`${track.author} ${track.title}`, { type: "video" }) @@ -333,7 +424,6 @@ class Queue { encoderArgs: options.encoderArgs ?? this._activeFilters.length ? ["-af", AudioFilters.create(this._activeFilters)] : [], seek: options.seek ? options.seek / 1000 : 0 }).on("error", (err) => { - pauseEvent = true; return err.message.toLowerCase().includes("premature close") ? null : this.player.emit("error", this, err); }); } else { @@ -348,7 +438,6 @@ class Queue { } ) .on("error", (err) => { - pauseEvent = true; return err.message.toLowerCase().includes("premature close") ? null : this.player.emit("error", this, err); }); } @@ -359,66 +448,45 @@ class Queue { }); if (options.seek) this._streamTime = options.seek; + this._filtersUpdate = options.filtersUpdate; - const dispatcher = await this.connection.playStream(resource); - dispatcher.setVolume(this.options.initialVolume); - - // need to use these events here - dispatcher.once("start", () => { - this.playing = true; - if (options.filtersUpdate || pauseEvent) return; - this.player.emit("trackStart", this, this.current); - }); - - dispatcher.once("finish", async () => { - this.playing = false; - if (options.filtersUpdate || pauseEvent) return; - - this._streamTime = 0; - - if (!this.tracks.length && this.repeatMode === QueueRepeatMode.OFF) { - if (this.options.leaveOnEnd) this.destroy(); - this.player.emit("queueEnd", this); - } else { - if (this.repeatMode !== QueueRepeatMode.AUTOPLAY) { - if (this.repeatMode === QueueRepeatMode.TRACK) return void this.play(Util.last(this.previousTracks), { immediate: true }); - if (this.repeatMode === QueueRepeatMode.QUEUE) this.tracks.push(Util.last(this.previousTracks)); - const nextTrack = this.tracks.shift(); - this.play(nextTrack, { immediate: true }); - return; - } else { - if (![track.source, track.raw?.source].includes("youtube")) { - if (this.options.leaveOnEnd) this.destroy(); - return void this.player.emit("queueEnd", this); - } - const info = await ytdl - .getInfo(track.url) - .then((x) => x.related_videos[0]) - .catch(() => {}); - if (!info) { - if (this.options.leaveOnEnd) this.destroy(); - return void this.player.emit("queueEnd", this); - } - - const nextTrack = new Track(this.player, { - title: info.title, - url: `https://www.youtube.com/watch?v=${info.id}`, - duration: info.length_seconds ? Util.buildTimeCode(Util.parseMS(info.length_seconds * 1000)) : "0:00", - description: "", - thumbnail: Util.last(info.thumbnails).url, - views: parseInt(info.view_count.replace(/[^0-9]/g, "")), - author: typeof info.author === "string" ? info.author : info.author.name, - requestedBy: track.requestedBy, - source: "youtube" - }); - - this.play(nextTrack, { immediate: true }); - } - } + this.connection.playStream(resource).then(() => { + this.connection.setVolume(this.options.initialVolume); }); } + private async _handleAutoplay(track: Track): Promise { + this.#watchDestroyed(); + if (!track || ![track.source, track.raw?.source].includes("youtube")) { + if (this.options.leaveOnEnd) this.destroy(); + return void this.player.emit("queueEnd", this); + } + const info = await ytdl + .getInfo(track.url) + .then((x) => x.related_videos[0]) + .catch(() => {}); + if (!info) { + if (this.options.leaveOnEnd) this.destroy(); + return void this.player.emit("queueEnd", this); + } + + const nextTrack = new Track(this.player, { + title: info.title, + url: `https://www.youtube.com/watch?v=${info.id}`, + duration: info.length_seconds ? Util.buildTimeCode(Util.parseMS(info.length_seconds * 1000)) : "0:00", + description: "", + thumbnail: Util.last(info.thumbnails).url, + views: parseInt(info.view_count.replace(/[^0-9]/g, "")), + author: typeof info.author === "string" ? info.author : info.author.name, + requestedBy: track.requestedBy, + source: "youtube" + }); + + this.play(nextTrack, { immediate: true }); + } + *[Symbol.iterator]() { + this.#watchDestroyed(); yield* this.tracks; } @@ -427,6 +495,7 @@ class Queue { * @returns {object} */ toJSON() { + this.#watchDestroyed(); return { guild: this.guild.id, voiceChannel: this.connection?.channel?.id, @@ -440,9 +509,14 @@ class Queue { * @returns {string} */ toString() { + this.#watchDestroyed(); if (!this.tracks.length) return "No songs available to display!"; return `**Upcoming Songs:**\n${this.tracks.map((m, i) => `${i + 1}. **${m.title}**`).join("\n")}`; } + + #watchDestroyed() { + if (this.destroyed) throw new Error("Cannot use destroyed queue"); + } } export { Queue }; diff --git a/src/VoiceInterface/BasicStreamDispatcher.ts b/src/VoiceInterface/BasicStreamDispatcher.ts index 5e24ea5..f7a0294 100644 --- a/src/VoiceInterface/BasicStreamDispatcher.ts +++ b/src/VoiceInterface/BasicStreamDispatcher.ts @@ -132,6 +132,7 @@ class StreamDispatcher extends EventEmitter { */ disconnect() { try { + this.audioPlayer.stop(true); this.voiceConnection.destroy(); } catch {} } diff --git a/src/types/types.ts b/src/types/types.ts index 56444dd..3430041 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -265,6 +265,13 @@ export enum QueryType { * @param {Error} error The error */ +/** + * Emitted on connection error. Sometimes stream errors are emitted here as well. + * @event Player#connectionError + * @param {Queue} queue The queue + * @param {Error} error The error + */ + /** * Emitted when queue ends * @event Player#queueEnd @@ -298,6 +305,7 @@ export interface PlayerEvents { connectionCreate: (queue: Queue, connection: StreamDispatcher) => any; debug: (queue: Queue, message: string) => any; error: (queue: Queue, error: Error) => any; + connectionError: (queue: Queue, error: Error) => any; queueEnd: (queue: Queue) => any; trackAdd: (queue: Queue, track: Track) => any; tracksAdd: (queue: Queue, track: Track[]) => any;