discord-player-play-dl/src/Structures/Queue.ts

316 lines
10 KiB
TypeScript
Raw Normal View History

2021-06-14 22:33:32 +05:00
import { Collection, Guild, StageChannel, VoiceChannel } from "discord.js";
2021-06-11 15:32:22 +05:00
import { Player } from "../Player";
2021-06-11 23:19:52 +05:00
import { StreamDispatcher } from "../VoiceInterface/BasicStreamDispatcher";
2021-06-11 15:32:22 +05:00
import Track from "./Track";
2021-06-13 17:43:36 +05:00
import { PlayerOptions, PlayOptions, QueueRepeatMode } from "../types/types";
2021-06-11 23:19:52 +05:00
import ytdl from "discord-ytdl-core";
import { AudioResource, StreamType } from "@discordjs/voice";
2021-06-13 17:43:36 +05:00
import { Util } from "../utils/Util";
2021-06-13 18:09:25 +05:00
import YouTube from "youtube-sr";
2021-06-11 15:32:22 +05:00
2021-06-12 11:37:41 +05:00
class Queue<T = unknown> {
2021-06-11 15:32:22 +05:00
public readonly guild: Guild;
public readonly player: Player;
2021-06-11 23:19:52 +05:00
public connection: StreamDispatcher;
2021-06-11 15:32:22 +05:00
public tracks: Track[] = [];
2021-06-13 17:43:36 +05:00
public previousTracks: Track[] = [];
2021-06-11 15:32:22 +05:00
public options: PlayerOptions;
2021-06-11 23:19:52 +05:00
public playing = false;
2021-06-12 11:37:41 +05:00
public metadata?: T = null;
2021-06-13 17:43:36 +05:00
public repeatMode: QueueRepeatMode = 0;
2021-06-13 21:47:04 +05:00
public _cooldownsTimeout = new Collection<string, NodeJS.Timeout>();
2021-06-11 15:32:22 +05:00
2021-06-14 22:51:54 +05:00
/**
* Queue constructor
* @param {Player} player The player that instantiated this queue
* @param {Guild} guild The guild that instantiated this queue
* @param {PlayerOptions={}} options Player options for the queue
*/
2021-06-11 15:32:22 +05:00
constructor(player: Player, guild: Guild, options: PlayerOptions = {}) {
2021-06-14 22:51:54 +05:00
/**
* The player that instantiated this queue
* @type {Player}
*/
2021-06-11 15:32:22 +05:00
this.player = player;
2021-06-14 22:51:54 +05:00
/**
* The guild that instantiated this queue
* @type {Guild}
*/
2021-06-11 15:32:22 +05:00
this.guild = guild;
2021-06-14 22:51:54 +05:00
/**
* The player options for this queue
* @type {PlayerOptions}
*/
2021-06-11 15:32:22 +05:00
this.options = {};
Object.assign(
this.options,
{
leaveOnEnd: true,
leaveOnStop: true,
leaveOnEmpty: true,
leaveOnEmptyCooldown: 1000,
autoSelfDeaf: true,
enableLive: false,
2021-06-14 19:32:37 +05:00
ytdlOptions: {},
2021-06-11 15:32:22 +05:00
useSafeSearch: false,
disableAutoRegister: false,
2021-06-11 23:19:52 +05:00
fetchBeforeQueued: false,
2021-06-17 18:22:03 +05:00
initialVolume: 50
2021-06-11 15:32:22 +05:00
} as PlayerOptions,
options
);
}
2021-06-14 22:51:54 +05:00
/**
* Returns current track
* @returns {Track}
*/
2021-06-11 16:50:43 +05:00
get current() {
2021-06-11 23:19:52 +05:00
return this.connection.audioResource?.metadata ?? this.tracks[0];
2021-06-11 16:50:43 +05:00
}
2021-06-14 22:51:54 +05:00
/**
* Returns current track
* @returns {Track}
*/
2021-06-13 21:47:04 +05:00
nowPlaying() {
return this.current;
}
2021-06-14 22:51:54 +05:00
/**
* Connects to a voice channel
* @param {StageChannel|VoiceChannel} channel
* @returns {Promise<Queue>}
*/
2021-06-11 23:19:52 +05:00
async connect(channel: StageChannel | VoiceChannel) {
2021-06-13 13:06:19 +05:00
if (!["stage", "voice"].includes(channel?.type)) throw new TypeError(`Channel type must be voice or stage, got ${channel?.type}!`);
2021-06-14 19:44:15 +05:00
const connection = await this.player.voiceUtils.connect(channel, {
deaf: this.options.autoSelfDeaf
});
2021-06-11 23:19:52 +05:00
this.connection = connection;
2021-06-11 15:32:22 +05:00
2021-06-13 14:22:45 +05:00
// it's ok to use this here since Queue listens to the events 1 time per play and destroys the listener
2021-06-13 14:20:05 +05:00
this.connection.setMaxListeners(Infinity);
2021-06-13 10:55:20 +05:00
if (channel.type === "stage") await channel.guild.me.voice.setRequestToSpeak(true).catch(() => {});
2021-06-12 11:37:41 +05:00
2021-06-13 17:03:20 +05:00
this.connection.on("error", (err) => this.player.emit("error", this, err));
this.connection.on("debug", (msg) => this.player.emit("debug", this, msg));
2021-06-13 14:22:45 +05:00
2021-06-14 00:34:59 +05:00
this.player.emit("connectionCreate", this, this.connection);
2021-06-14 11:45:03 +05:00
2021-06-11 15:32:22 +05:00
return this;
}
2021-06-14 22:51:54 +05:00
/**
* Destroys this queue
*/
2021-06-18 00:09:02 +05:00
destroy(disconnect = this.options.leaveOnStop) {
2021-06-11 23:19:52 +05:00
this.connection.end();
2021-06-18 00:09:02 +05:00
if (disconnect) this.connection.disconnect();
2021-06-11 15:32:22 +05:00
this.player.queues.delete(this.guild.id);
}
2021-06-11 16:50:43 +05:00
2021-06-14 22:51:54 +05:00
/**
* Skips current track
* @returns {boolean}
*/
2021-06-11 23:19:52 +05:00
skip() {
if (!this.connection) return false;
2021-06-12 00:18:53 +05:00
this.connection.end();
return true;
2021-06-11 23:19:52 +05:00
}
2021-06-14 22:51:54 +05:00
/**
* Adds single track to the queue
* @param {Track} track The track to add
* @returns {void}
*/
2021-06-11 23:19:52 +05:00
addTrack(track: Track) {
2021-06-13 13:17:50 +05:00
this.tracks.push(track);
2021-06-13 13:06:19 +05:00
this.player.emit("trackAdd", this, track);
2021-06-11 23:19:52 +05:00
}
2021-06-14 22:51:54 +05:00
/**
* Adds multiple tracks to the queue
* @param {Track[]} tracks Array of tracks to add
*/
2021-06-11 23:19:52 +05:00
addTracks(tracks: Track[]) {
this.tracks.push(...tracks);
2021-06-13 13:17:50 +05:00
this.player.emit("tracksAdd", this, tracks);
2021-06-11 23:19:52 +05:00
}
2021-06-14 22:51:54 +05:00
/**
* Sets paused state
* @param {boolean} paused The paused state
* @returns {boolean}
*/
2021-06-12 00:18:53 +05:00
setPaused(paused?: boolean) {
if (!this.connection) return false;
2021-06-19 19:12:13 +05:00
return paused ? this.connection.pause(true) : this.connection.resume();
2021-06-12 00:18:53 +05:00
}
2021-06-14 22:51:54 +05:00
/**
* Sets bitrate
* @param {number|"auto"} bitrate bitrate to set
*/
2021-06-12 11:37:41 +05:00
setBitrate(bitrate: number | "auto") {
if (!this.connection?.audioResource?.encoder) return;
if (bitrate === "auto") bitrate = this.connection.channel?.bitrate ?? 64000;
this.connection.audioResource.encoder.setBitrate(bitrate);
}
2021-06-14 22:51:54 +05:00
/**
* Sets volume
* @param {number} amount The volume amount
* @returns {boolean}
*/
2021-06-13 13:06:19 +05:00
setVolume(amount: number) {
if (!this.connection) return false;
2021-06-13 14:06:01 +05:00
this.options.initialVolume = amount;
2021-06-13 13:06:19 +05:00
return this.connection.setVolume(amount);
}
2021-06-14 22:51:54 +05:00
/**
* Sets repeat mode
* @param {QueueRepeatMode} mode The repeat mode
* @returns {boolean}
*/
2021-06-13 17:43:36 +05:00
setRepeatMode(mode: QueueRepeatMode) {
if (![QueueRepeatMode.OFF, QueueRepeatMode.QUEUE, QueueRepeatMode.TRACK].includes(mode)) throw new Error(`Unknown repeat mode "${mode}"!`);
2021-06-19 19:12:13 +05:00
if (mode === this.repeatMode) return false;
2021-06-13 17:43:36 +05:00
this.repeatMode = mode;
return true;
}
2021-06-14 22:51:54 +05:00
/**
* Returns current volume amount
*/
2021-06-13 13:06:19 +05:00
get volume() {
if (!this.connection) return 100;
return this.connection.volume;
}
2021-06-17 18:22:03 +05:00
/**
* Alternative volume setter
*/
set volume(amount: number) {
this.setVolume(amount);
}
2021-06-14 22:51:54 +05:00
/**
* Plays previous track
* @returns {Promise<void>}
*/
async back() {
return await this.play(Util.last(this.previousTracks), { immediate: true });
}
2021-06-14 22:51:54 +05:00
/**
* @param {Track} [src] The track to play (if empty, uses first track from the queue)
* @param {PlayOptions={}} options The options
* @returns {Promise<void>}
*/
2021-06-13 18:09:25 +05:00
async play(src?: Track, options: PlayOptions = {}): Promise<void> {
2021-06-13 13:06:19 +05:00
if (!this.connection || !this.connection.voiceConnection) throw new Error("Voice connection is not available, use <Queue>.connect()!");
if (src && (this.playing || this.tracks.length) && !options.immediate) return this.addTrack(src);
2021-06-13 21:57:58 +05:00
const track = options.filtersUpdate && !options.immediate ? this.current : src ?? this.tracks.shift();
2021-06-11 23:19:52 +05:00
if (!track) return;
2021-06-13 21:47:04 +05:00
if (!options.filtersUpdate) {
this.previousTracks = this.previousTracks.filter((x) => x._trackID !== track._trackID);
this.previousTracks.push(track);
}
2021-06-12 18:22:45 +05:00
let stream;
2021-06-11 23:19:52 +05:00
if (["youtube", "spotify"].includes(track.raw.source)) {
2021-06-13 18:09:25 +05:00
if (track.raw.source === "spotify" && !track.raw.engine) {
track.raw.engine = await YouTube.search(`${track.author} ${track.title}`, { type: "video" })
.then((x) => x[0].url)
.catch(() => null);
}
const link = track.raw.source === "spotify" ? track.raw.engine : track.url;
if (!link) return void this.play(this.tracks.shift(), { immediate: true });
stream = ytdl(link, {
2021-06-14 19:44:15 +05:00
...this.options.ytdlOptions,
// discord-ytdl-core
2021-06-11 23:19:52 +05:00
opusEncoded: false,
2021-06-12 11:37:41 +05:00
fmt: "s16le",
encoderArgs: options.encoderArgs ?? [],
seek: options.seek
2021-06-17 23:44:37 +05:00
}).on("error", (err) => this.player.emit("error", this, err));
2021-06-11 23:19:52 +05:00
} else {
2021-06-18 00:09:02 +05:00
stream = ytdl
.arbitraryStream(
track.raw.source === "soundcloud" ? await track.raw.engine.downloadProgressive() : typeof track.raw.engine === "function" ? await track.raw.engine() : track.raw.engine,
{
opusEncoded: false,
fmt: "s16le",
encoderArgs: options.encoderArgs ?? [],
seek: options.seek
}
)
.on("error", (err) => this.player.emit("error", this, err));
2021-06-11 23:19:52 +05:00
}
2021-06-12 18:22:45 +05:00
const resource: AudioResource<Track> = this.connection.createStream(stream, {
type: StreamType.Raw,
data: track
});
2021-06-12 00:18:53 +05:00
const dispatcher = await this.connection.playStream(resource);
2021-06-11 23:19:52 +05:00
dispatcher.setVolume(this.options.initialVolume);
2021-06-14 09:46:29 +05:00
// need to use these events here
2021-06-13 13:06:19 +05:00
dispatcher.once("start", () => {
2021-06-11 23:19:52 +05:00
this.playing = true;
2021-06-12 11:37:41 +05:00
if (!options.filtersUpdate) this.player.emit("trackStart", this, this.current);
2021-06-11 23:19:52 +05:00
});
2021-06-13 13:06:19 +05:00
dispatcher.once("finish", () => {
2021-06-11 23:19:52 +05:00
this.playing = false;
2021-06-12 11:37:41 +05:00
if (options.filtersUpdate) return;
2021-06-13 17:43:36 +05:00
if (!this.tracks.length && this.repeatMode === QueueRepeatMode.OFF) {
2021-06-18 00:09:02 +05:00
if (this.options.leaveOnEnd) this.destroy();
2021-06-11 23:19:52 +05:00
this.player.emit("queueEnd", this);
} else {
2021-06-13 17:43:36 +05:00
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));
2021-06-11 23:19:52 +05:00
const nextTrack = this.tracks.shift();
2021-06-13 13:06:19 +05:00
this.play(nextTrack, { immediate: true });
2021-06-11 23:19:52 +05:00
}
});
2021-06-11 16:50:43 +05:00
}
2021-06-11 19:57:49 +05:00
*[Symbol.iterator]() {
yield* this.tracks;
}
2021-06-11 23:19:52 +05:00
2021-06-14 22:51:54 +05:00
/**
* JSON representation of this queue
* @returns {object}
*/
2021-06-11 23:19:52 +05:00
toJSON() {
return {
guild: this.guild.id,
options: this.options,
tracks: this.tracks.map((m) => m.toJSON())
};
}
2021-06-14 22:51:54 +05:00
/**
* String representation of this queue
* @returns {string}
*/
2021-06-11 23:19:52 +05:00
toString() {
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")}`;
}
2021-06-11 15:32:22 +05:00
}
2021-06-13 23:28:37 +05:00
export { Queue };