diff --git a/src/Player.ts b/src/Player.ts index c0a3fb0..c0765ac 100644 --- a/src/Player.ts +++ b/src/Player.ts @@ -1,4 +1,4 @@ -import { Client, Collection, Guild, Snowflake, User } from "discord.js"; +import { Client, Collection, Guild, Snowflake, User, VoiceState } from "discord.js"; import { TypedEmitter as EventEmitter } from "tiny-typed-emitter"; import { Queue } from "./Structures/Queue"; import { VoiceUtils } from "./VoiceInterface/VoiceUtils"; @@ -31,6 +31,39 @@ class DiscordPlayer extends EventEmitter { * @type {Discord.Client} */ this.client = client; + + this.client.on("voiceStateUpdate", this._handleVoiceState.bind(this)); + } + + private _handleVoiceState(oldState: VoiceState, newState: VoiceState): void { + const queue = this.getQueue(oldState.guild.id); + if (!queue) return; + + if (oldState.member.id === this.client.user.id && !newState.channelID) { + queue.destroy(); + return void this.emit("botDisconnect", queue); + } + + if (!queue.options.leaveOnEmpty || !queue.connection || !queue.connection.channel) return; + + if (!oldState.channelID || newState.channelID) { + const emptyTimeout = queue._cooldownsTimeout.get(`empty_${oldState.guild.id}`); + const channelEmpty = Util.isVoiceEmpty(queue.connection.channel); + + if (!channelEmpty && emptyTimeout) { + clearTimeout(emptyTimeout); + queue._cooldownsTimeout.delete(`empty_${oldState.guild.id}`); + } + } else { + if (!Util.isVoiceEmpty(queue.connection.channel)) return; + const timeout = setTimeout(() => { + if (!Util.isVoiceEmpty(queue.connection.channel)) return; + if (!this.queues.has(queue.guild.id)) return; + queue.destroy(); + this.emit("channelEmpty", queue); + }, queue.options.leaveOnEmptyCooldown || 0); + queue._cooldownsTimeout.set(`empty_${oldState.guild.id}`, timeout); + } } /** diff --git a/src/Structures/Queue.ts b/src/Structures/Queue.ts index d8d8be0..5b609a1 100644 --- a/src/Structures/Queue.ts +++ b/src/Structures/Queue.ts @@ -1,4 +1,4 @@ -import { Guild, StageChannel, VoiceChannel } from "discord.js"; +import { Collection, Guild, Snowflake, StageChannel, VoiceChannel } from "discord.js"; import { Player } from "../Player"; import { StreamDispatcher } from "../VoiceInterface/BasicStreamDispatcher"; import Track from "./Track"; @@ -18,6 +18,7 @@ class Queue { public playing = false; public metadata?: T = null; public repeatMode: QueueRepeatMode = 0; + public _cooldownsTimeout = new Collection(); constructor(player: Player, guild: Guild, options: PlayerOptions = {}) { this.player = player; @@ -48,6 +49,10 @@ class Queue { return this.connection.audioResource?.metadata ?? this.tracks[0]; } + nowPlaying() { + return this.current; + } + async connect(channel: StageChannel | VoiceChannel) { 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); @@ -125,8 +130,12 @@ class Queue { if (src && (this.playing || this.tracks.length) && !options.immediate) return this.addTrack(src); const track = options.filtersUpdate ? this.current : src ?? this.tracks.shift(); if (!track) return; - this.previousTracks = this.previousTracks.filter((x) => x._trackID !== track._trackID); - this.previousTracks.push(track); + + if (!options.filtersUpdate) { + this.previousTracks = this.previousTracks.filter((x) => x._trackID !== track._trackID); + this.previousTracks.push(track); + } + let stream; if (["youtube", "spotify"].includes(track.raw.source)) { if (track.raw.source === "spotify" && !track.raw.engine) { diff --git a/src/types/types.ts b/src/types/types.ts index 76faee8..5bc0821 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -119,21 +119,21 @@ export enum QueryType { } export interface PlayerEvents { - botDisconnect: () => any; - channelEmpty: () => any; - connectionCreate: () => any; + botDisconnect: (queue: Queue) => any; + channelEmpty: (queue: Queue) => any; + connectionCreate: (queue: Queue) => any; debug: (queue: Queue, message: string) => any; error: (queue: Queue, error: Error) => any; - musicStop: () => any; - noResults: () => any; - playlistAdd: () => any; - playlistParseEnd: () => any; - playlistParseStart: () => any; - queueCreate: () => any; + musicStop: (queue: Queue) => any; + noResults: (queue: Queue) => any; + playlistAdd: (queue: Queue) => any; + playlistParseEnd: (queue: Queue) => any; + playlistParseStart: (queue: Queue) => any; + queueCreate: (queue: Queue) => any; queueEnd: (queue: Queue) => any; - searchCancel: () => any; - searchInvalidResponse: () => any; - searchResults: () => any; + searchCancel: (queue: Queue) => any; + searchInvalidResponse: (queue: Queue) => any; + searchResults: (queue: Queue) => any; trackAdd: (queue: Queue, track: Track) => any; tracksAdd: (queue: Queue, track: Track[]) => any; trackStart: (queue: Queue, track: Track) => any; diff --git a/src/utils/Util.ts b/src/utils/Util.ts index 7411df1..8baff32 100644 --- a/src/utils/Util.ts +++ b/src/utils/Util.ts @@ -1,3 +1,4 @@ +import { StageChannel, VoiceChannel } from "discord.js"; import { TimeData } from "../types/types"; class Util { @@ -34,6 +35,10 @@ class Util { if (!Array.isArray(arr)) return; return arr[arr.length - 1]; } + + static isVoiceEmpty(channel: VoiceChannel | StageChannel) { + return channel.members.filter((member) => !member.user.bot).size === 0; + } } export { Util };