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

743 lines
25 KiB
TypeScript
Raw Normal View History

2021-07-04 18:12:27 +05:00
import { Collection, Guild, StageChannel, VoiceChannel, Snowflake, SnowflakeUtil, GuildChannelResolvable } from "discord.js";
2021-06-11 15:32:22 +05:00
import { Player } from "../Player";
2021-06-25 12:24:53 +05:00
import { StreamDispatcher } from "../VoiceInterface/StreamDispatcher";
2021-06-11 15:32:22 +05:00
import Track from "./Track";
2021-06-24 10:20:12 +05:00
import { PlayerOptions, PlayerProgressbarOptions, PlayOptions, QueueFilters, 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-19 23:10:38 +05:00
import AudioFilters from "../utils/AudioFilters";
2021-08-07 19:25:21 +05:00
import { PlayerError, ErrorStatusCode } from "./PlayerError";
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-26 00:14:07 +05:00
public readonly id: Snowflake = SnowflakeUtil.generate();
2021-06-22 15:24:05 +05:00
private _streamTime = 0;
2021-06-13 21:47:04 +05:00
public _cooldownsTimeout = new Collection<string, NodeJS.Timeout>();
2021-06-22 15:24:05 +05:00
private _activeFilters: any[] = []; // eslint-disable-line @typescript-eslint/no-explicit-any
2021-06-22 10:33:20 +05:00
private _filtersUpdate = false;
2021-06-26 11:40:24 +05:00
#lastVolume = 0;
2021-06-24 10:24:34 +05:00
#destroyed = false;
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
2021-06-20 19:22:09 +05:00
* @param {PlayerOptions} [options={}] Player options for the queue
2021-06-14 22:51:54 +05:00
*/
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-20 19:22:09 +05:00
* @readonly
2021-06-14 22:51:54 +05:00
*/
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-20 19:22:09 +05:00
* @readonly
2021-06-14 22:51:54 +05:00
*/
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 = {};
2021-06-22 10:33:20 +05:00
/**
* 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
*/
2021-06-26 00:14:07 +05:00
/**
* The ID of this queue
* @type {Snowflake}
* @name Queue#id
*/
2021-06-11 15:32:22 +05:00
Object.assign(
this.options,
{
leaveOnEnd: true,
leaveOnStop: true,
leaveOnEmpty: true,
leaveOnEmptyCooldown: 1000,
autoSelfDeaf: true,
ytdlOptions: {
highWaterMark: 1 << 25
},
2021-07-21 10:27:40 +05:00
initialVolume: 100,
bufferingTimeout: 3000
2021-06-11 15:32:22 +05:00
} as PlayerOptions,
options
);
2021-08-13 20:44:27 +05:00
this.player.emit("debug", this, `Queue initialized:\n\n${this.player.scanDeps()}`);
2021-06-11 15:32:22 +05:00
}
2021-06-14 22:51:54 +05:00
/**
* Returns current track
2021-06-20 19:22:09 +05:00
* @type {Track}
2021-06-14 22:51:54 +05:00
*/
2021-06-11 16:50:43 +05:00
get current() {
2021-08-07 19:25:21 +05:00
if (this.#watchDestroyed()) return;
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-24 10:24:34 +05:00
/**
* If this queue is destroyed
* @type {boolean}
*/
get destroyed() {
return this.#destroyed;
}
2021-06-14 22:51:54 +05:00
/**
* Returns current track
* @returns {Track}
*/
2021-06-13 21:47:04 +05:00
nowPlaying() {
2021-08-07 19:25:21 +05:00
if (this.#watchDestroyed()) return;
2021-06-13 21:47:04 +05:00
return this.current;
}
2021-06-14 22:51:54 +05:00
/**
* Connects to a voice channel
2021-07-04 18:12:27 +05:00
* @param {GuildChannelResolvable} channel The voice/stage channel
2021-06-14 22:51:54 +05:00
* @returns {Promise<Queue>}
*/
2021-07-04 18:12:27 +05:00
async connect(channel: GuildChannelResolvable) {
2021-08-07 19:25:21 +05:00
if (this.#watchDestroyed()) return;
2021-07-04 18:12:27 +05:00
const _channel = this.guild.channels.resolve(channel) as StageChannel | VoiceChannel;
2021-08-07 19:25:21 +05:00
if (!["GUILD_STAGE_VOICE", "GUILD_VOICE"].includes(_channel?.type))
throw new PlayerError(`Channel type must be GUILD_VOICE or GUILD_STAGE_VOICE, got ${_channel?.type}!`, ErrorStatusCode.INVALID_ARG_TYPE);
2021-07-04 18:12:27 +05:00
const connection = await this.player.voiceUtils.connect(_channel, {
deaf: this.options.autoSelfDeaf,
maxTime: this.player.options.connectionTimeout || 20000
2021-06-14 19:44:15 +05:00
});
2021-06-11 23:19:52 +05:00
this.connection = connection;
2021-06-11 15:32:22 +05:00
2021-07-10 15:01:45 +05:00
if (_channel.type === "GUILD_STAGE_VOICE") {
2021-07-04 18:12:27 +05:00
await _channel.guild.me.voice.setSuppressed(false).catch(async () => {
return await _channel.guild.me.voice.setRequestToSpeak(true).catch(Util.noop);
2021-06-23 15:34:53 +05:00
});
2021-06-23 14:45:11 +05:00
}
2021-06-12 11:37:41 +05:00
this.connection.on("error", (err) => {
if (this.#watchDestroyed(false)) return;
this.player.emit("connectionError", this, err);
});
this.connection.on("debug", (msg) => {
if (this.#watchDestroyed(false)) return;
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-22 15:50:31 +05:00
this.connection.on("start", (resource) => {
if (this.#watchDestroyed(false)) return;
2021-06-22 10:33:20 +05:00
this.playing = true;
2021-06-23 15:34:53 +05:00
if (!this._filtersUpdate && resource?.metadata) this.player.emit("trackStart", this, resource?.metadata ?? this.current);
2021-06-22 10:33:20 +05:00
this._filtersUpdate = false;
});
2021-06-22 15:50:31 +05:00
this.connection.on("finish", async (resource) => {
if (this.#watchDestroyed(false)) return;
2021-06-22 10:33:20 +05:00
this.playing = false;
if (this._filtersUpdate) return;
this._streamTime = 0;
2021-06-23 15:34:53 +05:00
if (resource && resource.metadata) this.previousTracks.push(resource.metadata);
2021-06-22 10:33:20 +05:00
2021-07-26 08:44:21 +05:00
this.player.emit("trackEnd", this, resource.metadata);
2021-06-22 10:33:20 +05:00
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));
}
}
});
2021-06-11 15:32:22 +05:00
return this;
}
2021-06-14 22:51:54 +05:00
/**
* Destroys this queue
2021-06-20 19:22:09 +05:00
* @param {boolean} [disconnect=this.options.leaveOnStop] If it should leave on destroy
* @returns {void}
2021-06-14 22:51:54 +05:00
*/
2021-06-18 00:09:02 +05:00
destroy(disconnect = this.options.leaveOnStop) {
2021-08-07 19:25:21 +05:00
if (this.#watchDestroyed()) return;
2021-07-04 13:09:32 +05:00
if (this.connection) this.connection.end();
2021-07-04 19:27:56 +05:00
if (disconnect) this.connection?.disconnect();
2021-06-11 15:32:22 +05:00
this.player.queues.delete(this.guild.id);
2021-06-22 10:33:20 +05:00
this.player.voiceUtils.cache.delete(this.guild.id);
2021-06-24 10:24:34 +05:00
this.#destroyed = true;
2021-06-11 15:32:22 +05:00
}
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() {
2021-08-07 19:25:21 +05:00
if (this.#watchDestroyed()) return;
2021-06-11 23:19:52 +05:00
if (!this.connection) return false;
2021-06-22 10:33:20 +05:00
this._filtersUpdate = 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-08-07 19:25:21 +05:00
if (this.#watchDestroyed()) return;
if (!(track instanceof Track)) throw new PlayerError("invalid track", ErrorStatusCode.INVALID_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[]) {
2021-08-07 19:25:21 +05:00
if (this.#watchDestroyed()) return;
if (!tracks.every((y) => y instanceof Track)) throw new PlayerError("invalid track", ErrorStatusCode.INVALID_TRACK);
2021-06-11 23:19:52 +05:00
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) {
2021-08-07 19:25:21 +05:00
if (this.#watchDestroyed()) return;
2021-06-12 00:18:53 +05:00
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
2021-06-21 10:33:55 +05:00
* @param {number|auto} bitrate bitrate to set
2021-06-20 19:22:09 +05:00
* @returns {void}
2021-06-14 22:51:54 +05:00
*/
2021-06-12 11:37:41 +05:00
setBitrate(bitrate: number | "auto") {
2021-08-07 19:25:21 +05:00
if (this.#watchDestroyed()) return;
2021-06-12 11:37:41 +05:00
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) {
2021-08-07 19:25:21 +05:00
if (this.#watchDestroyed()) return;
2021-06-13 13:06:19 +05:00
if (!this.connection) return false;
2021-06-26 11:40:24 +05:00
this.#lastVolume = amount;
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) {
2021-08-07 19:25:21 +05:00
if (this.#watchDestroyed()) return;
if (![QueueRepeatMode.OFF, QueueRepeatMode.QUEUE, QueueRepeatMode.TRACK, QueueRepeatMode.AUTOPLAY].includes(mode))
throw new PlayerError(`Unknown repeat mode "${mode}"!`, ErrorStatusCode.UNKNOWN_REPEAT_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
/**
2021-06-20 19:22:09 +05:00
* The current volume amount
* @type {number}
2021-06-14 22:51:54 +05:00
*/
2021-06-13 13:06:19 +05:00
get volume() {
2021-08-07 19:25:21 +05:00
if (this.#watchDestroyed()) return;
2021-06-13 13:06:19 +05:00
if (!this.connection) return 100;
return this.connection.volume;
}
2021-06-17 18:22:03 +05:00
set volume(amount: number) {
this.setVolume(amount);
}
2021-06-20 19:22:09 +05:00
/**
* The stream time of this queue
* @type {number}
*/
2021-06-19 23:10:38 +05:00
get streamTime() {
2021-08-07 19:25:21 +05:00
if (this.#watchDestroyed()) return;
2021-06-19 23:10:38 +05:00
if (!this.connection) return 0;
2021-06-20 10:37:59 +05:00
const playbackTime = this._streamTime + this.connection.streamTime;
const NC = this._activeFilters.includes("nightcore") ? 1.25 : null;
const VW = this._activeFilters.includes("vaporwave") ? 0.8 : null;
if (NC && VW) return playbackTime * (NC + VW);
return NC ? playbackTime * NC : VW ? playbackTime * VW : playbackTime;
}
2021-06-24 10:20:12 +05:00
set streamTime(time: number) {
2021-08-07 19:25:21 +05:00
if (this.#watchDestroyed()) return;
2021-06-24 10:20:12 +05:00
this.seek(time);
}
2021-06-20 19:22:09 +05:00
/**
* Returns enabled filters
* @returns {AudioFilters}
*/
2021-06-20 10:37:59 +05:00
getFiltersEnabled() {
2021-08-07 19:25:21 +05:00
if (this.#watchDestroyed()) return;
2021-06-20 10:37:59 +05:00
return AudioFilters.names.filter((x) => this._activeFilters.includes(x));
2021-06-19 23:10:38 +05:00
}
2021-06-20 19:22:09 +05:00
/**
* Returns disabled filters
* @returns {AudioFilters}
*/
2021-06-20 10:37:59 +05:00
getFiltersDisabled() {
2021-08-07 19:25:21 +05:00
if (this.#watchDestroyed()) return;
2021-06-20 10:37:59 +05:00
return AudioFilters.names.filter((x) => !this._activeFilters.includes(x));
}
2021-06-20 19:22:09 +05:00
/**
* Sets filters
* @param {QueueFilters} filters Queue filters
* @returns {Promise<void>}
*/
2021-06-20 10:37:59 +05:00
async setFilters(filters?: QueueFilters) {
2021-08-07 19:25:21 +05:00
if (this.#watchDestroyed()) return;
2021-06-20 10:37:59 +05:00
if (!filters || !Object.keys(filters).length) {
// reset filters
const streamTime = this.streamTime;
this._activeFilters = [];
return await this.play(this.current, {
immediate: true,
filtersUpdate: true,
seek: streamTime,
encoderArgs: []
});
}
2021-06-19 23:10:38 +05:00
2021-06-22 15:24:05 +05:00
const _filters: any[] = []; // eslint-disable-line @typescript-eslint/no-explicit-any
2021-06-19 23:10:38 +05:00
for (const filter in filters) {
if (filters[filter as keyof QueueFilters] === true) _filters.push(filter);
}
2021-06-20 10:37:59 +05:00
if (this._activeFilters.join("") === _filters.join("")) return;
2021-06-20 14:30:39 +05:00
const newFilters = AudioFilters.create(_filters).trim();
2021-06-20 10:37:59 +05:00
const streamTime = this.streamTime;
this._activeFilters = _filters;
2021-06-19 23:10:38 +05:00
return await this.play(this.current, {
2021-06-20 00:48:48 +05:00
immediate: true,
2021-06-19 23:10:38 +05:00
filtersUpdate: true,
2021-06-20 10:37:59 +05:00
seek: streamTime,
2021-06-20 14:30:39 +05:00
encoderArgs: !_filters.length ? undefined : ["-af", newFilters]
2021-06-19 23:10:38 +05:00
});
}
2021-06-20 19:22:09 +05:00
/**
* Seeks to the given time
* @param {number} position The position
* @returns {boolean}
*/
2021-06-20 11:57:17 +05:00
async seek(position: number) {
2021-08-07 19:25:21 +05:00
if (this.#watchDestroyed()) return;
2021-06-20 11:57:17 +05:00
if (!this.playing || !this.current) return false;
if (position < 1) position = 0;
if (position >= this.current.durationMS) return this.skip();
2021-06-20 12:36:16 +05:00
2021-06-20 11:57:17 +05:00
await this.play(this.current, {
immediate: true,
filtersUpdate: true, // to stop events
seek: position
});
return true;
}
2021-06-14 22:51:54 +05:00
/**
* Plays previous track
* @returns {Promise<void>}
*/
async back() {
2021-08-07 19:25:21 +05:00
if (this.#watchDestroyed()) return;
2021-06-22 10:33:20 +05:00
const prev = this.previousTracks[this.previousTracks.length - 2]; // because last item is the current track
2021-08-07 19:25:21 +05:00
if (!prev) throw new PlayerError("Could not find previous track", ErrorStatusCode.TRACK_NOT_FOUND);
2021-06-22 10:33:20 +05:00
return await this.play(prev, { immediate: true });
}
2021-06-22 19:38:00 +05:00
/**
* Clear this queue
*/
2021-06-22 10:33:20 +05:00
clear() {
2021-08-07 19:25:21 +05:00
if (this.#watchDestroyed()) return;
2021-06-22 10:33:20 +05:00
this.tracks = [];
this.previousTracks = [];
}
2021-06-24 10:20:12 +05:00
/**
* Stops the player
* @returns {void}
*/
stop() {
2021-08-07 19:25:21 +05:00
if (this.#watchDestroyed()) return;
2021-06-24 10:20:12 +05:00
return this.destroy();
}
/**
* Shuffles this queue
* @returns {boolean}
*/
shuffle() {
2021-08-07 19:25:21 +05:00
if (this.#watchDestroyed()) return;
2021-06-24 10:20:12 +05:00
if (!this.tracks.length || this.tracks.length < 3) return false;
const currentTrack = this.tracks.shift();
for (let i = this.tracks.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[this.tracks[i], this.tracks[j]] = [this.tracks[j], this.tracks[i]];
}
this.tracks.unshift(currentTrack);
return true;
}
/**
* Removes a track from the queue
2021-06-26 00:14:07 +05:00
* @param {Track|Snowflake|number} track The track to remove
2021-06-24 10:20:12 +05:00
* @returns {Track}
*/
2021-06-26 00:14:07 +05:00
remove(track: Track | Snowflake | number) {
2021-08-07 19:25:21 +05:00
if (this.#watchDestroyed()) return;
2021-06-24 10:20:12 +05:00
let trackFound: Track = null;
if (typeof track === "number") {
trackFound = this.tracks[track];
if (trackFound) {
2021-06-26 00:14:07 +05:00
this.tracks = this.tracks.filter((t) => t.id !== trackFound.id);
2021-06-24 10:20:12 +05:00
}
} else {
2021-06-26 00:14:07 +05:00
trackFound = this.tracks.find((s) => s.id === (track instanceof Track ? track.id : track));
2021-06-24 10:20:12 +05:00
if (trackFound) {
2021-06-26 00:14:07 +05:00
this.tracks = this.tracks.filter((s) => s.id !== trackFound.id);
2021-06-24 10:20:12 +05:00
}
}
return trackFound;
}
/**
* Jumps to particular track
* @param {Track|number} track The track
* @returns {void}
*/
jump(track: Track | number): void {
2021-08-07 19:25:21 +05:00
if (this.#watchDestroyed()) return;
2021-08-23 17:31:53 +05:00
// remove the track if exists
2021-06-24 10:20:12 +05:00
const foundTrack = this.remove(track);
2021-08-07 19:25:21 +05:00
if (!foundTrack) throw new PlayerError("Track not found", ErrorStatusCode.TRACK_NOT_FOUND);
2021-08-23 17:31:53 +05:00
// since we removed the existing track from the queue,
// we now have to place that to position 1
// because we want to jump to that track
// this will skip current track and play the next one which will be the foundTrack
this.tracks.splice(1, 0, foundTrack);
2021-06-24 10:20:12 +05:00
return void this.skip();
}
2021-06-30 00:29:44 +05:00
/**
* Inserts the given track to specified index
* @param {Track} track The track to insert
* @param {number} [index=0] The index where this track should be
*/
insert(track: Track, index = 0) {
2021-08-07 19:25:21 +05:00
if (!track || !(track instanceof Track)) throw new PlayerError("track must be the instance of Track", ErrorStatusCode.INVALID_TRACK);
if (typeof index !== "number" || index < 0 || !Number.isFinite(index)) throw new PlayerError(`Invalid index "${index}"`, ErrorStatusCode.INVALID_ARG_TYPE);
2021-06-30 00:29:44 +05:00
this.tracks.splice(index, 0, track);
this.player.emit("trackAdd", this, track);
}
2021-06-24 10:20:12 +05:00
/**
* @typedef {object} PlayerTimestamp
2021-06-24 10:24:34 +05:00
* @property {string} current The current progress
* @property {string} end The total time
* @property {number} progress Progress in %
2021-06-24 10:20:12 +05:00
*/
/**
* Returns player stream timestamp
* @returns {PlayerTimestamp}
*/
getPlayerTimestamp() {
2021-08-07 19:25:21 +05:00
if (this.#watchDestroyed()) return;
const currentStreamTime = this.streamTime;
const totalTime = this.current.durationMS;
2021-06-24 10:20:12 +05:00
const currentTimecode = Util.buildTimeCode(Util.parseMS(currentStreamTime));
const endTimecode = Util.buildTimeCode(Util.parseMS(totalTime));
return {
current: currentTimecode,
end: endTimecode,
progress: Math.round((currentStreamTime / totalTime) * 100)
};
}
/**
* Creates progress bar string
* @param {PlayerProgressbarOptions} options The progress bar options
* @returns {string}
*/
createProgressBar(options: PlayerProgressbarOptions = { timecodes: true }) {
2021-08-07 19:25:21 +05:00
if (this.#watchDestroyed()) return;
2021-06-24 10:20:12 +05:00
const length = typeof options.length === "number" ? (options.length <= 0 || options.length === Infinity ? 15 : options.length) : 15;
const index = Math.round((this.streamTime / this.current.durationMS) * length);
2021-06-24 10:20:12 +05:00
const indicator = typeof options.indicator === "string" && options.indicator.length > 0 ? options.indicator : "🔘";
const line = typeof options.line === "string" && options.line.length > 0 ? options.line : "▬";
if (index >= 1 && index <= length) {
const bar = line.repeat(length - 1).split("");
bar.splice(index, 0, indicator);
if (options.timecodes) {
const timestamp = this.getPlayerTimestamp();
2021-06-24 10:20:12 +05:00
return `${timestamp.current}${bar.join("")}${timestamp.end}`;
} else {
return `${bar.join("")}`;
}
} else {
if (options.timecodes) {
const timestamp = this.getPlayerTimestamp();
2021-06-24 10:20:12 +05:00
return `${timestamp.current}${indicator}${line.repeat(length - 1)}${timestamp.end}`;
} else {
return `${indicator}${line.repeat(length - 1)}`;
}
}
}
/**
* Total duration
* @type {Number}
*/
get totalTime(): number {
2021-08-07 19:25:21 +05:00
if (this.#watchDestroyed()) return;
2021-06-24 10:20:12 +05:00
return this.tracks.length > 0 ? this.tracks.map((t) => t.durationMS).reduce((p, c) => p + c) : 0;
}
2021-06-14 22:51:54 +05:00
/**
2021-06-20 20:00:19 +05:00
* Play stream in a voice/stage channel
2021-06-20 19:22:09 +05:00
* @param {Track} [src] The track to play (if empty, uses first track from the queue)
* @param {PlayOptions} [options={}] The options
2021-06-14 22:51:54 +05:00
* @returns {Promise<void>}
*/
2021-06-13 18:09:25 +05:00
async play(src?: Track, options: PlayOptions = {}): Promise<void> {
if (this.#watchDestroyed(false)) return;
2021-08-07 19:25:21 +05:00
if (!this.connection || !this.connection.voiceConnection) throw new PlayerError("Voice connection is not available, use <Queue>.connect()!", ErrorStatusCode.NO_CONNECTION);
2021-06-13 13:06:19 +05:00
if (src && (this.playing || this.tracks.length) && !options.immediate) return this.addTrack(src);
2021-06-19 23:10:38 +05:00
const track = options.filtersUpdate && !options.immediate ? src || 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
this.player.emit("debug", this, "Received play request");
2021-06-13 21:47:04 +05:00
if (!options.filtersUpdate) {
2021-06-26 00:14:07 +05:00
this.previousTracks = this.previousTracks.filter((x) => x.id !== track.id);
2021-06-23 14:45:11 +05:00
this.previousTracks.push(track);
2021-06-13 21:47:04 +05:00
}
2021-08-13 20:44:27 +05:00
// TODO: remove discord-ytdl-core
2021-06-22 10:33:20 +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",
2021-06-20 10:37:59 +05:00
encoderArgs: options.encoderArgs ?? this._activeFilters.length ? ["-af", AudioFilters.create(this._activeFilters)] : [],
seek: options.seek ? options.seek / 1000 : 0
}).on("error", (err) => {
return err.message.toLowerCase().includes("premature close") ? null : 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",
2021-06-20 10:37:59 +05:00
encoderArgs: options.encoderArgs ?? this._activeFilters.length ? ["-af", AudioFilters.create(this._activeFilters)] : [],
seek: options.seek ? options.seek / 1000 : 0
2021-06-18 00:09:02 +05:00
}
)
.on("error", (err) => {
return err.message.toLowerCase().includes("premature close") ? null : 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-19 23:10:38 +05:00
if (options.seek) this._streamTime = options.seek;
2021-06-22 10:33:20 +05:00
this._filtersUpdate = options.filtersUpdate;
2021-08-23 17:25:31 +05:00
this.setVolume(this.options.initialVolume);
2021-06-19 23:10:38 +05:00
2021-07-21 10:27:40 +05:00
setTimeout(() => {
2021-08-23 17:25:31 +05:00
this.connection.playStream(resource);
2021-08-07 23:35:51 +05:00
}, this.#getBufferingTimeout()).unref();
2021-06-22 10:33:20 +05:00
}
2021-06-11 23:19:52 +05:00
2021-06-22 19:38:00 +05:00
/**
* Private method to handle autoplay
* @param {Track} track The source track to find its similar track for autoplay
* @returns {Promise<void>}
2021-06-25 12:24:53 +05:00
* @private
2021-06-22 19:38:00 +05:00
*/
2021-06-22 10:33:20 +05:00
private async _handleAutoplay(track: Track): Promise<void> {
2021-08-07 19:25:21 +05:00
if (this.#watchDestroyed()) return;
2021-06-22 10:33:20 +05:00
if (!track || ![track.source, track.raw?.source].includes("youtube")) {
if (this.options.leaveOnEnd) this.destroy();
return void this.player.emit("queueEnd", this);
}
2021-06-23 14:45:11 +05:00
const info = await YouTube.getVideo(track.url)
.then((x) => x.videos[0])
.catch(Util.noop);
2021-06-22 10:33:20 +05:00
if (!info) {
if (this.options.leaveOnEnd) this.destroy();
return void this.player.emit("queueEnd", this);
}
2021-06-20 12:36:05 +05:00
2021-06-22 10:33:20 +05:00
const nextTrack = new Track(this.player, {
title: info.title,
url: `https://www.youtube.com/watch?v=${info.id}`,
2021-06-23 14:45:11 +05:00
duration: info.durationFormatted ? Util.buildTimeCode(Util.parseMS(info.duration * 1000)) : "0:00",
2021-06-22 10:33:20 +05:00
description: "",
2021-06-23 14:45:11 +05:00
thumbnail: typeof info.thumbnail === "string" ? info.thumbnail : info.thumbnail.url,
views: info.views,
author: info.channel.name,
2021-06-22 10:33:20 +05:00
requestedBy: track.requestedBy,
source: "youtube"
2021-06-11 23:19:52 +05:00
});
2021-06-22 10:33:20 +05:00
this.play(nextTrack, { immediate: true });
2021-06-11 16:50:43 +05:00
}
2021-06-11 19:57:49 +05:00
*[Symbol.iterator]() {
2021-08-07 19:25:21 +05:00
if (this.#watchDestroyed()) return;
2021-06-11 19:57:49 +05:00
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() {
2021-08-07 19:25:21 +05:00
if (this.#watchDestroyed()) return;
2021-06-11 23:19:52 +05:00
return {
2021-06-26 00:14:07 +05:00
id: this.id,
2021-06-11 23:19:52 +05:00
guild: this.guild.id,
2021-06-20 12:36:05 +05:00
voiceChannel: this.connection?.channel?.id,
2021-06-11 23:19:52 +05:00
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() {
2021-08-07 19:25:21 +05:00
if (this.#watchDestroyed()) return;
2021-06-11 23:19:52 +05:00
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-22 10:33:20 +05:00
#watchDestroyed(emit = true) {
2021-08-07 19:25:21 +05:00
if (this.#destroyed) {
if (emit) this.player.emit("error", this, new PlayerError("Cannot use destroyed queue", ErrorStatusCode.DESTROYED_QUEUE));
2021-08-07 19:25:21 +05:00
return true;
}
return false;
2021-06-22 10:33:20 +05:00
}
2021-07-21 10:27:40 +05:00
#getBufferingTimeout() {
const timeout = this.options.bufferingTimeout;
if (isNaN(timeout) || timeout < 0 || !Number.isFinite(timeout)) return 1000;
return timeout;
}
2021-06-11 15:32:22 +05:00
}
2021-06-13 23:28:37 +05:00
export { Queue };