feat: create PlayerError

This commit is contained in:
DevAndromeda 2021-08-07 20:10:21 +05:45
parent 8e00bca2df
commit 59b1c60440
No known key found for this signature in database
GPG key ID: FA40E3EC5CB6DCD6
5 changed files with 106 additions and 49 deletions

View file

@ -8,6 +8,7 @@ import { QueryResolver } from "./utils/QueryResolver";
import YouTube from "youtube-sr";
import { Util } from "./utils/Util";
import Spotify from "spotify-url-info";
import { PlayerError, ErrorStatusCode } from "./Structures/PlayerError";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import { Client as SoundCloud } from "soundcloud-scraper";
@ -128,7 +129,7 @@ class Player extends EventEmitter<PlayerEvents> {
*/
createQueue<T = unknown>(guild: GuildResolvable, queueInitOptions: PlayerOptions & { metadata?: T } = {}): Queue<T> {
guild = this.client.guilds.resolve(guild);
if (!guild) throw new Error("Unknown Guild");
if (!guild) throw new PlayerError("Unknown Guild", ErrorStatusCode.UNKNOWN_GUILD);
if (this.queues.has(guild.id)) return this.queues.get(guild.id) as Queue<T>;
const _meta = queueInitOptions.metadata;
@ -148,7 +149,7 @@ class Player extends EventEmitter<PlayerEvents> {
*/
getQueue<T = unknown>(guild: GuildResolvable) {
guild = this.client.guilds.resolve(guild);
if (!guild) throw new Error("Unknown Guild");
if (!guild) throw new PlayerError("Unknown Guild", ErrorStatusCode.UNKNOWN_GUILD);
return this.queues.get(guild.id) as Queue<T>;
}
@ -159,7 +160,7 @@ class Player extends EventEmitter<PlayerEvents> {
*/
deleteQueue<T = unknown>(guild: GuildResolvable) {
guild = this.client.guilds.resolve(guild);
if (!guild) throw new Error("Unknown Guild");
if (!guild) throw new PlayerError("Unknown Guild", ErrorStatusCode.UNKNOWN_GUILD);
const prev = this.getQueue<T>(guild);
try {
@ -183,7 +184,7 @@ class Player extends EventEmitter<PlayerEvents> {
*/
async search(query: string | Track, options: SearchOptions) {
if (query instanceof Track) return { playlist: null, tracks: [query] };
if (!options) throw new Error("DiscordPlayer#search needs search options!");
if (!options) throw new PlayerError("DiscordPlayer#search needs search options!", ErrorStatusCode.INVALID_ARG_TYPE);
options.requestedBy = this.client.users.resolve(options.requestedBy);
if (!("searchEngine" in options)) options.searchEngine = QueryType.AUTO;
@ -451,7 +452,7 @@ class Player extends EventEmitter<PlayerEvents> {
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
use(extractorName: string, extractor: ExtractorModel | any, force = false): ExtractorModel {
if (!extractorName) throw new Error("Cannot use unknown extractor!");
if (!extractorName) throw new PlayerError("Cannot use unknown extractor!", ErrorStatusCode.UNKNOWN_EXTRACTOR);
if (this.extractors.has(extractorName) && !force) return this.extractors.get(extractorName);
if (extractor instanceof ExtractorModel) {
this.extractors.set(extractorName, extractor);
@ -459,7 +460,7 @@ class Player extends EventEmitter<PlayerEvents> {
}
for (const method of ["validate", "getInfo"]) {
if (typeof extractor[method] !== "function") throw new Error("Invalid extractor data!");
if (typeof extractor[method] !== "function") throw new PlayerError("Invalid extractor data!", ErrorStatusCode.INVALID_EXTRACTOR);
}
const model = new ExtractorModel(extractorName, extractor);
@ -474,7 +475,7 @@ class Player extends EventEmitter<PlayerEvents> {
* @returns {ExtractorModel}
*/
unuse(extractorName: string) {
if (!this.extractors.has(extractorName)) throw new Error(`Cannot find extractor "${extractorName}"`);
if (!this.extractors.has(extractorName)) throw new PlayerError(`Cannot find extractor "${extractorName}"`, ErrorStatusCode.UNKNOWN_EXTRACTOR);
const prev = this.extractors.get(extractorName);
this.extractors.delete(extractorName);
return prev;

View file

@ -0,0 +1,48 @@
export enum ErrorStatusCode {
STREAM_ERROR = "StreamError",
AUDIO_PLAYER_ERROR = "AudioPlayerError",
PLAYER_ERROR = "PlayerError",
NO_AUDIO_RESOURCE = "NoAudioResource",
UNKNOWN_GUILD = "UnknownGuild",
INVALID_ARG_TYPE = "InvalidArgType",
UNKNOWN_EXTRACTOR = "UnknownExtractor",
INVALID_EXTRACTOR = "InvalidExtractor",
INVALID_CHANNEL_TYPE = "InvalidChannelType",
INVALID_TRACK = "InvalidTrack",
UNKNOWN_REPEAT_MODE = "UnknownRepeatMode",
TRACK_NOT_FOUND = "TrackNotFound",
NO_CONNECTION = "NoConnection",
DESTROYED_QUEUE = "DestroyedQueue"
}
export class PlayerError extends Error {
message: string;
statusCode: ErrorStatusCode;
createdAt = new Date();
constructor(message: string, code: ErrorStatusCode = ErrorStatusCode.PLAYER_ERROR) {
super();
this.message = `[${code}] ${message}`;
this.statusCode = code;
this.name = code;
Error.captureStackTrace(this);
}
get createdTimestamp() {
return this.createdAt.getTime();
}
valueOf() {
return this.statusCode;
}
toJSON() {
return { stack: this.stack, code: this.statusCode, created: this.createdTimestamp };
}
toString() {
return this.stack;
}
}

View file

@ -8,6 +8,7 @@ import { AudioResource, StreamType } from "@discordjs/voice";
import { Util } from "../utils/Util";
import YouTube from "youtube-sr";
import AudioFilters from "../utils/AudioFilters";
import { PlayerError, ErrorStatusCode } from "./PlayerError";
class Queue<T = unknown> {
public readonly guild: Guild;
@ -113,7 +114,7 @@ class Queue<T = unknown> {
* @type {Track}
*/
get current() {
this.#watchDestroyed();
if (this.#watchDestroyed()) return;
return this.connection.audioResource?.metadata ?? this.tracks[0];
}
@ -130,7 +131,7 @@ class Queue<T = unknown> {
* @returns {Track}
*/
nowPlaying() {
this.#watchDestroyed();
if (this.#watchDestroyed()) return;
return this.current;
}
@ -140,9 +141,10 @@ class Queue<T = unknown> {
* @returns {Promise<Queue>}
*/
async connect(channel: GuildChannelResolvable) {
this.#watchDestroyed();
if (this.#watchDestroyed()) return;
const _channel = this.guild.channels.resolve(channel) as StageChannel | VoiceChannel;
if (!["GUILD_STAGE_VOICE", "GUILD_VOICE"].includes(_channel?.type)) throw new TypeError(`Channel type must be GUILD_VOICE or GUILD_STAGE_VOICE, got ${_channel?.type}!`);
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);
const connection = await this.player.voiceUtils.connect(_channel, {
deaf: this.options.autoSelfDeaf
});
@ -198,7 +200,7 @@ class Queue<T = unknown> {
* @returns {void}
*/
destroy(disconnect = this.options.leaveOnStop) {
this.#watchDestroyed();
if (this.#watchDestroyed()) return;
if (this.connection) this.connection.end();
if (disconnect) this.connection?.disconnect();
this.player.queues.delete(this.guild.id);
@ -211,7 +213,7 @@ class Queue<T = unknown> {
* @returns {boolean}
*/
skip() {
this.#watchDestroyed();
if (this.#watchDestroyed()) return;
if (!this.connection) return false;
this._filtersUpdate = false;
this.connection.end();
@ -224,8 +226,8 @@ class Queue<T = unknown> {
* @returns {void}
*/
addTrack(track: Track) {
this.#watchDestroyed();
if (!(track instanceof Track)) throw new Error("invalid track");
if (this.#watchDestroyed()) return;
if (!(track instanceof Track)) throw new PlayerError("invalid track", ErrorStatusCode.INVALID_TRACK);
this.tracks.push(track);
this.player.emit("trackAdd", this, track);
}
@ -235,8 +237,8 @@ class Queue<T = unknown> {
* @param {Track[]} tracks Array of tracks to add
*/
addTracks(tracks: Track[]) {
this.#watchDestroyed();
if (!tracks.every((y) => y instanceof Track)) throw new Error("invalid track");
if (this.#watchDestroyed()) return;
if (!tracks.every((y) => y instanceof Track)) throw new PlayerError("invalid track", ErrorStatusCode.INVALID_TRACK);
this.tracks.push(...tracks);
this.player.emit("tracksAdd", this, tracks);
}
@ -247,7 +249,7 @@ class Queue<T = unknown> {
* @returns {boolean}
*/
setPaused(paused?: boolean) {
this.#watchDestroyed();
if (this.#watchDestroyed()) return;
if (!this.connection) return false;
return paused ? this.connection.pause(true) : this.connection.resume();
}
@ -258,7 +260,7 @@ class Queue<T = unknown> {
* @returns {void}
*/
setBitrate(bitrate: number | "auto") {
this.#watchDestroyed();
if (this.#watchDestroyed()) return;
if (!this.connection?.audioResource?.encoder) return;
if (bitrate === "auto") bitrate = this.connection.channel?.bitrate ?? 64000;
this.connection.audioResource.encoder.setBitrate(bitrate);
@ -270,7 +272,7 @@ class Queue<T = unknown> {
* @returns {boolean}
*/
setVolume(amount: number) {
this.#watchDestroyed();
if (this.#watchDestroyed()) return;
if (!this.connection) return false;
this.#lastVolume = amount;
this.options.initialVolume = amount;
@ -282,8 +284,9 @@ class Queue<T = unknown> {
* @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 (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);
if (mode === this.repeatMode) return false;
this.repeatMode = mode;
return true;
@ -294,7 +297,7 @@ class Queue<T = unknown> {
* @type {number}
*/
get volume() {
this.#watchDestroyed();
if (this.#watchDestroyed()) return;
if (!this.connection) return 100;
return this.connection.volume;
}
@ -326,7 +329,7 @@ class Queue<T = unknown> {
* @type {number}
*/
get streamTime() {
this.#watchDestroyed();
if (this.#watchDestroyed()) return;
if (!this.connection) return 0;
const playbackTime = this._streamTime + this.connection.streamTime;
const NC = this._activeFilters.includes("nightcore") ? 1.25 : null;
@ -337,7 +340,7 @@ class Queue<T = unknown> {
}
set streamTime(time: number) {
this.#watchDestroyed();
if (this.#watchDestroyed()) return;
this.seek(time);
}
@ -346,7 +349,7 @@ class Queue<T = unknown> {
* @returns {AudioFilters}
*/
getFiltersEnabled() {
this.#watchDestroyed();
if (this.#watchDestroyed()) return;
return AudioFilters.names.filter((x) => this._activeFilters.includes(x));
}
@ -355,7 +358,7 @@ class Queue<T = unknown> {
* @returns {AudioFilters}
*/
getFiltersDisabled() {
this.#watchDestroyed();
if (this.#watchDestroyed()) return;
return AudioFilters.names.filter((x) => !this._activeFilters.includes(x));
}
@ -365,7 +368,7 @@ class Queue<T = unknown> {
* @returns {Promise<void>}
*/
async setFilters(filters?: QueueFilters) {
this.#watchDestroyed();
if (this.#watchDestroyed()) return;
if (!filters || !Object.keys(filters).length) {
// reset filters
const streamTime = this.streamTime;
@ -404,7 +407,7 @@ class Queue<T = unknown> {
* @returns {boolean}
*/
async seek(position: number) {
this.#watchDestroyed();
if (this.#watchDestroyed()) return;
if (!this.playing || !this.current) return false;
if (position < 1) position = 0;
if (position >= this.current.durationMS) return this.skip();
@ -423,9 +426,9 @@ class Queue<T = unknown> {
* @returns {Promise<void>}
*/
async back() {
this.#watchDestroyed();
if (this.#watchDestroyed()) return;
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");
if (!prev) throw new PlayerError("Could not find previous track", ErrorStatusCode.TRACK_NOT_FOUND);
return await this.play(prev, { immediate: true });
}
@ -434,7 +437,7 @@ class Queue<T = unknown> {
* Clear this queue
*/
clear() {
this.#watchDestroyed();
if (this.#watchDestroyed()) return;
this.tracks = [];
this.previousTracks = [];
}
@ -444,7 +447,7 @@ class Queue<T = unknown> {
* @returns {void}
*/
stop() {
this.#watchDestroyed();
if (this.#watchDestroyed()) return;
return this.destroy();
}
@ -453,7 +456,7 @@ class Queue<T = unknown> {
* @returns {boolean}
*/
shuffle() {
this.#watchDestroyed();
if (this.#watchDestroyed()) return;
if (!this.tracks.length || this.tracks.length < 3) return false;
const currentTrack = this.tracks.shift();
@ -473,7 +476,7 @@ class Queue<T = unknown> {
* @returns {Track}
*/
remove(track: Track | Snowflake | number) {
this.#watchDestroyed();
if (this.#watchDestroyed()) return;
let trackFound: Track = null;
if (typeof track === "number") {
trackFound = this.tracks[track];
@ -496,9 +499,9 @@ class Queue<T = unknown> {
* @returns {void}
*/
jump(track: Track | number): void {
this.#watchDestroyed();
if (this.#watchDestroyed()) return;
const foundTrack = this.remove(track);
if (!foundTrack) throw new Error("Track not found");
if (!foundTrack) throw new PlayerError("Track not found", ErrorStatusCode.TRACK_NOT_FOUND);
this.tracks.splice(1, 0, foundTrack);
return void this.skip();
@ -510,8 +513,8 @@ class Queue<T = unknown> {
* @param {number} [index=0] The index where this track should be
*/
insert(track: Track, index = 0) {
if (!track || !(track instanceof Track)) throw new TypeError("track must be the instance of Track");
if (typeof index !== "number" || index < 0 || !Number.isFinite(index)) throw new Error(`Invalid index "${index}"`);
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);
this.tracks.splice(index, 0, track);
@ -530,7 +533,7 @@ class Queue<T = unknown> {
* @returns {PlayerTimestamp}
*/
getPlayerTimestamp() {
this.#watchDestroyed();
if (this.#watchDestroyed()) return;
const currentStreamTime = this.streamTime;
const totalTime = this.current.durationMS;
@ -550,7 +553,7 @@ class Queue<T = unknown> {
* @returns {string}
*/
createProgressBar(options: PlayerProgressbarOptions = { timecodes: true }) {
this.#watchDestroyed();
if (this.#watchDestroyed()) return;
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);
@ -581,7 +584,7 @@ class Queue<T = unknown> {
* @type {Number}
*/
get totalTime(): number {
this.#watchDestroyed();
if (this.#watchDestroyed()) return;
return this.tracks.length > 0 ? this.tracks.map((t) => t.durationMS).reduce((p, c) => p + c) : 0;
}
@ -593,7 +596,7 @@ class Queue<T = unknown> {
*/
async play(src?: Track, options: PlayOptions = {}): Promise<void> {
if (!this.destroyed) this.#watchDestroyed();
if (!this.connection || !this.connection.voiceConnection) throw new Error("Voice connection is not available, use <Queue>.connect()!");
if (!this.connection || !this.connection.voiceConnection) throw new PlayerError("Voice connection is not available, use <Queue>.connect()!", ErrorStatusCode.NO_CONNECTION);
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();
if (!track) return;
@ -663,7 +666,7 @@ class Queue<T = unknown> {
* @private
*/
private async _handleAutoplay(track: Track): Promise<void> {
this.#watchDestroyed();
if (this.#watchDestroyed()) return;
if (!track || ![track.source, track.raw?.source].includes("youtube")) {
if (this.options.leaveOnEnd) this.destroy();
return void this.player.emit("queueEnd", this);
@ -692,7 +695,7 @@ class Queue<T = unknown> {
}
*[Symbol.iterator]() {
this.#watchDestroyed();
if (this.#watchDestroyed()) return;
yield* this.tracks;
}
@ -701,7 +704,7 @@ class Queue<T = unknown> {
* @returns {object}
*/
toJSON() {
this.#watchDestroyed();
if (this.#watchDestroyed()) return;
return {
id: this.id,
guild: this.guild.id,
@ -716,13 +719,16 @@ class Queue<T = unknown> {
* @returns {string}
*/
toString() {
this.#watchDestroyed();
if (this.#watchDestroyed()) return;
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");
if (this.#destroyed) {
this.player.emit("error", this, new PlayerError("Cannot use destroyed queue", ErrorStatusCode.DESTROYED_QUEUE));
return true;
}
}
#getBufferingTimeout() {

View file

@ -16,6 +16,7 @@ import { Duplex, Readable } from "stream";
import { TypedEmitter as EventEmitter } from "tiny-typed-emitter";
import Track from "../Structures/Track";
import { Util } from "../utils/Util";
import { PlayerError, ErrorStatusCode } from "../Structures/PlayerError";
export interface VoiceEvents {
/* eslint-disable @typescript-eslint/no-explicit-any */
@ -182,7 +183,7 @@ class StreamDispatcher extends EventEmitter<VoiceEvents> {
* @returns {Promise<StreamDispatcher>}
*/
async playStream(resource: AudioResource<Track> = this.audioResource) {
if (!resource) throw new Error("Audio resource is not available!");
if (!resource) throw new PlayerError("Audio resource is not available!", ErrorStatusCode.NO_AUDIO_RESOURCE);
if (!this.audioResource) this.audioResource = resource;
if (this.voiceConnection.state.status !== VoiceConnectionStatus.Ready) await entersState(this.voiceConnection, VoiceConnectionStatus.Ready, 20000);
this.audioPlayer.play(resource);

View file

@ -2,6 +2,7 @@ export { AudioFilters } from "./utils/AudioFilters";
export { ExtractorModel } from "./Structures/ExtractorModel";
export { Playlist } from "./Structures/Playlist";
export { Player } from "./Player";
export { PlayerError, ErrorStatusCode } from "./Structures/PlayerError";
export { QueryResolver } from "./utils/QueryResolver";
export { Queue } from "./Structures/Queue";
export { Track } from "./Structures/Track";