diff --git a/.prettierrc b/.prettierrc index 46a28f7..b9ac5c9 100644 --- a/.prettierrc +++ b/.prettierrc @@ -3,5 +3,6 @@ "trailingComma": "none", "singleQuote": false, "tabWidth": 4, + "useTabs": true, "semi": true } \ No newline at end of file diff --git a/src/Player.ts b/src/Player.ts index 47b7704..9a105f8 100644 --- a/src/Player.ts +++ b/src/Player.ts @@ -14,624 +14,624 @@ import { ExtractorModel } from "./Structures/ExtractorModel"; import { generateDependencyReport } from "@discordjs/voice"; class Player extends EventEmitter { - public readonly client: Client; - public readonly options: PlayerInitOptions = { - autoRegisterExtractor: true, - connectionTimeout: 20000 - }; - public readonly queues = new Collection(); - public readonly voiceUtils = new VoiceUtils(); - public readonly extractors = new Collection(); - public requiredEvents = ["error", "connectionError"] as string[]; + public readonly client: Client; + public readonly options: PlayerInitOptions = { + autoRegisterExtractor: true, + connectionTimeout: 20000 + }; + public readonly queues = new Collection(); + public readonly voiceUtils = new VoiceUtils(); + public readonly extractors = new Collection(); + public requiredEvents = ["error", "connectionError"] as string[]; - /** - * Creates new Discord Player - * @param {Client} client The Discord Client - * @param {PlayerInitOptions} [options] The player init options - */ - constructor(client: Client, options: PlayerInitOptions = {}) { - super(); + /** + * Creates new Discord Player + * @param {Client} client The Discord Client + * @param {PlayerInitOptions} [options] The player init options + */ + constructor(client: Client, options: PlayerInitOptions = {}) { + super(); - /** - * The discord.js client - * @type {Client} - */ - this.client = client; + /** + * The discord.js client + * @type {Client} + */ + this.client = client; - if (this.client?.options?.intents && !new IntentsBitField(this.client?.options?.intents).has(IntentsBitField.Flags.GuildVoiceStates)) { - throw new PlayerError('client is missing "GuildVoiceStates" intent'); - } + if (this.client?.options?.intents && !new IntentsBitField(this.client?.options?.intents).has(IntentsBitField.Flags.GuildVoiceStates)) { + throw new PlayerError('client is missing "GuildVoiceStates" intent'); + } - /** - * The extractors collection - * @type {ExtractorModel} - */ - this.options = Object.assign(this.options, options); + /** + * The extractors collection + * @type {ExtractorModel} + */ + this.options = Object.assign(this.options, options); - this.client.on("voiceStateUpdate", this._handleVoiceState.bind(this)); + this.client.on("voiceStateUpdate", this._handleVoiceState.bind(this)); - if (this.options?.autoRegisterExtractor) { - let nv: any; // eslint-disable-line @typescript-eslint/no-explicit-any + if (this.options?.autoRegisterExtractor) { + let nv: any; // eslint-disable-line @typescript-eslint/no-explicit-any - if ((nv = Util.require("@discord-player/extractor"))) { - ["Attachment", "Facebook", "Reverbnation", "Vimeo"].forEach((ext) => void this.use(ext, nv[ext])); - } - } - } + if ((nv = Util.require("@discord-player/extractor"))) { + ["Attachment", "Facebook", "Reverbnation", "Vimeo"].forEach((ext) => void this.use(ext, nv[ext])); + } + } + } - /** - * Handles voice state update - * @param {VoiceState} oldState The old voice state - * @param {VoiceState} newState The new voice state - * @returns {void} - * @private - */ - private _handleVoiceState(oldState: VoiceState, newState: VoiceState): void { - const queue = this.getQueue(oldState.guild.id); - if (!queue || !queue.connection) return; + /** + * Handles voice state update + * @param {VoiceState} oldState The old voice state + * @param {VoiceState} newState The new voice state + * @returns {void} + * @private + */ + private _handleVoiceState(oldState: VoiceState, newState: VoiceState): void { + const queue = this.getQueue(oldState.guild.id); + if (!queue || !queue.connection) return; - if (oldState.channelId && !newState.channelId && newState.member.id === newState.guild.members.me.id) { - try { - queue.destroy(); - } catch { - /* noop */ - } - return void this.emit("botDisconnect", queue); - } + if (oldState.channelId && !newState.channelId && newState.member.id === newState.guild.members.me.id) { + try { + queue.destroy(); + } catch { + /* noop */ + } + return void this.emit("botDisconnect", queue); + } - if (!oldState.channelId && newState.channelId && newState.member.id === newState.guild.members.me.id) { - if (!oldState.serverMute && newState.serverMute) { - // state.serverMute can be null - queue.setPaused(!!newState.serverMute); - } else if (!oldState.suppress && newState.suppress) { - // state.suppress can be null - queue.setPaused(!!newState.suppress); - if (newState.suppress) { - newState.guild.members.me.voice.setRequestToSpeak(true).catch(Util.noop); - } - } - } + if (!oldState.channelId && newState.channelId && newState.member.id === newState.guild.members.me.id) { + if (!oldState.serverMute && newState.serverMute) { + // state.serverMute can be null + queue.setPaused(!!newState.serverMute); + } else if (!oldState.suppress && newState.suppress) { + // state.suppress can be null + queue.setPaused(!!newState.suppress); + if (newState.suppress) { + newState.guild.members.me.voice.setRequestToSpeak(true).catch(Util.noop); + } + } + } - if (oldState.channelId === newState.channelId && newState.member.id === newState.guild.members.me.id) { - if (!oldState.serverMute && newState.serverMute) { - // state.serverMute can be null - queue.setPaused(!!newState.serverMute); - } else if (!oldState.suppress && newState.suppress) { - // state.suppress can be null - queue.setPaused(!!newState.suppress); - if (newState.suppress) { - newState.guild.members.me.voice.setRequestToSpeak(true).catch(Util.noop); - } - } - } + if (oldState.channelId === newState.channelId && newState.member.id === newState.guild.members.me.id) { + if (!oldState.serverMute && newState.serverMute) { + // state.serverMute can be null + queue.setPaused(!!newState.serverMute); + } else if (!oldState.suppress && newState.suppress) { + // state.suppress can be null + queue.setPaused(!!newState.suppress); + if (newState.suppress) { + newState.guild.members.me.voice.setRequestToSpeak(true).catch(Util.noop); + } + } + } - if (queue.connection && !newState.channelId && oldState.channelId === queue.connection.channel.id) { - 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; - if (queue.options.leaveOnEmpty) queue.destroy(true); - this.emit("channelEmpty", queue); - }, queue.options.leaveOnEmptyCooldown || 0).unref(); - queue._cooldownsTimeout.set(`empty_${oldState.guild.id}`, timeout); - } + if (queue.connection && !newState.channelId && oldState.channelId === queue.connection.channel.id) { + 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; + if (queue.options.leaveOnEmpty) queue.destroy(true); + this.emit("channelEmpty", queue); + }, queue.options.leaveOnEmptyCooldown || 0).unref(); + queue._cooldownsTimeout.set(`empty_${oldState.guild.id}`, timeout); + } - if (queue.connection && newState.channelId && newState.channelId === queue.connection.channel.id) { - 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}`); - } - } + if (queue.connection && newState.channelId && newState.channelId === queue.connection.channel.id) { + 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}`); + } + } - if (oldState.channelId && newState.channelId && oldState.channelId !== newState.channelId && newState.member.id === newState.guild.members.me.id) { - if (queue.connection && newState.member.id === newState.guild.members.me.id) queue.connection.channel = newState.channel; - 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 { - const timeout = setTimeout(() => { - if (queue.connection && !Util.isVoiceEmpty(queue.connection.channel)) return; - if (!this.queues.has(queue.guild.id)) return; - if (queue.options.leaveOnEmpty) queue.destroy(true); - this.emit("channelEmpty", queue); - }, queue.options.leaveOnEmptyCooldown || 0).unref(); - queue._cooldownsTimeout.set(`empty_${oldState.guild.id}`, timeout); - } - } + if (oldState.channelId && newState.channelId && oldState.channelId !== newState.channelId && newState.member.id === newState.guild.members.me.id) { + if (queue.connection && newState.member.id === newState.guild.members.me.id) queue.connection.channel = newState.channel; + 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 { + const timeout = setTimeout(() => { + if (queue.connection && !Util.isVoiceEmpty(queue.connection.channel)) return; + if (!this.queues.has(queue.guild.id)) return; + if (queue.options.leaveOnEmpty) queue.destroy(true); + this.emit("channelEmpty", queue); + }, queue.options.leaveOnEmptyCooldown || 0).unref(); + queue._cooldownsTimeout.set(`empty_${oldState.guild.id}`, timeout); + } + } - if (queue.connection && oldState.channelId && newState.channelId && oldState.channelId !== newState.channelId && newState.member.id != newState.guild.members.me.id) { - if (newState.channelId !== queue.connection.channel.id) { - if (!Util.isVoiceEmpty(queue.connection.channel)) return; - if (queue._cooldownsTimeout.has(`empty_${oldState.guild.id}`)) return; - const timeout = setTimeout(() => { - if (!Util.isVoiceEmpty(queue.connection.channel)) return; - if (!this.queues.has(queue.guild.id)) return; - if (queue.options.leaveOnEmpty) queue.destroy(true); - this.emit("channelEmpty", queue); - }, queue.options.leaveOnEmptyCooldown || 0).unref(); - queue._cooldownsTimeout.set(`empty_${oldState.guild.id}`, timeout); - } else { - 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}`); - } - } - } - } + if (queue.connection && oldState.channelId && newState.channelId && oldState.channelId !== newState.channelId && newState.member.id != newState.guild.members.me.id) { + if (newState.channelId !== queue.connection.channel.id) { + if (!Util.isVoiceEmpty(queue.connection.channel)) return; + if (queue._cooldownsTimeout.has(`empty_${oldState.guild.id}`)) return; + const timeout = setTimeout(() => { + if (!Util.isVoiceEmpty(queue.connection.channel)) return; + if (!this.queues.has(queue.guild.id)) return; + if (queue.options.leaveOnEmpty) queue.destroy(true); + this.emit("channelEmpty", queue); + }, queue.options.leaveOnEmptyCooldown || 0).unref(); + queue._cooldownsTimeout.set(`empty_${oldState.guild.id}`, timeout); + } else { + 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}`); + } + } + } + } - /** - * Creates a queue for a guild if not available, else returns existing queue - * @param {GuildResolvable} guild The guild - * @param {PlayerOptions} queueInitOptions Queue init options - * @returns {Queue} - */ - createQueue(guild: GuildResolvable, queueInitOptions: PlayerOptions & { metadata?: T } = {}): Queue { - guild = this.client.guilds.resolve(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; + /** + * Creates a queue for a guild if not available, else returns existing queue + * @param {GuildResolvable} guild The guild + * @param {PlayerOptions} queueInitOptions Queue init options + * @returns {Queue} + */ + createQueue(guild: GuildResolvable, queueInitOptions: PlayerOptions & { metadata?: T } = {}): Queue { + guild = this.client.guilds.resolve(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; - const _meta = queueInitOptions.metadata; - delete queueInitOptions["metadata"]; - queueInitOptions.volumeSmoothness ??= 0.08; - const queue = new Queue(this, guild, queueInitOptions); - queue.metadata = _meta; - this.queues.set(guild.id, queue); + const _meta = queueInitOptions.metadata; + delete queueInitOptions["metadata"]; + queueInitOptions.volumeSmoothness ??= 0.08; + const queue = new Queue(this, guild, queueInitOptions); + queue.metadata = _meta; + this.queues.set(guild.id, queue); - return queue as Queue; - } + return queue as Queue; + } - /** - * Returns the queue if available - * @param {GuildResolvable} guild The guild id - * @returns {Queue} - */ - getQueue(guild: GuildResolvable) { - guild = this.client.guilds.resolve(guild); - if (!guild) throw new PlayerError("Unknown Guild", ErrorStatusCode.UNKNOWN_GUILD); - return this.queues.get(guild.id) as Queue; - } + /** + * Returns the queue if available + * @param {GuildResolvable} guild The guild id + * @returns {Queue} + */ + getQueue(guild: GuildResolvable) { + guild = this.client.guilds.resolve(guild); + if (!guild) throw new PlayerError("Unknown Guild", ErrorStatusCode.UNKNOWN_GUILD); + return this.queues.get(guild.id) as Queue; + } - /** - * Deletes a queue and returns deleted queue object - * @param {GuildResolvable} guild The guild id to remove - * @returns {Queue} - */ - deleteQueue(guild: GuildResolvable) { - guild = this.client.guilds.resolve(guild); - if (!guild) throw new PlayerError("Unknown Guild", ErrorStatusCode.UNKNOWN_GUILD); - const prev = this.getQueue(guild); + /** + * Deletes a queue and returns deleted queue object + * @param {GuildResolvable} guild The guild id to remove + * @returns {Queue} + */ + deleteQueue(guild: GuildResolvable) { + guild = this.client.guilds.resolve(guild); + if (!guild) throw new PlayerError("Unknown Guild", ErrorStatusCode.UNKNOWN_GUILD); + const prev = this.getQueue(guild); - try { - prev.destroy(); - } catch {} // eslint-disable-line no-empty - this.queues.delete(guild.id); + try { + prev.destroy(); + } catch {} // eslint-disable-line no-empty + this.queues.delete(guild.id); - return prev; - } + return prev; + } - /** - * @typedef {object} PlayerSearchResult - * @property {Playlist} [playlist] The playlist (if any) - * @property {Track[]} tracks The tracks - */ - /** - * Search tracks - * @param {string|Track} query The search query - * @param {SearchOptions} options The search options - * @returns {Promise} - */ - async search(query: string | Track, options: SearchOptions): Promise { - if (query instanceof Track) return { playlist: query.playlist || null, tracks: [query] }; - 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; - if (typeof options.searchEngine === "string" && this.extractors.has(options.searchEngine)) { - const extractor = this.extractors.get(options.searchEngine); - if (!extractor.validate(query)) return { playlist: null, tracks: [] }; - const data = await extractor.handle(query); - if (data && data.data.length) { - const playlist = !data.playlist - ? null - : new Playlist(this, { - ...data.playlist, - tracks: [] - }); + /** + * @typedef {object} PlayerSearchResult + * @property {Playlist} [playlist] The playlist (if any) + * @property {Track[]} tracks The tracks + */ + /** + * Search tracks + * @param {string|Track} query The search query + * @param {SearchOptions} options The search options + * @returns {Promise} + */ + async search(query: string | Track, options: SearchOptions): Promise { + if (query instanceof Track) return { playlist: query.playlist || null, tracks: [query] }; + 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; + if (typeof options.searchEngine === "string" && this.extractors.has(options.searchEngine)) { + const extractor = this.extractors.get(options.searchEngine); + if (!extractor.validate(query)) return { playlist: null, tracks: [] }; + const data = await extractor.handle(query); + if (data && data.data.length) { + const playlist = !data.playlist + ? null + : new Playlist(this, { + ...data.playlist, + tracks: [] + }); - const tracks = data.data.map( - (m) => - new Track(this, { - ...m, - requestedBy: options.requestedBy as User, - duration: Util.buildTimeCode(Util.parseMS(m.duration)), - playlist: playlist - }) - ); + const tracks = data.data.map( + (m) => + new Track(this, { + ...m, + requestedBy: options.requestedBy as User, + duration: Util.buildTimeCode(Util.parseMS(m.duration)), + playlist: playlist + }) + ); - if (playlist) playlist.tracks = tracks; + if (playlist) playlist.tracks = tracks; - return { playlist: playlist, tracks: tracks }; - } - } + return { playlist: playlist, tracks: tracks }; + } + } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - for (const [_, extractor] of this.extractors) { - if (options.blockExtractor) break; - if (!extractor.validate(query)) continue; - const data = await extractor.handle(query); - if (data && data.data.length) { - const playlist = !data.playlist - ? null - : new Playlist(this, { - ...data.playlist, - tracks: [] - }); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + for (const [_, extractor] of this.extractors) { + if (options.blockExtractor) break; + if (!extractor.validate(query)) continue; + const data = await extractor.handle(query); + if (data && data.data.length) { + const playlist = !data.playlist + ? null + : new Playlist(this, { + ...data.playlist, + tracks: [] + }); - const tracks = data.data.map( - (m) => - new Track(this, { - ...m, - requestedBy: options.requestedBy as User, - duration: Util.buildTimeCode(Util.parseMS(m.duration)), - playlist: playlist - }) - ); + const tracks = data.data.map( + (m) => + new Track(this, { + ...m, + requestedBy: options.requestedBy as User, + duration: Util.buildTimeCode(Util.parseMS(m.duration)), + playlist: playlist + }) + ); - if (playlist) playlist.tracks = tracks; + if (playlist) playlist.tracks = tracks; - return { playlist: playlist, tracks: tracks }; - } - } + return { playlist: playlist, tracks: tracks }; + } + } - const qt = options.searchEngine === QueryType.AUTO ? await QueryResolver.resolve(query) : options.searchEngine; - switch (qt) { - case QueryType.YOUTUBE_VIDEO: { - const info = await play.video_info(query).catch(Util.noop); - if (!info) return { playlist: null, tracks: [] }; + const qt = options.searchEngine === QueryType.AUTO ? await QueryResolver.resolve(query) : options.searchEngine; + switch (qt) { + case QueryType.YOUTUBE_VIDEO: { + const info = await play.video_info(query).catch(Util.noop); + if (!info) return { playlist: null, tracks: [] }; - const track = new Track(this, { - title: info.video_details.title, - description: info.video_details.description, - author: info.video_details.channel?.name, - url: info.video_details.url, - requestedBy: options.requestedBy as User, - thumbnail: Util.last(info.video_details.thumbnails)?.url, - views: info.video_details.views || 0, - duration: Util.buildTimeCode(Util.parseMS(info.video_details.durationInSec * 1000)), - source: "youtube", - raw: info - }); + const track = new Track(this, { + title: info.video_details.title, + description: info.video_details.description, + author: info.video_details.channel?.name, + url: info.video_details.url, + requestedBy: options.requestedBy as User, + thumbnail: Util.last(info.video_details.thumbnails)?.url, + views: info.video_details.views || 0, + duration: Util.buildTimeCode(Util.parseMS(info.video_details.durationInSec * 1000)), + source: "youtube", + raw: info + }); - return { playlist: null, tracks: [track] }; - } - case QueryType.YOUTUBE_SEARCH: { - const videos = await play - .search(query, { - limit: 10, - source: { youtube: "video" } - }) - .catch(Util.noop); - if (!videos) return { playlist: null, tracks: [] }; + return { playlist: null, tracks: [track] }; + } + case QueryType.YOUTUBE_SEARCH: { + const videos = await play + .search(query, { + limit: 10, + source: { youtube: "video" } + }) + .catch(Util.noop); + if (!videos) return { playlist: null, tracks: [] }; - const tracks = videos.map((m) => { - (m as any).source = "youtube"; // eslint-disable-line @typescript-eslint/no-explicit-any - return new Track(this, { - title: m.title, - description: m.description, - author: m.channel?.name, - url: m.url, - requestedBy: options.requestedBy as User, - thumbnail: Util.last(m.thumbnails).url, - views: m.views, - duration: m.durationRaw, - source: "youtube", - raw: m - }); - }); + const tracks = videos.map((m) => { + (m as any).source = "youtube"; // eslint-disable-line @typescript-eslint/no-explicit-any + return new Track(this, { + title: m.title, + description: m.description, + author: m.channel?.name, + url: m.url, + requestedBy: options.requestedBy as User, + thumbnail: Util.last(m.thumbnails).url, + views: m.views, + duration: m.durationRaw, + source: "youtube", + raw: m + }); + }); - return { playlist: null, tracks, searched: true }; - } - case QueryType.SOUNDCLOUD_TRACK: - case QueryType.SOUNDCLOUD_SEARCH: { - const result = - (await QueryResolver.resolve(query)) === QueryType.SOUNDCLOUD_TRACK - ? [{ url: query }] - : await play - .search(query, { - limit: 5, - source: { soundcloud: "tracks" } - }) - .catch(() => []); - if (!result || !result.length) return { playlist: null, tracks: [] }; - const res: Track[] = []; + return { playlist: null, tracks, searched: true }; + } + case QueryType.SOUNDCLOUD_TRACK: + case QueryType.SOUNDCLOUD_SEARCH: { + const result = + (await QueryResolver.resolve(query)) === QueryType.SOUNDCLOUD_TRACK + ? [{ url: query }] + : await play + .search(query, { + limit: 5, + source: { soundcloud: "tracks" } + }) + .catch(() => []); + if (!result || !result.length) return { playlist: null, tracks: [] }; + const res: Track[] = []; - for (const r of result) { - const trackInfo = await play.soundcloud(r.url).catch(Util.noop); - if (!trackInfo) continue; + for (const r of result) { + const trackInfo = await play.soundcloud(r.url).catch(Util.noop); + if (!trackInfo) continue; - const track = new Track(this, { - title: trackInfo.name, - url: trackInfo.url, - duration: Util.buildTimeCode(Util.parseMS(trackInfo.durationInMs)), - description: "", - thumbnail: trackInfo.user.thumbnail, - views: 0, - author: trackInfo.user.name, - requestedBy: options.requestedBy, - source: "soundcloud", - engine: trackInfo - }); + const track = new Track(this, { + title: trackInfo.name, + url: trackInfo.url, + duration: Util.buildTimeCode(Util.parseMS(trackInfo.durationInMs)), + description: "", + thumbnail: trackInfo.user.thumbnail, + views: 0, + author: trackInfo.user.name, + requestedBy: options.requestedBy, + source: "soundcloud", + engine: trackInfo + }); - res.push(track); - } + res.push(track); + } - return { playlist: null, tracks: res }; - } - case QueryType.SPOTIFY_SONG: { - const spotifyData = await Spotify(await Util.getFetch()) - .getData(query) - .catch(Util.noop); - if (!spotifyData) return { playlist: null, tracks: [] }; - const spotifyTrack = new Track(this, { - title: spotifyData.name, - description: spotifyData.description ?? "", - author: spotifyData.artists[0]?.name ?? "Unknown Artist", - url: spotifyData.external_urls?.spotify ?? query, - thumbnail: - spotifyData.album?.images[0]?.url ?? spotifyData.preview_url?.length - ? `https://i.scdn.co/image/${spotifyData.preview_url?.split("?cid=")[1]}` - : "https://www.scdn.co/i/_global/twitter_card-default.jpg", - duration: Util.buildTimeCode(Util.parseMS(spotifyData.duration_ms)), - views: 0, - requestedBy: options.requestedBy, - source: "spotify" - }); + return { playlist: null, tracks: res }; + } + case QueryType.SPOTIFY_SONG: { + const spotifyData = await Spotify(await Util.getFetch()) + .getData(query) + .catch(Util.noop); + if (!spotifyData) return { playlist: null, tracks: [] }; + const spotifyTrack = new Track(this, { + title: spotifyData.name, + description: spotifyData.description ?? "", + author: spotifyData.artists[0]?.name ?? "Unknown Artist", + url: spotifyData.external_urls?.spotify ?? query, + thumbnail: + spotifyData.album?.images[0]?.url ?? spotifyData.preview_url?.length + ? `https://i.scdn.co/image/${spotifyData.preview_url?.split("?cid=")[1]}` + : "https://www.scdn.co/i/_global/twitter_card-default.jpg", + duration: Util.buildTimeCode(Util.parseMS(spotifyData.duration_ms)), + views: 0, + requestedBy: options.requestedBy, + source: "spotify" + }); - return { playlist: null, tracks: [spotifyTrack] }; - } - case QueryType.SPOTIFY_PLAYLIST: - case QueryType.SPOTIFY_ALBUM: { - const spotifyPlaylist = await Spotify(await Util.getFetch()) - .getData(query) - .catch(Util.noop); - if (!spotifyPlaylist) return { playlist: null, tracks: [] }; + return { playlist: null, tracks: [spotifyTrack] }; + } + case QueryType.SPOTIFY_PLAYLIST: + case QueryType.SPOTIFY_ALBUM: { + const spotifyPlaylist = await Spotify(await Util.getFetch()) + .getData(query) + .catch(Util.noop); + if (!spotifyPlaylist) return { playlist: null, tracks: [] }; - const playlist = new Playlist(this, { - title: spotifyPlaylist.name ?? spotifyPlaylist.title, - description: spotifyPlaylist.description ?? "", - thumbnail: spotifyPlaylist.images[0]?.url ?? "https://www.scdn.co/i/_global/twitter_card-default.jpg", - type: spotifyPlaylist.type, - source: "spotify", - author: - spotifyPlaylist.type !== "playlist" - ? { - name: spotifyPlaylist.artists[0]?.name ?? "Unknown Artist", - url: spotifyPlaylist.artists[0]?.external_urls?.spotify ?? null - } - : { - name: spotifyPlaylist.owner?.display_name ?? spotifyPlaylist.owner?.id ?? "Unknown Artist", - url: spotifyPlaylist.owner?.external_urls?.spotify ?? null - }, - tracks: [], - id: spotifyPlaylist.id, - url: spotifyPlaylist.external_urls?.spotify ?? query, - rawPlaylist: spotifyPlaylist - }); + const playlist = new Playlist(this, { + title: spotifyPlaylist.name ?? spotifyPlaylist.title, + description: spotifyPlaylist.description ?? "", + thumbnail: spotifyPlaylist.images[0]?.url ?? "https://www.scdn.co/i/_global/twitter_card-default.jpg", + type: spotifyPlaylist.type, + source: "spotify", + author: + spotifyPlaylist.type !== "playlist" + ? { + name: spotifyPlaylist.artists[0]?.name ?? "Unknown Artist", + url: spotifyPlaylist.artists[0]?.external_urls?.spotify ?? null + } + : { + name: spotifyPlaylist.owner?.display_name ?? spotifyPlaylist.owner?.id ?? "Unknown Artist", + url: spotifyPlaylist.owner?.external_urls?.spotify ?? null + }, + tracks: [], + id: spotifyPlaylist.id, + url: spotifyPlaylist.external_urls?.spotify ?? query, + rawPlaylist: spotifyPlaylist + }); - if (spotifyPlaylist.type !== "playlist") { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - playlist.tracks = spotifyPlaylist.tracks.items.map((m: any) => { - const data = new Track(this, { - title: m.name ?? "", - description: m.description ?? "", - author: m.artists[0]?.name ?? "Unknown Artist", - url: m.external_urls?.spotify ?? query, - thumbnail: spotifyPlaylist.images[0]?.url ?? "https://www.scdn.co/i/_global/twitter_card-default.jpg", - duration: Util.buildTimeCode(Util.parseMS(m.duration_ms)), - views: 0, - requestedBy: options.requestedBy as User, - playlist, - source: "spotify" - }); + if (spotifyPlaylist.type !== "playlist") { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + playlist.tracks = spotifyPlaylist.tracks.items.map((m: any) => { + const data = new Track(this, { + title: m.name ?? "", + description: m.description ?? "", + author: m.artists[0]?.name ?? "Unknown Artist", + url: m.external_urls?.spotify ?? query, + thumbnail: spotifyPlaylist.images[0]?.url ?? "https://www.scdn.co/i/_global/twitter_card-default.jpg", + duration: Util.buildTimeCode(Util.parseMS(m.duration_ms)), + views: 0, + requestedBy: options.requestedBy as User, + playlist, + source: "spotify" + }); - return data; - }) as Track[]; - } else { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - playlist.tracks = spotifyPlaylist.tracks.items.map((m: any) => { - const data = new Track(this, { - title: m.track.name ?? "", - description: m.track.description ?? "", - author: m.track.artists?.[0]?.name ?? "Unknown Artist", - url: m.track.external_urls?.spotify ?? query, - thumbnail: m.track.album?.images?.[0]?.url ?? "https://www.scdn.co/i/_global/twitter_card-default.jpg", - duration: Util.buildTimeCode(Util.parseMS(m.track.duration_ms)), - views: 0, - requestedBy: options.requestedBy as User, - playlist, - source: "spotify" - }); + return data; + }) as Track[]; + } else { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + playlist.tracks = spotifyPlaylist.tracks.items.map((m: any) => { + const data = new Track(this, { + title: m.track.name ?? "", + description: m.track.description ?? "", + author: m.track.artists?.[0]?.name ?? "Unknown Artist", + url: m.track.external_urls?.spotify ?? query, + thumbnail: m.track.album?.images?.[0]?.url ?? "https://www.scdn.co/i/_global/twitter_card-default.jpg", + duration: Util.buildTimeCode(Util.parseMS(m.track.duration_ms)), + views: 0, + requestedBy: options.requestedBy as User, + playlist, + source: "spotify" + }); - return data; - }) as Track[]; - } + return data; + }) as Track[]; + } - return { playlist: playlist, tracks: playlist.tracks }; - } - case QueryType.SOUNDCLOUD_PLAYLIST: { - const data = (await play.soundcloud(query).catch(Util.noop)) as unknown as SoundCloudPlaylist; - if (!data) return { playlist: null, tracks: [] }; + return { playlist: playlist, tracks: playlist.tracks }; + } + case QueryType.SOUNDCLOUD_PLAYLIST: { + const data = (await play.soundcloud(query).catch(Util.noop)) as unknown as SoundCloudPlaylist; + if (!data) return { playlist: null, tracks: [] }; - const res = new Playlist(this, { - title: data.name, - description: "", - thumbnail: "https://soundcloud.com/pwa-icon-192.png", - type: "playlist", - source: "soundcloud", - author: { - name: data.user.name ?? "Unknown Owner", - url: data.user.url - }, - tracks: [], - id: `${data.id}`, // stringified - url: data.url, - rawPlaylist: data - }); + const res = new Playlist(this, { + title: data.name, + description: "", + thumbnail: "https://soundcloud.com/pwa-icon-192.png", + type: "playlist", + source: "soundcloud", + author: { + name: data.user.name ?? "Unknown Owner", + url: data.user.url + }, + tracks: [], + id: `${data.id}`, // stringified + url: data.url, + rawPlaylist: data + }); - const songs = await data.all_tracks(); - for (const song of songs) { - const track = new Track(this, { - title: song.name, - description: "", - author: song.publisher?.name ?? "Unknown", - url: song.permalink, - thumbnail: song.thumbnail, - duration: Util.buildTimeCode(Util.parseMS(song.durationInMs)), - views: 0, - requestedBy: options.requestedBy, - playlist: res, - source: "soundcloud", - engine: song - }); - res.tracks.push(track); - } + const songs = await data.all_tracks(); + for (const song of songs) { + const track = new Track(this, { + title: song.name, + description: "", + author: song.publisher?.name ?? "Unknown", + url: song.permalink, + thumbnail: song.thumbnail, + duration: Util.buildTimeCode(Util.parseMS(song.durationInMs)), + views: 0, + requestedBy: options.requestedBy, + playlist: res, + source: "soundcloud", + engine: song + }); + res.tracks.push(track); + } - return { playlist: res, tracks: res.tracks }; - } - case QueryType.YOUTUBE_PLAYLIST: { - const ytpl = (await play.playlist_info(query, { incomplete: true }).catch(Util.noop)) as unknown as YouTubePlayList; - if (!ytpl) return { playlist: null, tracks: [] }; + return { playlist: res, tracks: res.tracks }; + } + case QueryType.YOUTUBE_PLAYLIST: { + const ytpl = (await play.playlist_info(query, { incomplete: true }).catch(Util.noop)) as unknown as YouTubePlayList; + if (!ytpl) return { playlist: null, tracks: [] }; - const playlist: Playlist = new Playlist(this, { - title: ytpl.title, - thumbnail: ytpl.thumbnail as unknown as string, - description: "", - type: "playlist", - source: "youtube", - author: { - name: ytpl.channel.name, - url: ytpl.channel.url - }, - tracks: [], - id: ytpl.id, - url: ytpl.url, - rawPlaylist: ytpl - }); + const playlist: Playlist = new Playlist(this, { + title: ytpl.title, + thumbnail: ytpl.thumbnail as unknown as string, + description: "", + type: "playlist", + source: "youtube", + author: { + name: ytpl.channel.name, + url: ytpl.channel.url + }, + tracks: [], + id: ytpl.id, + url: ytpl.url, + rawPlaylist: ytpl + }); - const videos = await ytpl.all_videos(); - playlist.tracks = videos.map( - (video) => - new Track(this, { - title: video.title, - description: video.description, - author: video.channel?.name, - url: video.url, - requestedBy: options.requestedBy as User, - thumbnail: Util.last(video.thumbnails).url, - views: video.views, - duration: video.durationRaw, - raw: video, - playlist: playlist, - source: "youtube" - }) - ); + const videos = await ytpl.all_videos(); + playlist.tracks = videos.map( + (video) => + new Track(this, { + title: video.title, + description: video.description, + author: video.channel?.name, + url: video.url, + requestedBy: options.requestedBy as User, + thumbnail: Util.last(video.thumbnails).url, + views: video.views, + duration: video.durationRaw, + raw: video, + playlist: playlist, + source: "youtube" + }) + ); - return { playlist: playlist, tracks: playlist.tracks }; - } - default: - return { playlist: null, tracks: [] }; - } - } + return { playlist: playlist, tracks: playlist.tracks }; + } + default: + return { playlist: null, tracks: [] }; + } + } - /** - * Registers extractor - * @param {string} extractorName The extractor name - * @param {ExtractorModel|any} extractor The extractor object - * @param {boolean} [force=false] Overwrite existing extractor with this name (if available) - * @returns {ExtractorModel} - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - use(extractorName: string, extractor: ExtractorModel | any, force = false): ExtractorModel { - 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); - return extractor; - } + /** + * Registers extractor + * @param {string} extractorName The extractor name + * @param {ExtractorModel|any} extractor The extractor object + * @param {boolean} [force=false] Overwrite existing extractor with this name (if available) + * @returns {ExtractorModel} + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + use(extractorName: string, extractor: ExtractorModel | any, force = false): ExtractorModel { + 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); + return extractor; + } - for (const method of ["validate", "getInfo"]) { - if (typeof extractor[method] !== "function") throw new PlayerError("Invalid extractor data!", ErrorStatusCode.INVALID_EXTRACTOR); - } + for (const method of ["validate", "getInfo"]) { + if (typeof extractor[method] !== "function") throw new PlayerError("Invalid extractor data!", ErrorStatusCode.INVALID_EXTRACTOR); + } - const model = new ExtractorModel(extractorName, extractor); - this.extractors.set(model.name, model); + const model = new ExtractorModel(extractorName, extractor); + this.extractors.set(model.name, model); - return model; - } + return model; + } - /** - * Removes registered extractor - * @param {string} extractorName The extractor name - * @returns {ExtractorModel} - */ - unuse(extractorName: string) { - 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; - } + /** + * Removes registered extractor + * @param {string} extractorName The extractor name + * @returns {ExtractorModel} + */ + unuse(extractorName: string) { + 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; + } - /** - * Generates a report of the dependencies used by the `@discordjs/voice` module. Useful for debugging. - * @returns {string} - */ - scanDeps() { - const line = "-".repeat(50); - const depsReport = generateDependencyReport(); - const extractorReport = this.extractors - .map((m) => { - return `${m.name} :: ${m.version || "0.1.0"}`; - }) - .join("\n"); - return `${depsReport}\n${line}\nLoaded Extractors:\n${extractorReport || "None"}`; - } + /** + * Generates a report of the dependencies used by the `@discordjs/voice` module. Useful for debugging. + * @returns {string} + */ + scanDeps() { + const line = "-".repeat(50); + const depsReport = generateDependencyReport(); + const extractorReport = this.extractors + .map((m) => { + return `${m.name} :: ${m.version || "0.1.0"}`; + }) + .join("\n"); + return `${depsReport}\n${line}\nLoaded Extractors:\n${extractorReport || "None"}`; + } - emit(eventName: U, ...args: Parameters): boolean { - if (this.requiredEvents.includes(eventName) && !super.eventNames().includes(eventName)) { - // eslint-disable-next-line no-console - console.error(...args); - process.emitWarning(`[DiscordPlayerWarning] Unhandled "${eventName}" event! Events ${this.requiredEvents.map((m) => `"${m}"`).join(", ")} must have event listeners!`); - return false; - } else { - return super.emit(eventName, ...args); - } - } + emit(eventName: U, ...args: Parameters): boolean { + if (this.requiredEvents.includes(eventName) && !super.eventNames().includes(eventName)) { + // eslint-disable-next-line no-console + console.error(...args); + process.emitWarning(`[DiscordPlayerWarning] Unhandled "${eventName}" event! Events ${this.requiredEvents.map((m) => `"${m}"`).join(", ")} must have event listeners!`); + return false; + } else { + return super.emit(eventName, ...args); + } + } - /** - * Resolves queue - * @param {GuildResolvable|Queue} queueLike Queue like object - * @returns {Queue} - */ - resolveQueue(queueLike: GuildResolvable | Queue): Queue { - return this.getQueue(queueLike instanceof Queue ? queueLike.guild : queueLike); - } + /** + * Resolves queue + * @param {GuildResolvable|Queue} queueLike Queue like object + * @returns {Queue} + */ + resolveQueue(queueLike: GuildResolvable | Queue): Queue { + return this.getQueue(queueLike instanceof Queue ? queueLike.guild : queueLike); + } - *[Symbol.iterator]() { - yield* Array.from(this.queues.values()); - } + *[Symbol.iterator]() { + yield* Array.from(this.queues.values()); + } - /** - * Creates `Playlist` instance - * @param data The data to initialize a playlist - */ - createPlaylist(data: PlaylistInitData) { - return new Playlist(this, data); - } + /** + * Creates `Playlist` instance + * @param data The data to initialize a playlist + */ + createPlaylist(data: PlaylistInitData) { + return new Playlist(this, data); + } } export { Player }; diff --git a/src/Structures/ExtractorModel.ts b/src/Structures/ExtractorModel.ts index dc81618..3ff5f04 100644 --- a/src/Structures/ExtractorModel.ts +++ b/src/Structures/ExtractorModel.ts @@ -1,73 +1,73 @@ import { ExtractorModelData } from "../types/types"; class ExtractorModel { - name: string; - private _raw: any; // eslint-disable-line @typescript-eslint/no-explicit-any + name: string; + private _raw: any; // eslint-disable-line @typescript-eslint/no-explicit-any - /** - * Model for raw Discord Player extractors - * @param {string} extractorName Name of the extractor - * @param {object} data Extractor object - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - constructor(extractorName: string, data: any) { - /** - * The extractor name - * @type {string} - */ - this.name = extractorName; + /** + * Model for raw Discord Player extractors + * @param {string} extractorName Name of the extractor + * @param {object} data Extractor object + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + constructor(extractorName: string, data: any) { + /** + * The extractor name + * @type {string} + */ + this.name = extractorName; - /** - * The raw model - * @name ExtractorModel#_raw - * @type {any} - * @private - */ - Object.defineProperty(this, "_raw", { value: data, configurable: false, writable: false, enumerable: false }); - } + /** + * The raw model + * @name ExtractorModel#_raw + * @type {any} + * @private + */ + Object.defineProperty(this, "_raw", { value: data, configurable: false, writable: false, enumerable: false }); + } - /** - * Method to handle requests from `Player.play()` - * @param {string} query Query to handle - * @returns {Promise} - */ - async handle(query: string): Promise { - const data = await this._raw.getInfo(query); - if (!data) return null; + /** + * Method to handle requests from `Player.play()` + * @param {string} query Query to handle + * @returns {Promise} + */ + async handle(query: string): Promise { + const data = await this._raw.getInfo(query); + if (!data) return null; - return { - playlist: data.playlist ?? null, - data: - (data.info as Omit["data"])?.map((m) => ({ - title: m.title as string, - duration: m.duration as number, - thumbnail: m.thumbnail as string, - engine: m.engine, - views: m.views as number, - author: m.author as string, - description: m.description as string, - url: m.url as string, - source: m.source || "arbitrary" - })) ?? [] - }; - } + return { + playlist: data.playlist ?? null, + data: + (data.info as Omit["data"])?.map((m) => ({ + title: m.title as string, + duration: m.duration as number, + thumbnail: m.thumbnail as string, + engine: m.engine, + views: m.views as number, + author: m.author as string, + description: m.description as string, + url: m.url as string, + source: m.source || "arbitrary" + })) ?? [] + }; + } - /** - * Method used by Discord Player to validate query with this extractor - * @param {string} query The query to validate - * @returns {boolean} - */ - validate(query: string): boolean { - return Boolean(this._raw.validate(query)); - } + /** + * Method used by Discord Player to validate query with this extractor + * @param {string} query The query to validate + * @returns {boolean} + */ + validate(query: string): boolean { + return Boolean(this._raw.validate(query)); + } - /** - * The extractor version - * @type {string} - */ - get version(): string { - return this._raw.version ?? "0.0.0"; - } + /** + * The extractor version + * @type {string} + */ + get version(): string { + return this._raw.version ?? "0.0.0"; + } } export { ExtractorModel }; diff --git a/src/Structures/PlayerError.ts b/src/Structures/PlayerError.ts index da36da6..7d2695b 100644 --- a/src/Structures/PlayerError.ts +++ b/src/Structures/PlayerError.ts @@ -1,53 +1,53 @@ 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" + 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(); + message: string; + statusCode: ErrorStatusCode; + createdAt = new Date(); - constructor(message: string, code: ErrorStatusCode = ErrorStatusCode.PLAYER_ERROR) { - super(); + constructor(message: string, code: ErrorStatusCode = ErrorStatusCode.PLAYER_ERROR) { + super(); - this.message = `[${code}] ${message}`; - this.statusCode = code; - this.name = code; + this.message = `[${code}] ${message}`; + this.statusCode = code; + this.name = code; - Error.captureStackTrace(this); - } + Error.captureStackTrace(this); + } - get createdTimestamp() { - return this.createdAt.getTime(); - } + get createdTimestamp() { + return this.createdAt.getTime(); + } - valueOf() { - return this.statusCode; - } + valueOf() { + return this.statusCode; + } - toJSON() { - return { - stack: this.stack, - code: this.statusCode, - message: this.message, - created: this.createdTimestamp - }; - } + toJSON() { + return { + stack: this.stack, + code: this.statusCode, + message: this.message, + created: this.createdTimestamp + }; + } - toString() { - return this.stack; - } + toString() { + return this.stack; + } } diff --git a/src/Structures/Playlist.ts b/src/Structures/Playlist.ts index d76dd66..aa9bbec 100644 --- a/src/Structures/Playlist.ts +++ b/src/Structures/Playlist.ts @@ -3,136 +3,136 @@ import { Track } from "./Track"; import { PlaylistInitData, PlaylistJSON, TrackJSON, TrackSource } from "../types/types"; class Playlist { - public readonly player: Player; - public tracks: Track[]; - public title: string; - public description: string; - public thumbnail: string; - public type: "album" | "playlist"; - public source: TrackSource; - public author: { - name: string; - url: string; - }; - public id: string; - public url: string; - public readonly rawPlaylist?: any; // eslint-disable-line @typescript-eslint/no-explicit-any + public readonly player: Player; + public tracks: Track[]; + public title: string; + public description: string; + public thumbnail: string; + public type: "album" | "playlist"; + public source: TrackSource; + public author: { + name: string; + url: string; + }; + public id: string; + public url: string; + public readonly rawPlaylist?: any; // eslint-disable-line @typescript-eslint/no-explicit-any - /** - * Playlist constructor - * @param {Player} player The player - * @param {PlaylistInitData} data The data - */ - constructor(player: Player, data: PlaylistInitData) { - /** - * The player - * @name Playlist#player - * @type {Player} - * @readonly - */ - this.player = player; + /** + * Playlist constructor + * @param {Player} player The player + * @param {PlaylistInitData} data The data + */ + constructor(player: Player, data: PlaylistInitData) { + /** + * The player + * @name Playlist#player + * @type {Player} + * @readonly + */ + this.player = player; - /** - * The tracks in this playlist - * @name Playlist#tracks - * @type {Track[]} - */ - this.tracks = data.tracks ?? []; + /** + * The tracks in this playlist + * @name Playlist#tracks + * @type {Track[]} + */ + this.tracks = data.tracks ?? []; - /** - * The author of this playlist - * @name Playlist#author - * @type {object} - */ - this.author = data.author; + /** + * The author of this playlist + * @name Playlist#author + * @type {object} + */ + this.author = data.author; - /** - * The description - * @name Playlist#description - * @type {string} - */ - this.description = data.description; + /** + * The description + * @name Playlist#description + * @type {string} + */ + this.description = data.description; - /** - * The thumbnail of this playlist - * @name Playlist#thumbnail - * @type {string} - */ - this.thumbnail = data.thumbnail; + /** + * The thumbnail of this playlist + * @name Playlist#thumbnail + * @type {string} + */ + this.thumbnail = data.thumbnail; - /** - * The playlist type: - * - `album` - * - `playlist` - * @name Playlist#type - * @type {string} - */ - this.type = data.type; + /** + * The playlist type: + * - `album` + * - `playlist` + * @name Playlist#type + * @type {string} + */ + this.type = data.type; - /** - * The source of this playlist: - * - `youtube` - * - `soundcloud` - * - `spotify` - * - `arbitrary` - * @name Playlist#source - * @type {string} - */ - this.source = data.source; + /** + * The source of this playlist: + * - `youtube` + * - `soundcloud` + * - `spotify` + * - `arbitrary` + * @name Playlist#source + * @type {string} + */ + this.source = data.source; - /** - * The playlist id - * @name Playlist#id - * @type {string} - */ - this.id = data.id; + /** + * The playlist id + * @name Playlist#id + * @type {string} + */ + this.id = data.id; - /** - * The playlist url - * @name Playlist#url - * @type {string} - */ - this.url = data.url; + /** + * The playlist url + * @name Playlist#url + * @type {string} + */ + this.url = data.url; - /** - * The playlist title - * @type {string} - */ - this.title = data.title; + /** + * The playlist title + * @type {string} + */ + this.title = data.title; - /** - * @name Playlist#rawPlaylist - * @type {any} - * @readonly - */ - } + /** + * @name Playlist#rawPlaylist + * @type {any} + * @readonly + */ + } - *[Symbol.iterator]() { - yield* this.tracks; - } + *[Symbol.iterator]() { + yield* this.tracks; + } - /** - * JSON representation of this playlist - * @param {boolean} [withTracks=true] If it should build json with tracks - * @returns {PlaylistJSON} - */ - toJSON(withTracks = true) { - const payload = { - id: this.id, - url: this.url, - title: this.title, - description: this.description, - thumbnail: this.thumbnail, - type: this.type, - source: this.source, - author: this.author, - tracks: [] as TrackJSON[] - }; + /** + * JSON representation of this playlist + * @param {boolean} [withTracks=true] If it should build json with tracks + * @returns {PlaylistJSON} + */ + toJSON(withTracks = true) { + const payload = { + id: this.id, + url: this.url, + title: this.title, + description: this.description, + thumbnail: this.thumbnail, + type: this.type, + source: this.source, + author: this.author, + tracks: [] as TrackJSON[] + }; - if (withTracks) payload.tracks = this.tracks.map((m) => m.toJSON(true)); + if (withTracks) payload.tracks = this.tracks.map((m) => m.toJSON(true)); - return payload as PlaylistJSON; - } + return payload as PlaylistJSON; + } } export { Playlist }; diff --git a/src/Structures/Queue.ts b/src/Structures/Queue.ts index 0525643..aef63f3 100644 --- a/src/Structures/Queue.ts +++ b/src/Structures/Queue.ts @@ -13,752 +13,752 @@ import { VolumeTransformer } from "../VoiceInterface/VolumeTransformer"; import { createFFmpegStream } from "../utils/FFmpegStream"; class Queue { - public readonly guild: Guild; - public readonly player: Player; - public connection: StreamDispatcher; - public tracks: Track[] = []; - public previousTracks: Track[] = []; - public options: PlayerOptions; - public playing = false; - public metadata?: T = null; - public repeatMode: QueueRepeatMode = 0; - public readonly id = SnowflakeUtil.generate().toString(); - private _streamTime = 0; - public _cooldownsTimeout = new Collection(); - private _activeFilters: any[] = []; // eslint-disable-line @typescript-eslint/no-explicit-any - private _filtersUpdate = false; - #lastVolume = 0; - #destroyed = false; - public onBeforeCreateStream: (track: Track, source: TrackSource, queue: Queue) => Promise = null; - - /** - * 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 - */ - constructor(player: Player, guild: Guild, options: PlayerOptions = {}) { - /** - * The player that instantiated this queue - * @type {Player} - * @readonly - */ - this.player = player; - - /** - * The guild that instantiated this queue - * @type {Guild} - * @readonly - */ - this.guild = guild; - - /** - * The player options for this queue - * @type {PlayerOptions} - */ - this.options = {}; - - /** - * 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 - */ - - /** - * The ID of this queue - * @type {Snowflake} - * @name Queue#id - */ - - Object.assign( - this.options, - { - leaveOnEnd: true, - leaveOnStop: true, - leaveOnEmpty: true, - leaveOnEmptyCooldown: 1000, - autoSelfDeaf: true, - initialVolume: 100, - bufferingTimeout: 3000, - disableVolume: false - } as PlayerOptions, - options - ); - - if ("onBeforeCreateStream" in this.options) this.onBeforeCreateStream = this.options.onBeforeCreateStream; - - this.player.emit("debug", this, `Queue initialized:\n\n${this.player.scanDeps()}`); - } - - /** - * Returns current track - * @type {Track} - */ - get current() { - if (this.#watchDestroyed()) return; - return this.connection.audioResource?.metadata ?? this.tracks[0]; - } - - /** - * If this queue is destroyed - * @type {boolean} - */ - get destroyed() { - return this.#destroyed; - } - - /** - * Returns current track - * @returns {Track} - */ - nowPlaying() { - if (this.#watchDestroyed()) return; - return this.current; - } - - /** - * Connects to a voice channel - * @param {GuildChannelResolvable} channel The voice/stage channel - * @returns {Promise} - */ - async connect(channel: GuildChannelResolvable) { - if (this.#watchDestroyed()) return; - const _channel = this.guild.channels.resolve(channel) as StageChannel | VoiceChannel; - if (![ChannelType.GuildStageVoice, ChannelType.GuildVoice].includes(_channel?.type)) - throw new PlayerError(`Channel type must be GuildVoice or GuildStageVoice, got ${_channel?.type}!`, ErrorStatusCode.INVALID_ARG_TYPE); - const connection = await this.player.voiceUtils.connect(_channel, { - deaf: this.options.autoSelfDeaf - }); - this.connection = connection; - - if (_channel.type === ChannelType.GuildStageVoice) { - await _channel.guild.members.me.voice.setSuppressed(false).catch(async () => { - return await _channel.guild.members.me.voice.setRequestToSpeak(true).catch(Util.noop); - }); - } - - 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); - }); - - this.player.emit("connectionCreate", this, this.connection); - - this.connection.on("start", (resource) => { - if (this.#watchDestroyed(false)) return; - this.playing = true; - if (!this._filtersUpdate) this.player.emit("trackStart", this, resource?.metadata ?? this.current); - this._filtersUpdate = false; - }); - - this.connection.on("finish", async (resource) => { - if (this.#watchDestroyed(false)) return; - this.playing = false; - if (this._filtersUpdate) return; - this._streamTime = 0; - if (resource?.metadata) this.previousTracks.push(resource.metadata); - - this.player.emit("trackEnd", this, resource.metadata); - - if (!this.tracks.length && this.repeatMode === QueueRepeatMode.OFF) { - if (this.options.leaveOnEnd) this.destroy(); - this.player.emit("queueEnd", this); - } else if (!this.tracks.length && this.repeatMode === QueueRepeatMode.AUTOPLAY) { - this._handleAutoplay(this.current); - } else { - if (this.repeatMode === QueueRepeatMode.TRACK) return void this.play(this.current, { immediate: true }); - if (this.repeatMode === QueueRepeatMode.QUEUE) this.tracks.push(this.current); - const nextTrack = this.tracks.shift(); - this.play(nextTrack, { immediate: true }); - return; - } - }); - - return this; - } - - /** - * Destroys this queue - * @param {boolean} [disconnect=this.options.leaveOnStop] If it should leave on destroy - * @returns {void} - */ - destroy(disconnect = this.options.leaveOnStop) { - if (this.#watchDestroyed()) return; - if (this.connection) 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; - } - - /** - * Skips current track - * @returns {boolean} - */ - skip() { - if (this.#watchDestroyed()) return; - if (!this.connection) return false; - this._filtersUpdate = false; - this.connection.end(); - return true; - } - - /** - * Adds single track to the queue - * @param {Track} track The track to add - * @returns {void} - */ - addTrack(track: 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); - } - - /** - * Adds multiple tracks to the queue - * @param {Track[]} tracks Array of tracks to add - */ - addTracks(tracks: 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); - } - - /** - * Sets paused state - * @param {boolean} paused The paused state - * @returns {boolean} - */ - setPaused(paused?: boolean) { - if (this.#watchDestroyed()) return; - if (!this.connection) return false; - return paused ? this.connection.pause(true) : this.connection.resume(); - } - - /** - * Sets bitrate - * @param {number|auto} bitrate bitrate to set - * @returns {void} - */ - setBitrate(bitrate: number | "auto") { - 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); - } - - /** - * Sets volume - * @param {number} amount The volume amount - * @returns {boolean} - */ - setVolume(amount: number) { - if (this.#watchDestroyed()) return; - if (!this.connection) return false; - this.#lastVolume = amount; - this.options.initialVolume = amount; - return this.connection.setVolume(amount); - } - /** - * Sets repeat mode - * @param {QueueRepeatMode} mode The repeat mode - * @returns {boolean} - */ - setRepeatMode(mode: QueueRepeatMode) { - 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; - } - - /** - * The current volume amount - * @type {number} - */ - get volume() { - if (this.#watchDestroyed()) return; - if (!this.connection) return 100; - return this.connection.volume; - } - - set volume(amount: number) { - this.setVolume(amount); - } - - /** - * The stream time of this queue - * @type {number} - */ - get streamTime() { - 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; - const VW = this._activeFilters.includes("vaporwave") ? 0.8 : null; - - if (NC && VW) return playbackTime * (NC + VW); - return NC ? playbackTime * NC : VW ? playbackTime * VW : playbackTime; - } - - set streamTime(time: number) { - if (this.#watchDestroyed()) return; - this.seek(time); - } - - /** - * Returns enabled filters - * @returns {AudioFilters} - */ - getFiltersEnabled() { - if (this.#watchDestroyed()) return; - return AudioFilters.names.filter((x) => this._activeFilters.includes(x)); - } - - /** - * Returns disabled filters - * @returns {AudioFilters} - */ - getFiltersDisabled() { - if (this.#watchDestroyed()) return; - return AudioFilters.names.filter((x) => !this._activeFilters.includes(x)); - } - - /** - * Sets filters - * @param {QueueFilters} filters Queue filters - * @returns {Promise} - */ - async setFilters(filters?: QueueFilters) { - if (this.#watchDestroyed()) return; - 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: [] - }); - } - - const _filters: any[] = []; // eslint-disable-line @typescript-eslint/no-explicit-any - - for (const filter in filters) { - if (filters[filter as keyof QueueFilters] === true) _filters.push(filter); - } - - if (this._activeFilters.join("") === _filters.join("")) return; - - const newFilters = AudioFilters.create(_filters).trim(); - const streamTime = this.streamTime; - this._activeFilters = _filters; - - return await this.play(this.current, { - immediate: true, - filtersUpdate: true, - seek: streamTime, - encoderArgs: !_filters.length ? undefined : ["-af", newFilters] - }); - } - - /** - * Seeks to the given time - * @param {number} position The position - * @returns {boolean} - */ - async seek(position: number) { - if (this.#watchDestroyed()) return; - if (!this.playing || !this.current) return false; - if (position < 1) position = 0; - if (position >= this.current.durationMS) return this.skip(); - - await this.play(this.current, { - immediate: true, - filtersUpdate: true, // to stop events - seek: position - }); - - return true; - } - - /** - * Plays previous track - * @returns {Promise} - */ - async back() { - if (this.#watchDestroyed()) return; - const prev = this.previousTracks[this.previousTracks.length - 1]; - if (!prev) throw new PlayerError("Could not find previous track", ErrorStatusCode.TRACK_NOT_FOUND); - - return await this.play(prev, { immediate: true }); - } - - /** - * Clear this queue - */ - clear() { - if (this.#watchDestroyed()) return; - this.tracks = []; - this.previousTracks = []; - } - - /** - * Stops the player - * @returns {void} - */ - stop() { - if (this.#watchDestroyed()) return; - return this.destroy(); - } - - /** - * Shuffles this queue - * @returns {boolean} - */ - shuffle() { - if (this.#watchDestroyed()) return; - if (!this.tracks.length || this.tracks.length < 2) return false; - - 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]]; - } - - return true; - } - - /** - * Removes a track from the queue - * @param {Track|string|number} track The track to remove - * @returns {Track} - */ - remove(track: Track | string | number) { - if (this.#watchDestroyed()) return; - let trackFound: Track = null; - if (typeof track === "number") { - trackFound = this.tracks[track]; - if (trackFound) { - this.tracks = this.tracks.filter((t) => t.id !== trackFound.id); - } - } else { - trackFound = this.tracks.find((s) => s.id === (track instanceof Track ? track.id : track)); - if (trackFound) { - this.tracks = this.tracks.filter((s) => s.id !== trackFound.id); - } - } - - return trackFound; - } - - /** - * Returns the index of the specified track. If found, returns the track index else returns -1. - * @param {number|Track|string} track The track - * @returns {number} - */ - getTrackPosition(track: number | Track | string) { - if (this.#watchDestroyed()) return; - if (typeof track === "number") return this.tracks[track] != null ? track : -1; - return this.tracks.findIndex((pred) => pred.id === (track instanceof Track ? track.id : track)); - } - - /** - * Jumps to particular track - * @param {Track|number} track The track - * @returns {void} - */ - jump(track: Track | number): void { - if (this.#watchDestroyed()) return; - const foundTrack = this.remove(track); - if (!foundTrack) throw new PlayerError("Track not found", ErrorStatusCode.TRACK_NOT_FOUND); - - this.tracks.splice(0, 0, foundTrack); - - return void this.skip(); - } - - /** - * Jumps to particular track, removing other tracks on the way - * @param {Track|number} track The track - * @returns {void} - */ - skipTo(track: Track | number): void { - if (this.#watchDestroyed()) return; - const trackIndex = this.getTrackPosition(track); - const removedTrack = this.remove(track); - if (!removedTrack) throw new PlayerError("Track not found", ErrorStatusCode.TRACK_NOT_FOUND); - - this.tracks.splice(0, trackIndex, removedTrack); - - return void this.skip(); - } - - /** - * 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) { - if (this.#watchDestroyed()) return; - 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); - - this.player.emit("trackAdd", this, track); - } - - /** - * @typedef {object} PlayerTimestamp - * @property {string} current The current progress - * @property {string} end The total time - * @property {number} progress Progress in % - */ - - /** - * Returns player stream timestamp - * @returns {PlayerTimestamp} - */ - getPlayerTimestamp() { - if (this.#watchDestroyed()) return; - const currentStreamTime = this.streamTime; - const totalTime = this.current.durationMS; - - 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 }) { - if (this.#watchDestroyed()) return; - const length = typeof options.length === "number" ? (options.length <= 0 || options.length === Infinity ? 15 : options.length) : 15; - - const index = Math.floor((this.streamTime / this.current.durationMS) * length); - 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(); - return `${timestamp.current} ┃ ${bar.join("")} ┃ ${timestamp.end}`; - } else { - return `${bar.join("")}`; - } - } else { - if (options.timecodes) { - const timestamp = this.getPlayerTimestamp(); - return `${timestamp.current} ┃ ${indicator}${line.repeat(length - 1)} ┃ ${timestamp.end}`; - } else { - return `${indicator}${line.repeat(length - 1)}`; - } - } - } - - /** - * Total duration - * @type {Number} - */ - get totalTime(): number { - if (this.#watchDestroyed()) return; - return this.tracks.length > 0 ? this.tracks.map((t) => t.durationMS).reduce((p, c) => p + c) : 0; - } - - /** - * Play stream in a voice/stage channel - * @param {Track} [src] The track to play (if empty, uses first track from the queue) - * @param {PlayOptions} [options] The options - * @returns {Promise} - */ - async play(src?: Track, options: PlayOptions = {}): Promise { - if (this.#watchDestroyed(false)) return; - if (!this.connection || !this.connection.voiceConnection) throw new PlayerError("Voice connection is not available, use .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; - - this.player.emit("debug", this, "Received play request"); - - if (!options.filtersUpdate) this.previousTracks = this.previousTracks.filter((x) => x.id !== track.id); - - let stream = null; - const hasCustomDownloader = typeof this.onBeforeCreateStream === "function"; - - if (["youtube", "spotify", "soundcloud"].includes(track.raw.source)) { - let spotifyResolved = false; - if (track.raw.source === "spotify" && !track.raw.engine) { - track.raw.engine = await play - .search(`${track.author} ${track.title}`, { limit: 5 }) - .then((res) => res[0].url) - .catch(Util.noop); - spotifyResolved = true; - } - - const url = track.raw.source === "spotify" ? track.raw.engine : track.url; - if (!url) return void this.play(this.tracks.shift(), { immediate: true }); - - if (hasCustomDownloader) { - stream = (await this.onBeforeCreateStream(track, spotifyResolved ? "youtube" : track.raw.source, this)) || null; - } - - if (!stream) { - stream = (await play.stream(url, { discordPlayerCompatibility: true })).stream; - } - } else { - const arbitraryStream = (hasCustomDownloader && (await this.onBeforeCreateStream(track, track.raw.source || track.raw.engine, this))) || null; - stream = arbitraryStream ? await track.raw.engine.downloadProgressive() : typeof track.raw.engine === "function" ? await track.raw.engine() : track.raw.engine; - } - - const ffmpegStream = createFFmpegStream(stream, { - encoderArgs: options.encoderArgs || [], - seek: options.seek ? options.seek / 1000 : 0, - fmt: "s16le" - }).on("error", (err) => { - if (!`${err}`.toLowerCase().includes("premature close")) this.player.emit("error", this, err); - }); - - const resource: AudioResource = this.connection.createStream(ffmpegStream, { - type: StreamType.Raw, - data: track, - disableVolume: Boolean(this.options.disableVolume) - }); - - if (options.seek) this._streamTime = options.seek; - this._filtersUpdate = options.filtersUpdate; - - const volumeTransformer = resource.volume as VolumeTransformer; - if (volumeTransformer && typeof this.options.initialVolume === "number") Reflect.set(volumeTransformer, "volume", Math.pow(this.options.initialVolume / 100, 1.660964)); - if (volumeTransformer?.hasSmoothness && typeof this.options.volumeSmoothness === "number") { - if (typeof volumeTransformer.setSmoothness === "function") volumeTransformer.setSmoothness(this.options.volumeSmoothness || 0); - } - - setTimeout(() => { - this.connection.playStream(resource); - }, this.#getBufferingTimeout()).unref(); - } - - /** - * Private method to handle autoplay - * @param {Track} track The source track to find its similar track for autoplay - * @returns {Promise} - * @private - */ - private async _handleAutoplay(track: Track): Promise { - 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); - } - const info = await play.video_info(track.url).catch(Util.noop); - if (!info) { - if (this.options.leaveOnEnd) this.destroy(); - return void this.player.emit("queueEnd", this); - } - - const related = await play.video_info(info.related_videos[Math.floor(Math.random() * (info.related_videos.length - 1))]); - const nextTrack = new Track(this.player, { - title: related.video_details.title, - url: related.video_details.url, - duration: related.video_details.durationRaw ? Util.buildTimeCode(Util.parseMS(related.video_details.durationInSec * 1000)) : "0:00", - description: "", - thumbnail: Util.last(related.video_details.thumbnails).url, - views: related.video_details.views, - author: related.video_details.channel.name, - requestedBy: track.requestedBy, - source: "youtube" - }); - - this.play(nextTrack, { immediate: true }); - } - - *[Symbol.iterator]() { - if (this.#watchDestroyed()) return; - yield* this.tracks; - } - - /** - * JSON representation of this queue - * @returns {object} - */ - toJSON() { - if (this.#watchDestroyed()) return; - return { - id: this.id, - guild: this.guild.id, - voiceChannel: this.connection?.channel?.id, - options: this.options, - tracks: this.tracks.map((m) => m.toJSON()) - }; - } - - /** - * String representation of this queue - * @returns {string} - */ - toString() { - 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(emit = true) { - if (this.#destroyed) { - if (emit) this.player.emit("error", this, new PlayerError("Cannot use destroyed queue", ErrorStatusCode.DESTROYED_QUEUE)); - return true; - } - - return false; - } - - #getBufferingTimeout() { - const timeout = this.options.bufferingTimeout; - - if (isNaN(timeout) || timeout < 0 || !Number.isFinite(timeout)) return 1000; - return timeout; - } + public readonly guild: Guild; + public readonly player: Player; + public connection: StreamDispatcher; + public tracks: Track[] = []; + public previousTracks: Track[] = []; + public options: PlayerOptions; + public playing = false; + public metadata?: T = null; + public repeatMode: QueueRepeatMode = 0; + public readonly id = SnowflakeUtil.generate().toString(); + private _streamTime = 0; + public _cooldownsTimeout = new Collection(); + private _activeFilters: any[] = []; // eslint-disable-line @typescript-eslint/no-explicit-any + private _filtersUpdate = false; + #lastVolume = 0; + #destroyed = false; + public onBeforeCreateStream: (track: Track, source: TrackSource, queue: Queue) => Promise = null; + + /** + * 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 + */ + constructor(player: Player, guild: Guild, options: PlayerOptions = {}) { + /** + * The player that instantiated this queue + * @type {Player} + * @readonly + */ + this.player = player; + + /** + * The guild that instantiated this queue + * @type {Guild} + * @readonly + */ + this.guild = guild; + + /** + * The player options for this queue + * @type {PlayerOptions} + */ + this.options = {}; + + /** + * 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 + */ + + /** + * The ID of this queue + * @type {Snowflake} + * @name Queue#id + */ + + Object.assign( + this.options, + { + leaveOnEnd: true, + leaveOnStop: true, + leaveOnEmpty: true, + leaveOnEmptyCooldown: 1000, + autoSelfDeaf: false, + initialVolume: 100, + bufferingTimeout: 3000, + disableVolume: false + } as PlayerOptions, + options + ); + + if ("onBeforeCreateStream" in this.options) this.onBeforeCreateStream = this.options.onBeforeCreateStream; + + this.player.emit("debug", this, `Queue initialized:\n\n${this.player.scanDeps()}`); + } + + /** + * Returns current track + * @type {Track} + */ + get current() { + if (this.#watchDestroyed()) return; + return this.connection.audioResource?.metadata ?? this.tracks[0]; + } + + /** + * If this queue is destroyed + * @type {boolean} + */ + get destroyed() { + return this.#destroyed; + } + + /** + * Returns current track + * @returns {Track} + */ + nowPlaying() { + if (this.#watchDestroyed()) return; + return this.current; + } + + /** + * Connects to a voice channel + * @param {GuildChannelResolvable} channel The voice/stage channel + * @returns {Promise} + */ + async connect(channel: GuildChannelResolvable) { + if (this.#watchDestroyed()) return; + const _channel = this.guild.channels.resolve(channel) as StageChannel | VoiceChannel; + if (![ChannelType.GuildStageVoice, ChannelType.GuildVoice].includes(_channel?.type)) + throw new PlayerError(`Channel type must be GuildVoice or GuildStageVoice, got ${_channel?.type}!`, ErrorStatusCode.INVALID_ARG_TYPE); + const connection = await this.player.voiceUtils.connect(_channel, { + deaf: this.options.autoSelfDeaf + }); + this.connection = connection; + + if (_channel.type === ChannelType.GuildStageVoice) { + await _channel.guild.members.me.voice.setSuppressed(false).catch(async () => { + return await _channel.guild.members.me.voice.setRequestToSpeak(true).catch(Util.noop); + }); + } + + 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); + }); + + this.player.emit("connectionCreate", this, this.connection); + + this.connection.on("start", (resource) => { + if (this.#watchDestroyed(false)) return; + this.playing = true; + if (!this._filtersUpdate) this.player.emit("trackStart", this, resource?.metadata ?? this.current); + this._filtersUpdate = false; + }); + + this.connection.on("finish", async (resource) => { + if (this.#watchDestroyed(false)) return; + this.playing = false; + if (this._filtersUpdate) return; + this._streamTime = 0; + if (resource?.metadata) this.previousTracks.push(resource.metadata); + + this.player.emit("trackEnd", this, resource.metadata); + + if (!this.tracks.length && this.repeatMode === QueueRepeatMode.OFF) { + if (this.options.leaveOnEnd) this.destroy(); + this.player.emit("queueEnd", this); + } else if (!this.tracks.length && this.repeatMode === QueueRepeatMode.AUTOPLAY) { + this._handleAutoplay(this.current); + } else { + if (this.repeatMode === QueueRepeatMode.TRACK) return void this.play(this.current, { immediate: true }); + if (this.repeatMode === QueueRepeatMode.QUEUE) this.tracks.push(this.current); + const nextTrack = this.tracks.shift(); + this.play(nextTrack, { immediate: true }); + return; + } + }); + + return this; + } + + /** + * Destroys this queue + * @param {boolean} [disconnect=this.options.leaveOnStop] If it should leave on destroy + * @returns {void} + */ + destroy(disconnect = this.options.leaveOnStop) { + if (this.#watchDestroyed()) return; + if (this.connection) 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; + } + + /** + * Skips current track + * @returns {boolean} + */ + skip() { + if (this.#watchDestroyed()) return; + if (!this.connection) return false; + this._filtersUpdate = false; + this.connection.end(); + return true; + } + + /** + * Adds single track to the queue + * @param {Track} track The track to add + * @returns {void} + */ + addTrack(track: 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); + } + + /** + * Adds multiple tracks to the queue + * @param {Track[]} tracks Array of tracks to add + */ + addTracks(tracks: 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); + } + + /** + * Sets paused state + * @param {boolean} paused The paused state + * @returns {boolean} + */ + setPaused(paused?: boolean) { + if (this.#watchDestroyed()) return; + if (!this.connection) return false; + return paused ? this.connection.pause(true) : this.connection.resume(); + } + + /** + * Sets bitrate + * @param {number|auto} bitrate bitrate to set + * @returns {void} + */ + setBitrate(bitrate: number | "auto") { + 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); + } + + /** + * Sets volume + * @param {number} amount The volume amount + * @returns {boolean} + */ + setVolume(amount: number) { + if (this.#watchDestroyed()) return; + if (!this.connection) return false; + this.#lastVolume = amount; + this.options.initialVolume = amount; + return this.connection.setVolume(amount); + } + /** + * Sets repeat mode + * @param {QueueRepeatMode} mode The repeat mode + * @returns {boolean} + */ + setRepeatMode(mode: QueueRepeatMode) { + 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; + } + + /** + * The current volume amount + * @type {number} + */ + get volume() { + if (this.#watchDestroyed()) return; + if (!this.connection) return 100; + return this.connection.volume; + } + + set volume(amount: number) { + this.setVolume(amount); + } + + /** + * The stream time of this queue + * @type {number} + */ + get streamTime() { + 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; + const VW = this._activeFilters.includes("vaporwave") ? 0.8 : null; + + if (NC && VW) return playbackTime * (NC + VW); + return NC ? playbackTime * NC : VW ? playbackTime * VW : playbackTime; + } + + set streamTime(time: number) { + if (this.#watchDestroyed()) return; + this.seek(time); + } + + /** + * Returns enabled filters + * @returns {AudioFilters} + */ + getFiltersEnabled() { + if (this.#watchDestroyed()) return; + return AudioFilters.names.filter((x) => this._activeFilters.includes(x)); + } + + /** + * Returns disabled filters + * @returns {AudioFilters} + */ + getFiltersDisabled() { + if (this.#watchDestroyed()) return; + return AudioFilters.names.filter((x) => !this._activeFilters.includes(x)); + } + + /** + * Sets filters + * @param {QueueFilters} filters Queue filters + * @returns {Promise} + */ + async setFilters(filters?: QueueFilters) { + if (this.#watchDestroyed()) return; + 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: [] + }); + } + + const _filters: any[] = []; // eslint-disable-line @typescript-eslint/no-explicit-any + + for (const filter in filters) { + if (filters[filter as keyof QueueFilters] === true) _filters.push(filter); + } + + if (this._activeFilters.join("") === _filters.join("")) return; + + const newFilters = AudioFilters.create(_filters).trim(); + const streamTime = this.streamTime; + this._activeFilters = _filters; + + return await this.play(this.current, { + immediate: true, + filtersUpdate: true, + seek: streamTime, + encoderArgs: !_filters.length ? undefined : ["-af", newFilters] + }); + } + + /** + * Seeks to the given time + * @param {number} position The position + * @returns {boolean} + */ + async seek(position: number) { + if (this.#watchDestroyed()) return; + if (!this.playing || !this.current) return false; + if (position < 1) position = 0; + if (position >= this.current.durationMS) return this.skip(); + + await this.play(this.current, { + immediate: true, + filtersUpdate: true, // to stop events + seek: position + }); + + return true; + } + + /** + * Plays previous track + * @returns {Promise} + */ + async back() { + if (this.#watchDestroyed()) return; + const prev = this.previousTracks[this.previousTracks.length - 1]; + if (!prev) throw new PlayerError("Could not find previous track", ErrorStatusCode.TRACK_NOT_FOUND); + + return await this.play(prev, { immediate: true }); + } + + /** + * Clear this queue + */ + clear() { + if (this.#watchDestroyed()) return; + this.tracks = []; + this.previousTracks = []; + } + + /** + * Stops the player + * @returns {void} + */ + stop() { + if (this.#watchDestroyed()) return; + return this.destroy(); + } + + /** + * Shuffles this queue + * @returns {boolean} + */ + shuffle() { + if (this.#watchDestroyed()) return; + if (!this.tracks.length || this.tracks.length < 2) return false; + + 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]]; + } + + return true; + } + + /** + * Removes a track from the queue + * @param {Track|string|number} track The track to remove + * @returns {Track} + */ + remove(track: Track | string | number) { + if (this.#watchDestroyed()) return; + let trackFound: Track = null; + if (typeof track === "number") { + trackFound = this.tracks[track]; + if (trackFound) { + this.tracks = this.tracks.filter((t) => t.id !== trackFound.id); + } + } else { + trackFound = this.tracks.find((s) => s.id === (track instanceof Track ? track.id : track)); + if (trackFound) { + this.tracks = this.tracks.filter((s) => s.id !== trackFound.id); + } + } + + return trackFound; + } + + /** + * Returns the index of the specified track. If found, returns the track index else returns -1. + * @param {number|Track|string} track The track + * @returns {number} + */ + getTrackPosition(track: number | Track | string) { + if (this.#watchDestroyed()) return; + if (typeof track === "number") return this.tracks[track] != null ? track : -1; + return this.tracks.findIndex((pred) => pred.id === (track instanceof Track ? track.id : track)); + } + + /** + * Jumps to particular track + * @param {Track|number} track The track + * @returns {void} + */ + jump(track: Track | number): void { + if (this.#watchDestroyed()) return; + const foundTrack = this.remove(track); + if (!foundTrack) throw new PlayerError("Track not found", ErrorStatusCode.TRACK_NOT_FOUND); + + this.tracks.splice(0, 0, foundTrack); + + return void this.skip(); + } + + /** + * Jumps to particular track, removing other tracks on the way + * @param {Track|number} track The track + * @returns {void} + */ + skipTo(track: Track | number): void { + if (this.#watchDestroyed()) return; + const trackIndex = this.getTrackPosition(track); + const removedTrack = this.remove(track); + if (!removedTrack) throw new PlayerError("Track not found", ErrorStatusCode.TRACK_NOT_FOUND); + + this.tracks.splice(0, trackIndex, removedTrack); + + return void this.skip(); + } + + /** + * 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) { + if (this.#watchDestroyed()) return; + 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); + + this.player.emit("trackAdd", this, track); + } + + /** + * @typedef {object} PlayerTimestamp + * @property {string} current The current progress + * @property {string} end The total time + * @property {number} progress Progress in % + */ + + /** + * Returns player stream timestamp + * @returns {PlayerTimestamp} + */ + getPlayerTimestamp() { + if (this.#watchDestroyed()) return; + const currentStreamTime = this.streamTime; + const totalTime = this.current.durationMS; + + 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 }) { + if (this.#watchDestroyed()) return; + const length = typeof options.length === "number" ? (options.length <= 0 || options.length === Infinity ? 15 : options.length) : 15; + + const index = Math.floor((this.streamTime / this.current.durationMS) * length); + 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(); + return `${timestamp.current} ┃ ${bar.join("")} ┃ ${timestamp.end}`; + } else { + return `${bar.join("")}`; + } + } else { + if (options.timecodes) { + const timestamp = this.getPlayerTimestamp(); + return `${timestamp.current} ┃ ${indicator}${line.repeat(length - 1)} ┃ ${timestamp.end}`; + } else { + return `${indicator}${line.repeat(length - 1)}`; + } + } + } + + /** + * Total duration + * @type {Number} + */ + get totalTime(): number { + if (this.#watchDestroyed()) return; + return this.tracks.length > 0 ? this.tracks.map((t) => t.durationMS).reduce((p, c) => p + c) : 0; + } + + /** + * Play stream in a voice/stage channel + * @param {Track} [src] The track to play (if empty, uses first track from the queue) + * @param {PlayOptions} [options] The options + * @returns {Promise} + */ + async play(src?: Track, options: PlayOptions = {}): Promise { + if (this.#watchDestroyed(false)) return; + if (!this.connection || !this.connection.voiceConnection) throw new PlayerError("Voice connection is not available, use .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; + + this.player.emit("debug", this, "Received play request"); + + if (!options.filtersUpdate) this.previousTracks = this.previousTracks.filter((x) => x.id !== track.id); + + let stream = null; + const hasCustomDownloader = typeof this.onBeforeCreateStream === "function"; + + if (["youtube", "spotify", "soundcloud"].includes(track.raw.source)) { + let spotifyResolved = false; + if (track.raw.source === "spotify" && !track.raw.engine) { + track.raw.engine = await play + .search(`${track.author} ${track.title}`, { limit: 5 }) + .then((res) => res[0].url) + .catch(Util.noop); + spotifyResolved = true; + } + + const url = track.raw.source === "spotify" ? track.raw.engine : track.url; + if (!url) return void this.play(this.tracks.shift(), { immediate: true }); + + if (hasCustomDownloader) { + stream = (await this.onBeforeCreateStream(track, spotifyResolved ? "youtube" : track.raw.source, this)) || null; + } + + if (!stream) { + stream = (await play.stream(url, { discordPlayerCompatibility: true })).stream; + } + } else { + const arbitraryStream = (hasCustomDownloader && (await this.onBeforeCreateStream(track, track.raw.source || track.raw.engine, this))) || null; + stream = arbitraryStream ? await track.raw.engine.downloadProgressive() : typeof track.raw.engine === "function" ? await track.raw.engine() : track.raw.engine; + } + + const ffmpegStream = createFFmpegStream(stream, { + encoderArgs: options.encoderArgs || [], + seek: options.seek ? options.seek / 1000 : 0, + fmt: "s16le" + }).on("error", (err) => { + if (!`${err}`.toLowerCase().includes("premature close")) this.player.emit("error", this, err); + }); + + const resource: AudioResource = this.connection.createStream(ffmpegStream, { + type: StreamType.Raw, + data: track, + disableVolume: Boolean(this.options.disableVolume) + }); + + if (options.seek) this._streamTime = options.seek; + this._filtersUpdate = options.filtersUpdate; + + const volumeTransformer = resource.volume as VolumeTransformer; + if (volumeTransformer && typeof this.options.initialVolume === "number") Reflect.set(volumeTransformer, "volume", Math.pow(this.options.initialVolume / 100, 1.660964)); + if (volumeTransformer?.hasSmoothness && typeof this.options.volumeSmoothness === "number") { + if (typeof volumeTransformer.setSmoothness === "function") volumeTransformer.setSmoothness(this.options.volumeSmoothness || 0); + } + + setTimeout(() => { + this.connection.playStream(resource); + }, this.#getBufferingTimeout()).unref(); + } + + /** + * Private method to handle autoplay + * @param {Track} track The source track to find its similar track for autoplay + * @returns {Promise} + * @private + */ + private async _handleAutoplay(track: Track): Promise { + 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); + } + const info = await play.video_info(track.url).catch(Util.noop); + if (!info) { + if (this.options.leaveOnEnd) this.destroy(); + return void this.player.emit("queueEnd", this); + } + + const related = await play.video_info(info.related_videos[Math.floor(Math.random() * (info.related_videos.length - 1))]); + const nextTrack = new Track(this.player, { + title: related.video_details.title, + url: related.video_details.url, + duration: related.video_details.durationRaw ? Util.buildTimeCode(Util.parseMS(related.video_details.durationInSec * 1000)) : "0:00", + description: "", + thumbnail: Util.last(related.video_details.thumbnails).url, + views: related.video_details.views, + author: related.video_details.channel.name, + requestedBy: track.requestedBy, + source: "youtube" + }); + + this.play(nextTrack, { immediate: true }); + } + + *[Symbol.iterator]() { + if (this.#watchDestroyed()) return; + yield* this.tracks; + } + + /** + * JSON representation of this queue + * @returns {object} + */ + toJSON() { + if (this.#watchDestroyed()) return; + return { + id: this.id, + guild: this.guild.id, + voiceChannel: this.connection?.channel?.id, + options: this.options, + tracks: this.tracks.map((m) => m.toJSON()) + }; + } + + /** + * String representation of this queue + * @returns {string} + */ + toString() { + 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(emit = true) { + if (this.#destroyed) { + if (emit) this.player.emit("error", this, new PlayerError("Cannot use destroyed queue", ErrorStatusCode.DESTROYED_QUEUE)); + return true; + } + + return false; + } + + #getBufferingTimeout() { + const timeout = this.options.bufferingTimeout; + + if (isNaN(timeout) || timeout < 0 || !Number.isFinite(timeout)) return 1000; + return timeout; + } } export { Queue }; diff --git a/src/Structures/Track.ts b/src/Structures/Track.ts index fd14275..4f6dd83 100644 --- a/src/Structures/Track.ts +++ b/src/Structures/Track.ts @@ -5,185 +5,185 @@ import { Playlist } from "./Playlist"; import { Queue } from "./Queue"; class Track { - public player!: Player; - public title!: string; - public description!: string; - public author!: string; - public url!: string; - public thumbnail!: string; - public duration!: string; - public views!: number; - public requestedBy!: User; - public playlist?: Playlist; - public readonly raw: RawTrackData = {} as RawTrackData; - public readonly id = SnowflakeUtil.generate().toString(); + public player!: Player; + public title!: string; + public description!: string; + public author!: string; + public url!: string; + public thumbnail!: string; + public duration!: string; + public views!: number; + public requestedBy!: User; + public playlist?: Playlist; + public readonly raw: RawTrackData = {} as RawTrackData; + public readonly id = SnowflakeUtil.generate().toString(); - /** - * Track constructor - * @param {Player} player The player that instantiated this Track - * @param {RawTrackData} data Track data - */ - constructor(player: Player, data: RawTrackData) { - /** - * The player that instantiated this Track - * @name Track#player - * @type {Player} - * @readonly - */ - Object.defineProperty(this, "player", { value: player, enumerable: false }); + /** + * Track constructor + * @param {Player} player The player that instantiated this Track + * @param {RawTrackData} data Track data + */ + constructor(player: Player, data: RawTrackData) { + /** + * The player that instantiated this Track + * @name Track#player + * @type {Player} + * @readonly + */ + Object.defineProperty(this, "player", { value: player, enumerable: false }); - /** - * Title of this track - * @name Track#title - * @type {string} - */ + /** + * Title of this track + * @name Track#title + * @type {string} + */ - /** - * Description of this track - * @name Track#description - * @type {string} - */ + /** + * Description of this track + * @name Track#description + * @type {string} + */ - /** - * Author of this track - * @name Track#author - * @type {string} - */ + /** + * Author of this track + * @name Track#author + * @type {string} + */ - /** - * URL of this track - * @name Track#url - * @type {string} - */ + /** + * URL of this track + * @name Track#url + * @type {string} + */ - /** - * Thumbnail of this track - * @name Track#thumbnail - * @type {string} - */ + /** + * Thumbnail of this track + * @name Track#thumbnail + * @type {string} + */ - /** - * Duration of this track - * @name Track#duration - * @type {string} - */ + /** + * Duration of this track + * @name Track#duration + * @type {string} + */ - /** - * Views count of this track - * @name Track#views - * @type {number} - */ + /** + * Views count of this track + * @name Track#views + * @type {number} + */ - /** - * Person who requested this track - * @name Track#requestedBy - * @type {User} - */ + /** + * Person who requested this track + * @name Track#requestedBy + * @type {User} + */ - /** - * If this track belongs to playlist - * @name Track#fromPlaylist - * @type {boolean} - */ + /** + * If this track belongs to playlist + * @name Track#fromPlaylist + * @type {boolean} + */ - /** - * Raw track data - * @name Track#raw - * @type {RawTrackData} - */ + /** + * Raw track data + * @name Track#raw + * @type {RawTrackData} + */ - /** - * The track id - * @name Track#id - * @type {Snowflake} - * @readonly - */ + /** + * The track id + * @name Track#id + * @type {Snowflake} + * @readonly + */ - /** - * The playlist which track belongs - * @name Track#playlist - * @type {Playlist} - */ + /** + * The playlist which track belongs + * @name Track#playlist + * @type {Playlist} + */ - void this._patch(data); - } + void this._patch(data); + } - private _patch(data: RawTrackData) { - this.title = escapeMarkdown(data.title ?? ""); - this.description = data.description ?? ""; - this.author = data.author ?? ""; - this.url = data.url ?? ""; - this.thumbnail = data.thumbnail ?? ""; - this.duration = data.duration ?? ""; - this.views = data.views ?? 0; - this.requestedBy = data.requestedBy; - this.playlist = data.playlist; + private _patch(data: RawTrackData) { + this.title = escapeMarkdown(data.title ?? ""); + this.description = data.description ?? ""; + this.author = data.author ?? ""; + this.url = data.url ?? ""; + this.thumbnail = data.thumbnail ?? ""; + this.duration = data.duration ?? ""; + this.views = data.views ?? 0; + this.requestedBy = data.requestedBy; + this.playlist = data.playlist; - // raw - Object.defineProperty(this, "raw", { value: Object.assign({}, { source: data.raw?.source ?? data.source }, data.raw ?? data), enumerable: false }); - } + // raw + Object.defineProperty(this, "raw", { value: Object.assign({}, { source: data.raw?.source ?? data.source }, data.raw ?? data), enumerable: false }); + } - /** - * The queue in which this track is located - * @type {Queue} - */ - get queue(): Queue { - return this.player.queues.find((q) => q.tracks.some((ab) => ab.id === this.id)); - } + /** + * The queue in which this track is located + * @type {Queue} + */ + get queue(): Queue { + return this.player.queues.find((q) => q.tracks.some((ab) => ab.id === this.id)); + } - /** - * The track duration in millisecond - * @type {number} - */ - get durationMS(): number { - const times = (n: number, t: number) => { - let tn = 1; - for (let i = 0; i < t; i++) tn *= n; - return t <= 0 ? 1000 : tn * 1000; - }; + /** + * The track duration in millisecond + * @type {number} + */ + get durationMS(): number { + const times = (n: number, t: number) => { + let tn = 1; + for (let i = 0; i < t; i++) tn *= n; + return t <= 0 ? 1000 : tn * 1000; + }; - return this.duration - .split(":") - .reverse() - .map((m, i) => parseInt(m) * times(60, i)) - .reduce((a, c) => a + c, 0); - } + return this.duration + .split(":") + .reverse() + .map((m, i) => parseInt(m) * times(60, i)) + .reduce((a, c) => a + c, 0); + } - /** - * Returns source of this track - * @type {TrackSource} - */ - get source() { - return this.raw.source ?? "arbitrary"; - } + /** + * Returns source of this track + * @type {TrackSource} + */ + get source() { + return this.raw.source ?? "arbitrary"; + } - /** - * String representation of this track - * @returns {string} - */ - toString(): string { - return `${this.title} by ${this.author}`; - } + /** + * String representation of this track + * @returns {string} + */ + toString(): string { + return `${this.title} by ${this.author}`; + } - /** - * Raw JSON representation of this track - * @returns {TrackJSON} - */ - toJSON(hidePlaylist?: boolean) { - return { - id: this.id, - title: this.title, - description: this.description, - author: this.author, - url: this.url, - thumbnail: this.thumbnail, - duration: this.duration, - durationMS: this.durationMS, - views: this.views, - requestedBy: this.requestedBy?.id, - playlist: hidePlaylist ? null : this.playlist?.toJSON() ?? null - } as TrackJSON; - } + /** + * Raw JSON representation of this track + * @returns {TrackJSON} + */ + toJSON(hidePlaylist?: boolean) { + return { + id: this.id, + title: this.title, + description: this.description, + author: this.author, + url: this.url, + thumbnail: this.thumbnail, + duration: this.duration, + durationMS: this.durationMS, + views: this.views, + requestedBy: this.requestedBy?.id, + playlist: hidePlaylist ? null : this.playlist?.toJSON() ?? null + } as TrackJSON; + } } export default Track; diff --git a/src/VoiceInterface/StreamDispatcher.ts b/src/VoiceInterface/StreamDispatcher.ts index e8566fc..d6575e0 100644 --- a/src/VoiceInterface/StreamDispatcher.ts +++ b/src/VoiceInterface/StreamDispatcher.ts @@ -1,15 +1,15 @@ import { - AudioPlayer, - AudioPlayerError, - AudioPlayerStatus, - AudioResource, - createAudioPlayer, - createAudioResource, - entersState, - StreamType, - VoiceConnection, - VoiceConnectionStatus, - VoiceConnectionDisconnectReason + AudioPlayer, + AudioPlayerError, + AudioPlayerStatus, + AudioResource, + createAudioPlayer, + createAudioResource, + entersState, + StreamType, + VoiceConnection, + VoiceConnectionStatus, + VoiceConnectionDisconnectReason } from "@discordjs/voice"; import { StageChannel, VoiceChannel } from "discord.js"; import { Duplex, Readable } from "stream"; @@ -19,235 +19,235 @@ import { Util } from "../utils/Util"; import { PlayerError, ErrorStatusCode } from "../Structures/PlayerError"; export interface VoiceEvents { - /* eslint-disable @typescript-eslint/no-explicit-any */ - error: (error: AudioPlayerError) => any; - debug: (message: string) => any; - start: (resource: AudioResource) => any; - finish: (resource: AudioResource) => any; - /* eslint-enable @typescript-eslint/no-explicit-any */ + /* eslint-disable @typescript-eslint/no-explicit-any */ + error: (error: AudioPlayerError) => any; + debug: (message: string) => any; + start: (resource: AudioResource) => any; + finish: (resource: AudioResource) => any; + /* eslint-enable @typescript-eslint/no-explicit-any */ } class StreamDispatcher extends EventEmitter { - public readonly voiceConnection: VoiceConnection; - public readonly audioPlayer: AudioPlayer; - public channel: VoiceChannel | StageChannel; - public audioResource?: AudioResource; - private readyLock = false; - public paused: boolean; + public readonly voiceConnection: VoiceConnection; + public readonly audioPlayer: AudioPlayer; + public channel: VoiceChannel | StageChannel; + public audioResource?: AudioResource; + private readyLock = false; + public paused: boolean; - /** - * Creates new connection object - * @param {VoiceConnection} connection The connection - * @param {VoiceChannel|StageChannel} channel The connected channel - * @private - */ - constructor(connection: VoiceConnection, channel: VoiceChannel | StageChannel, public readonly connectionTimeout: number = 20000) { - super(); + /** + * Creates new connection object + * @param {VoiceConnection} connection The connection + * @param {VoiceChannel|StageChannel} channel The connected channel + * @private + */ + constructor(connection: VoiceConnection, channel: VoiceChannel | StageChannel, public readonly connectionTimeout: number = 20000) { + super(); - /** - * The voice connection - * @type {VoiceConnection} - */ - this.voiceConnection = connection; + /** + * The voice connection + * @type {VoiceConnection} + */ + this.voiceConnection = connection; - /** - * The audio player - * @type {AudioPlayer} - */ - this.audioPlayer = createAudioPlayer(); + /** + * The audio player + * @type {AudioPlayer} + */ + this.audioPlayer = createAudioPlayer(); - /** - * The voice channel - * @type {VoiceChannel|StageChannel} - */ - this.channel = channel; + /** + * The voice channel + * @type {VoiceChannel|StageChannel} + */ + this.channel = channel; - /** - * The paused state - * @type {boolean} - */ - this.paused = false; + /** + * The paused state + * @type {boolean} + */ + this.paused = false; - this.voiceConnection.on("stateChange", async (_, newState) => { - if (newState.status === VoiceConnectionStatus.Disconnected) { - if (newState.reason === VoiceConnectionDisconnectReason.WebSocketClose && newState.closeCode === 4014) { - try { - await entersState(this.voiceConnection, VoiceConnectionStatus.Connecting, this.connectionTimeout); - } catch { - try { - this.voiceConnection.destroy(); - } catch (err) { - this.emit("error", err as AudioPlayerError); - } - } - } else if (this.voiceConnection.rejoinAttempts < 5) { - await Util.wait((this.voiceConnection.rejoinAttempts + 1) * 5000); - this.voiceConnection.rejoin(); - } else { - try { - this.voiceConnection.destroy(); - } catch (err) { - this.emit("error", err as AudioPlayerError); - } - } - } else if (newState.status === VoiceConnectionStatus.Destroyed) { - this.end(); - } else if (!this.readyLock && (newState.status === VoiceConnectionStatus.Connecting || newState.status === VoiceConnectionStatus.Signalling)) { - this.readyLock = true; - try { - await entersState(this.voiceConnection, VoiceConnectionStatus.Ready, this.connectionTimeout); - } catch { - if (this.voiceConnection.state.status !== VoiceConnectionStatus.Destroyed) { - try { - this.voiceConnection.destroy(); - } catch (err) { - this.emit("error", err as AudioPlayerError); - } - } - } finally { - this.readyLock = false; - } - } - }); + this.voiceConnection.on("stateChange", async (_, newState) => { + if (newState.status === VoiceConnectionStatus.Disconnected) { + if (newState.reason === VoiceConnectionDisconnectReason.WebSocketClose && newState.closeCode === 4014) { + try { + await entersState(this.voiceConnection, VoiceConnectionStatus.Connecting, this.connectionTimeout); + } catch { + try { + this.voiceConnection.destroy(); + } catch (err) { + this.emit("error", err as AudioPlayerError); + } + } + } else if (this.voiceConnection.rejoinAttempts < 5) { + await Util.wait((this.voiceConnection.rejoinAttempts + 1) * 5000); + this.voiceConnection.rejoin(); + } else { + try { + this.voiceConnection.destroy(); + } catch (err) { + this.emit("error", err as AudioPlayerError); + } + } + } else if (newState.status === VoiceConnectionStatus.Destroyed) { + this.end(); + } else if (!this.readyLock && (newState.status === VoiceConnectionStatus.Connecting || newState.status === VoiceConnectionStatus.Signalling)) { + this.readyLock = true; + try { + await entersState(this.voiceConnection, VoiceConnectionStatus.Ready, this.connectionTimeout); + } catch { + if (this.voiceConnection.state.status !== VoiceConnectionStatus.Destroyed) { + try { + this.voiceConnection.destroy(); + } catch (err) { + this.emit("error", err as AudioPlayerError); + } + } + } finally { + this.readyLock = false; + } + } + }); - this.audioPlayer.on("stateChange", (oldState, newState) => { - if (newState.status === AudioPlayerStatus.Playing) { - if (!this.paused) return void this.emit("start", this.audioResource); - } else if (newState.status === AudioPlayerStatus.Idle && oldState.status !== AudioPlayerStatus.Idle) { - if (!this.paused) { - void this.emit("finish", this.audioResource); - this.audioResource = null; - } - } - }); + this.audioPlayer.on("stateChange", (oldState, newState) => { + if (newState.status === AudioPlayerStatus.Playing) { + if (!this.paused) return void this.emit("start", this.audioResource); + } else if (newState.status === AudioPlayerStatus.Idle && oldState.status !== AudioPlayerStatus.Idle) { + if (!this.paused) { + void this.emit("finish", this.audioResource); + this.audioResource = null; + } + } + }); - this.audioPlayer.on("debug", (m) => void this.emit("debug", m)); - this.audioPlayer.on("error", (error) => void this.emit("error", error)); - this.voiceConnection.subscribe(this.audioPlayer); - } + this.audioPlayer.on("debug", (m) => void this.emit("debug", m)); + this.audioPlayer.on("error", (error) => void this.emit("error", error)); + this.voiceConnection.subscribe(this.audioPlayer); + } - /** - * Creates stream - * @param {Readable|Duplex|string} src The stream source - * @param {object} [ops] Options - * @returns {AudioResource} - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - createStream(src: Readable | Duplex | string, ops?: { type?: StreamType; data?: any; disableVolume?: boolean }) { - this.audioResource = createAudioResource(src, { - inputType: ops?.type ?? StreamType.Arbitrary, - metadata: ops?.data, - // eslint-disable-next-line no-extra-boolean-cast - inlineVolume: !Boolean(ops?.disableVolume) - }); + /** + * Creates stream + * @param {Readable|Duplex|string} src The stream source + * @param {object} [ops] Options + * @returns {AudioResource} + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + createStream(src: Readable | Duplex | string, ops?: { type?: StreamType; data?: any; disableVolume?: boolean }) { + this.audioResource = createAudioResource(src, { + inputType: ops?.type ?? StreamType.Arbitrary, + metadata: ops?.data, + // eslint-disable-next-line no-extra-boolean-cast + inlineVolume: !Boolean(ops?.disableVolume) + }); - return this.audioResource; - } + return this.audioResource; + } - /** - * The player status - * @type {AudioPlayerStatus} - */ - get status() { - return this.audioPlayer.state.status; - } + /** + * The player status + * @type {AudioPlayerStatus} + */ + get status() { + return this.audioPlayer.state.status; + } - /** - * Disconnects from voice - * @returns {void} - */ - disconnect() { - try { - this.audioPlayer.stop(true); - this.voiceConnection.destroy(); - } catch {} // eslint-disable-line no-empty - } + /** + * Disconnects from voice + * @returns {void} + */ + disconnect() { + try { + this.audioPlayer.stop(true); + this.voiceConnection.destroy(); + } catch {} // eslint-disable-line no-empty + } - /** - * Stops the player - * @returns {void} - */ - end() { - this.audioPlayer.stop(); - } + /** + * Stops the player + * @returns {void} + */ + end() { + this.audioPlayer.stop(); + } - /** - * Pauses the stream playback - * @param {boolean} [interpolateSilence=false] If true, the player will play 5 packets of silence after pausing to prevent audio glitches. - * @returns {boolean} - */ - pause(interpolateSilence?: boolean) { - const success = this.audioPlayer.pause(interpolateSilence); - this.paused = success; - return success; - } + /** + * Pauses the stream playback + * @param {boolean} [interpolateSilence=false] If true, the player will play 5 packets of silence after pausing to prevent audio glitches. + * @returns {boolean} + */ + pause(interpolateSilence?: boolean) { + const success = this.audioPlayer.pause(interpolateSilence); + this.paused = success; + return success; + } - /** - * Resumes the stream playback - * @returns {boolean} - */ - resume() { - const success = this.audioPlayer.unpause(); - this.paused = !success; - return success; - } + /** + * Resumes the stream playback + * @returns {boolean} + */ + resume() { + const success = this.audioPlayer.unpause(); + this.paused = !success; + return success; + } - /** - * Play stream - * @param {AudioResource} [resource=this.audioResource] The audio resource to play - * @returns {Promise} - */ - async playStream(resource: AudioResource = this.audioResource) { - if (!resource) throw new PlayerError("Audio resource is not available!", ErrorStatusCode.NO_AUDIO_RESOURCE); - if (resource.ended) return void this.emit("error", new PlayerError("Cannot play a resource that has already ended.") as unknown as AudioPlayerError); - if (!this.audioResource) this.audioResource = resource; - if (this.voiceConnection.state.status !== VoiceConnectionStatus.Ready) { - try { - await entersState(this.voiceConnection, VoiceConnectionStatus.Ready, this.connectionTimeout); - } catch (err) { - return void this.emit("error", err as AudioPlayerError); - } - } + /** + * Play stream + * @param {AudioResource} [resource=this.audioResource] The audio resource to play + * @returns {Promise} + */ + async playStream(resource: AudioResource = this.audioResource) { + if (!resource) throw new PlayerError("Audio resource is not available!", ErrorStatusCode.NO_AUDIO_RESOURCE); + if (resource.ended) return void this.emit("error", new PlayerError("Cannot play a resource that has already ended.") as unknown as AudioPlayerError); + if (!this.audioResource) this.audioResource = resource; + if (this.voiceConnection.state.status !== VoiceConnectionStatus.Ready) { + try { + await entersState(this.voiceConnection, VoiceConnectionStatus.Ready, this.connectionTimeout); + } catch (err) { + return void this.emit("error", err as AudioPlayerError); + } + } - try { - this.audioPlayer.play(resource); - } catch (e) { - this.emit("error", e as AudioPlayerError); - } + try { + this.audioPlayer.play(resource); + } catch (e) { + this.emit("error", e as AudioPlayerError); + } - return this; - } + return this; + } - /** - * Sets playback volume - * @param {number} value The volume amount - * @returns {boolean} - */ - setVolume(value: number) { - if (!this.audioResource?.volume || isNaN(value) || value < 0 || value > Infinity) return false; + /** + * Sets playback volume + * @param {number} value The volume amount + * @returns {boolean} + */ + setVolume(value: number) { + if (!this.audioResource?.volume || isNaN(value) || value < 0 || value > Infinity) return false; - this.audioResource.volume.setVolumeLogarithmic(value / 100); - return true; - } + this.audioResource.volume.setVolumeLogarithmic(value / 100); + return true; + } - /** - * The current volume - * @type {number} - */ - get volume() { - if (!this.audioResource?.volume) return 100; - const currentVol = this.audioResource.volume.volume; - return Math.round(Math.pow(currentVol, 1 / 1.660964) * 100); - } + /** + * The current volume + * @type {number} + */ + get volume() { + if (!this.audioResource?.volume) return 100; + const currentVol = this.audioResource.volume.volume; + return Math.round(Math.pow(currentVol, 1 / 1.660964) * 100); + } - /** - * The playback time - * @type {number} - */ - get streamTime() { - if (!this.audioResource) return 0; - return this.audioResource.playbackDuration; - } + /** + * The playback time + * @type {number} + */ + get streamTime() { + if (!this.audioResource) return 0; + return this.audioResource.playbackDuration; + } } export { StreamDispatcher as StreamDispatcher }; diff --git a/src/VoiceInterface/VoiceUtils.ts b/src/VoiceInterface/VoiceUtils.ts index ec7a99d..e793d8f 100644 --- a/src/VoiceInterface/VoiceUtils.ts +++ b/src/VoiceInterface/VoiceUtils.ts @@ -3,80 +3,80 @@ import { DiscordGatewayAdapterCreator, joinVoiceChannel, VoiceConnection } from import { StreamDispatcher } from "./StreamDispatcher"; class VoiceUtils { - public cache: Collection; + public cache: Collection; - /** - * The voice utils - * @private - */ - constructor() { - /** - * The cache where voice utils stores stream managers - * @type {Collection} - */ - this.cache = new Collection(); - } + /** + * The voice utils + * @private + */ + constructor() { + /** + * The cache where voice utils stores stream managers + * @type {Collection} + */ + this.cache = new Collection(); + } - /** - * Joins a voice channel, creating basic stream dispatch manager - * @param {StageChannel|VoiceChannel} channel The voice channel - * @param {object} [options] Join options - * @returns {Promise} - */ - public async connect( - channel: VoiceChannel | StageChannel, - options?: { - deaf?: boolean; - maxTime?: number; - } - ): Promise { - const conn = await this.join(channel, options); - const sub = new StreamDispatcher(conn, channel, options.maxTime); - this.cache.set(channel.guild.id, sub); - return sub; - } + /** + * Joins a voice channel, creating basic stream dispatch manager + * @param {StageChannel|VoiceChannel} channel The voice channel + * @param {object} [options] Join options + * @returns {Promise} + */ + public async connect( + channel: VoiceChannel | StageChannel, + options?: { + deaf?: boolean; + maxTime?: number; + } + ): Promise { + const conn = await this.join(channel, options); + const sub = new StreamDispatcher(conn, channel, options.maxTime); + this.cache.set(channel.guild.id, sub); + return sub; + } - /** - * Joins a voice channel - * @param {StageChannel|VoiceChannel} [channel] The voice/stage channel to join - * @param {object} [options] Join options - * @returns {VoiceConnection} - */ - public async join( - channel: VoiceChannel | StageChannel, - options?: { - deaf?: boolean; - maxTime?: number; - } - ) { - const conn = joinVoiceChannel({ - guildId: channel.guild.id, - channelId: channel.id, - adapterCreator: channel.guild.voiceAdapterCreator as unknown as DiscordGatewayAdapterCreator, - selfDeaf: Boolean(options.deaf) - }); + /** + * Joins a voice channel + * @param {StageChannel|VoiceChannel} [channel] The voice/stage channel to join + * @param {object} [options] Join options + * @returns {VoiceConnection} + */ + public async join( + channel: VoiceChannel | StageChannel, + options?: { + deaf?: boolean; + maxTime?: number; + } + ) { + const conn = joinVoiceChannel({ + guildId: channel.guild.id, + channelId: channel.id, + adapterCreator: channel.guild.voiceAdapterCreator as unknown as DiscordGatewayAdapterCreator, + selfDeaf: Boolean(options.deaf) + }); - return conn; - } + return conn; + } - /** - * Disconnects voice connection - * @param {VoiceConnection} connection The voice connection - * @returns {void} - */ - public disconnect(connection: VoiceConnection | StreamDispatcher) { - if (connection instanceof StreamDispatcher) return connection.voiceConnection.destroy(); - return connection.destroy(); - } + /** + * Disconnects voice connection + * @param {VoiceConnection} connection The voice connection + * @returns {void} + */ + public disconnect(connection: VoiceConnection | StreamDispatcher) { + if (connection instanceof StreamDispatcher) return connection.voiceConnection.destroy(); + return connection.destroy(); + } - /** - * Returns Discord Player voice connection - * @param {Snowflake} guild The guild id - * @returns {StreamDispatcher} - */ - public getConnection(guild: Snowflake) { - return this.cache.get(guild); - } + /** + * Returns Discord Player voice connection + * @param {Snowflake} guild The guild id + * @returns {StreamDispatcher} + */ + public getConnection(guild: Snowflake) { + return this.cache.get(guild); + } } export { VoiceUtils }; diff --git a/src/VoiceInterface/VolumeTransformer.ts b/src/VoiceInterface/VolumeTransformer.ts index b493dd1..f67746b 100644 --- a/src/VoiceInterface/VolumeTransformer.ts +++ b/src/VoiceInterface/VolumeTransformer.ts @@ -3,142 +3,142 @@ import { Transform, TransformOptions } from "stream"; export interface VolumeTransformerOptions extends TransformOptions { - type?: "s16le" | "s16be" | "s32le" | "s32be"; - smoothness?: number; - volume?: number; + type?: "s16le" | "s16be" | "s32le" | "s32be"; + smoothness?: number; + volume?: number; } export class VolumeTransformer extends Transform { - private _bits: number; - private _smoothing: number; - private _bytes: number; - private _extremum: number; - private _chunk: Buffer; - public volume: number; - private _targetVolume: number; - public type: "s16le" | "s32le" | "s16be" | "s32be"; - constructor(options: VolumeTransformerOptions = {}) { - super(options); - switch (options.type) { - case "s16le": - this._readInt = (buffer, index) => buffer.readInt16LE(index); - this._writeInt = (buffer, int, index) => buffer.writeInt16LE(int, index); - this._bits = 16; - break; - case "s16be": - this._readInt = (buffer, index) => buffer.readInt16BE(index); - this._writeInt = (buffer, int, index) => buffer.writeInt16BE(int, index); - this._bits = 16; - break; - case "s32le": - this._readInt = (buffer, index) => buffer.readInt32LE(index); - this._writeInt = (buffer, int, index) => buffer.writeInt32LE(int, index); - this._bits = 32; - break; - case "s32be": - this._readInt = (buffer, index) => buffer.readInt32BE(index); - this._writeInt = (buffer, int, index) => buffer.writeInt32BE(int, index); - this._bits = 32; - break; - default: - throw new Error("VolumeTransformer type should be one of s16le, s16be, s32le, s32be"); - } - this.type = options.type; - this._bytes = this._bits / 8; - this._extremum = Math.pow(2, this._bits - 1); - this.volume = Number.isNaN(options.volume) ? 1 : Number(options.volume); - if (!Number.isFinite(this.volume)) this.volume = 1; - this._targetVolume = this.volume; - this._chunk = Buffer.alloc(0); - this._smoothing = options.smoothness || 0; - } + private _bits: number; + private _smoothing: number; + private _bytes: number; + private _extremum: number; + private _chunk: Buffer; + public volume: number; + private _targetVolume: number; + public type: "s16le" | "s32le" | "s16be" | "s32be"; + constructor(options: VolumeTransformerOptions = {}) { + super(options); + switch (options.type) { + case "s16le": + this._readInt = (buffer, index) => buffer.readInt16LE(index); + this._writeInt = (buffer, int, index) => buffer.writeInt16LE(int, index); + this._bits = 16; + break; + case "s16be": + this._readInt = (buffer, index) => buffer.readInt16BE(index); + this._writeInt = (buffer, int, index) => buffer.writeInt16BE(int, index); + this._bits = 16; + break; + case "s32le": + this._readInt = (buffer, index) => buffer.readInt32LE(index); + this._writeInt = (buffer, int, index) => buffer.writeInt32LE(int, index); + this._bits = 32; + break; + case "s32be": + this._readInt = (buffer, index) => buffer.readInt32BE(index); + this._writeInt = (buffer, int, index) => buffer.writeInt32BE(int, index); + this._bits = 32; + break; + default: + throw new Error("VolumeTransformer type should be one of s16le, s16be, s32le, s32be"); + } + this.type = options.type; + this._bytes = this._bits / 8; + this._extremum = Math.pow(2, this._bits - 1); + this.volume = Number.isNaN(options.volume) ? 1 : Number(options.volume); + if (!Number.isFinite(this.volume)) this.volume = 1; + this._targetVolume = this.volume; + this._chunk = Buffer.alloc(0); + this._smoothing = options.smoothness || 0; + } - _readInt(buffer: Buffer, index: number) { - return index; - } - _writeInt(buffer: Buffer, int: number, index: number) { - return index; - } + _readInt(buffer: Buffer, index: number) { + return index; + } + _writeInt(buffer: Buffer, int: number, index: number) { + return index; + } - _applySmoothness() { - if (this.volume < this._targetVolume) { - this.volume = this.volume + this._smoothing >= this._targetVolume ? this._targetVolume : this.volume + this._smoothing; - } else if (this.volume > this._targetVolume) { - this.volume = this.volume - this._smoothing <= this._targetVolume ? this._targetVolume : this.volume - this._smoothing; - } - } + _applySmoothness() { + if (this.volume < this._targetVolume) { + this.volume = this.volume + this._smoothing >= this._targetVolume ? this._targetVolume : this.volume + this._smoothing; + } else if (this.volume > this._targetVolume) { + this.volume = this.volume - this._smoothing <= this._targetVolume ? this._targetVolume : this.volume - this._smoothing; + } + } - _transform(chunk: Buffer, encoding: BufferEncoding, done: () => unknown) { - if (this.smoothingEnabled() && this.volume !== this._targetVolume) this._applySmoothness(); + _transform(chunk: Buffer, encoding: BufferEncoding, done: () => unknown) { + if (this.smoothingEnabled() && this.volume !== this._targetVolume) this._applySmoothness(); - if (this.volume === 1) { - this.push(chunk); - return done(); - } + if (this.volume === 1) { + this.push(chunk); + return done(); + } - const { _bytes, _extremum } = this; + const { _bytes, _extremum } = this; - chunk = this._chunk = Buffer.concat([this._chunk, chunk]); - if (chunk.length < _bytes) return done(); + chunk = this._chunk = Buffer.concat([this._chunk, chunk]); + if (chunk.length < _bytes) return done(); - const complete = Math.floor(chunk.length / _bytes) * _bytes; + const complete = Math.floor(chunk.length / _bytes) * _bytes; - for (let i = 0; i < complete; i += _bytes) { - const int = Math.min(_extremum - 1, Math.max(-_extremum, Math.floor(this.volume * this._readInt(chunk, i)))); - this._writeInt(chunk, int, i); - } + for (let i = 0; i < complete; i += _bytes) { + const int = Math.min(_extremum - 1, Math.max(-_extremum, Math.floor(this.volume * this._readInt(chunk, i)))); + this._writeInt(chunk, int, i); + } - this._chunk = chunk.slice(complete); - this.push(chunk.slice(0, complete)); - return done(); - } + this._chunk = chunk.slice(complete); + this.push(chunk.slice(0, complete)); + return done(); + } - _destroy(err: Error, cb: (error: Error) => void) { - super._destroy(err, cb); - this._chunk = null; - } + _destroy(err: Error, cb: (error: Error) => void) { + super._destroy(err, cb); + this._chunk = null; + } - setVolume(volume: number) { - if (Number.isNaN(volume)) volume = 1; - if (typeof volume !== "number") volume = Number(volume); - if (!Number.isFinite(volume)) volume = volume < 0 ? 0 : 1; - this._targetVolume = volume; - if (this._smoothing <= 0) this.volume = volume; - } + setVolume(volume: number) { + if (Number.isNaN(volume)) volume = 1; + if (typeof volume !== "number") volume = Number(volume); + if (!Number.isFinite(volume)) volume = volume < 0 ? 0 : 1; + this._targetVolume = volume; + if (this._smoothing <= 0) this.volume = volume; + } - setVolumeDecibels(db: number) { - this.setVolume(Math.pow(10, db / 20)); - } + setVolumeDecibels(db: number) { + this.setVolume(Math.pow(10, db / 20)); + } - setVolumeLogarithmic(value: number) { - this.setVolume(Math.pow(value, 1.660964)); - } + setVolumeLogarithmic(value: number) { + this.setVolume(Math.pow(value, 1.660964)); + } - get volumeDecibels() { - return Math.log10(this.volume) * 20; - } + get volumeDecibels() { + return Math.log10(this.volume) * 20; + } - get volumeLogarithmic() { - return Math.pow(this.volume, 1 / 1.660964); - } + get volumeLogarithmic() { + return Math.pow(this.volume, 1 / 1.660964); + } - get smoothness() { - return this._smoothing; - } + get smoothness() { + return this._smoothing; + } - setSmoothness(smoothness: number) { - this._smoothing = smoothness; - } + setSmoothness(smoothness: number) { + this._smoothing = smoothness; + } - smoothingEnabled() { - return Number.isFinite(this._smoothing) && this._smoothing > 0; - } + smoothingEnabled() { + return Number.isFinite(this._smoothing) && this._smoothing > 0; + } - get hasSmoothness() { - return true; - } + get hasSmoothness() { + return true; + } - static get hasSmoothing() { - return true; - } + static get hasSmoothing() { + return true; + } } diff --git a/src/smoothVolume.ts b/src/smoothVolume.ts index d7c6af7..e10eba8 100644 --- a/src/smoothVolume.ts +++ b/src/smoothVolume.ts @@ -1,12 +1,12 @@ import { VolumeTransformer as VolumeTransformerMock } from "./VoiceInterface/VolumeTransformer"; try { - // eslint-disable-next-line - const mod = require("prism-media") as typeof import("prism-media") & { VolumeTransformer: typeof VolumeTransformerMock }; + // eslint-disable-next-line + const mod = require("prism-media") as typeof import("prism-media") & { VolumeTransformer: typeof VolumeTransformerMock }; - if (typeof mod.VolumeTransformer.hasSmoothing !== "boolean") { - Reflect.set(mod, "VolumeTransformer", VolumeTransformerMock); - } + if (typeof mod.VolumeTransformer.hasSmoothing !== "boolean") { + Reflect.set(mod, "VolumeTransformer", VolumeTransformerMock); + } } catch { - /* do nothing */ + /* do nothing */ } diff --git a/src/types/types.ts b/src/types/types.ts index 63f73b1..64c580f 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -8,48 +8,48 @@ import { StreamDispatcher } from "../VoiceInterface/StreamDispatcher"; export type FiltersName = keyof QueueFilters; export interface PlayerSearchResult { - playlist: Playlist | null; - tracks: Track[]; - searched?: boolean; + playlist: Playlist | null; + tracks: Track[]; + searched?: boolean; } /** * @typedef {AudioFilters} QueueFilters */ export interface QueueFilters { - bassboost_low?: boolean; - bassboost?: boolean; - bassboost_high?: boolean; - "8D"?: boolean; - vaporwave?: boolean; - nightcore?: boolean; - phaser?: boolean; - tremolo?: boolean; - vibrato?: boolean; - reverse?: boolean; - treble?: boolean; - normalizer?: boolean; - normalizer2?: boolean; - surrounding?: boolean; - pulsator?: boolean; - subboost?: boolean; - karaoke?: boolean; - flanger?: boolean; - gate?: boolean; - haas?: boolean; - mcompand?: boolean; - mono?: boolean; - mstlr?: boolean; - mstrr?: boolean; - compressor?: boolean; - expander?: boolean; - softlimiter?: boolean; - chorus?: boolean; - chorus2d?: boolean; - chorus3d?: boolean; - fadein?: boolean; - dim?: boolean; - earrape?: boolean; + bassboost_low?: boolean; + bassboost?: boolean; + bassboost_high?: boolean; + "8D"?: boolean; + vaporwave?: boolean; + nightcore?: boolean; + phaser?: boolean; + tremolo?: boolean; + vibrato?: boolean; + reverse?: boolean; + treble?: boolean; + normalizer?: boolean; + normalizer2?: boolean; + surrounding?: boolean; + pulsator?: boolean; + subboost?: boolean; + karaoke?: boolean; + flanger?: boolean; + gate?: boolean; + haas?: boolean; + mcompand?: boolean; + mono?: boolean; + mstlr?: boolean; + mstrr?: boolean; + compressor?: boolean; + expander?: boolean; + softlimiter?: boolean; + chorus?: boolean; + chorus2d?: boolean; + chorus3d?: boolean; + fadein?: boolean; + dim?: boolean; + earrape?: boolean; } /** @@ -79,19 +79,19 @@ export type TrackSource = "soundcloud" | "youtube" | "spotify" | "arbitrary"; * @property {any} [raw] The raw data */ export interface RawTrackData { - title: string; - description: string; - author: string; - url: string; - thumbnail: string; - duration: string; - views: number; - requestedBy: User; - playlist?: Playlist; - source?: TrackSource; - engine?: any; // eslint-disable-line @typescript-eslint/no-explicit-any - live?: boolean; - raw?: any; // eslint-disable-line @typescript-eslint/no-explicit-any + title: string; + description: string; + author: string; + url: string; + thumbnail: string; + duration: string; + views: number; + requestedBy: User; + playlist?: Playlist; + source?: TrackSource; + engine?: any; // eslint-disable-line @typescript-eslint/no-explicit-any + live?: boolean; + raw?: any; // eslint-disable-line @typescript-eslint/no-explicit-any } /** @@ -102,10 +102,10 @@ export interface RawTrackData { * @property {number} seconds Time in seconds */ export interface TimeData { - days: number; - hours: number; - minutes: number; - seconds: number; + days: number; + hours: number; + minutes: number; + seconds: number; } /** @@ -117,11 +117,11 @@ export interface TimeData { * @property {string} [indicator] The indicator */ export interface PlayerProgressbarOptions { - timecodes?: boolean; - length?: number; - line?: string; - indicator?: string; - queue?: boolean; + timecodes?: boolean; + length?: number; + line?: string; + indicator?: string; + queue?: boolean; } /** @@ -130,7 +130,7 @@ export interface PlayerProgressbarOptions { * @property {boolean} [leaveOnStop=true] If it should leave on stop * @property {boolean} [leaveOnEmpty=true] If it should leave on empty * @property {number} [leaveOnEmptyCooldown=1000] The cooldown in ms - * @property {boolean} [autoSelfDeaf=true] If it should set the bot in deaf mode + * @property {boolean} [autoSelfDeaf=false] If it should set the bot in deaf mode * @property {number} [initialVolume=100] The initial player volume * @property {number} [bufferingTimeout=3000] Buffering timeout for the stream * @property {boolean} [disableVolume=false] If player should disable inline volume @@ -139,16 +139,16 @@ export interface PlayerProgressbarOptions { * @property {Function} [onBeforeCreateStream] Runs before creating stream */ export interface PlayerOptions { - leaveOnEnd?: boolean; - leaveOnStop?: boolean; - leaveOnEmpty?: boolean; - leaveOnEmptyCooldown?: number; - autoSelfDeaf?: boolean; - initialVolume?: number; - bufferingTimeout?: number; - disableVolume?: boolean; - volumeSmoothness?: number; - onBeforeCreateStream?: (track: Track, source: TrackSource, queue: Queue) => Promise; + leaveOnEnd?: boolean; + leaveOnStop?: boolean; + leaveOnEmpty?: boolean; + leaveOnEmptyCooldown?: number; + autoSelfDeaf?: boolean; + initialVolume?: number; + bufferingTimeout?: number; + disableVolume?: boolean; + volumeSmoothness?: number; + onBeforeCreateStream?: (track: Track, source: TrackSource, queue: Queue) => Promise; } /** @@ -182,32 +182,32 @@ export interface PlayerOptions { * @property {TrackSource} [source="arbitrary"] The source */ export interface ExtractorModelData { - playlist?: { - title: string; - description: string; - thumbnail: string; - type: "album" | "playlist"; - source: TrackSource; - author: { - name: string; - url: string; - }; - id: string; - url: string; - rawPlaylist?: any; // eslint-disable-line @typescript-eslint/no-explicit-any - }; - data: { - title: string; - duration: number; - thumbnail: string; - engine: string | Readable | Duplex; - views: number; - author: string; - description: string; - url: string; - version?: string; - source?: TrackSource; - }[]; + playlist?: { + title: string; + description: string; + thumbnail: string; + type: "album" | "playlist"; + source: TrackSource; + author: { + name: string; + url: string; + }; + id: string; + url: string; + rawPlaylist?: any; // eslint-disable-line @typescript-eslint/no-explicit-any + }; + data: { + title: string; + duration: number; + thumbnail: string; + engine: string | Readable | Duplex; + views: number; + author: string; + description: string; + url: string; + version?: string; + source?: TrackSource; + }[]; } /** @@ -232,22 +232,22 @@ export interface ExtractorModelData { * @typedef {number} QueryType */ export enum QueryType { - AUTO, - YOUTUBE, - YOUTUBE_PLAYLIST, - SOUNDCLOUD_TRACK, - SOUNDCLOUD_PLAYLIST, - SOUNDCLOUD, - SPOTIFY_SONG, - SPOTIFY_ALBUM, - SPOTIFY_PLAYLIST, - FACEBOOK, - VIMEO, - ARBITRARY, - REVERBNATION, - YOUTUBE_SEARCH, - YOUTUBE_VIDEO, - SOUNDCLOUD_SEARCH + AUTO, + YOUTUBE, + YOUTUBE_PLAYLIST, + SOUNDCLOUD_TRACK, + SOUNDCLOUD_PLAYLIST, + SOUNDCLOUD, + SPOTIFY_SONG, + SPOTIFY_ALBUM, + SPOTIFY_PLAYLIST, + FACEBOOK, + VIMEO, + ARBITRARY, + REVERBNATION, + YOUTUBE_SEARCH, + YOUTUBE_VIDEO, + SOUNDCLOUD_SEARCH } /** @@ -327,17 +327,17 @@ export enum QueryType { /* eslint-disable @typescript-eslint/no-explicit-any */ export interface PlayerEvents { - botDisconnect: (queue: Queue) => any; - channelEmpty: (queue: Queue) => any; - 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; - trackStart: (queue: Queue, track: Track) => any; - trackEnd: (queue: Queue, track: Track) => any; + botDisconnect: (queue: Queue) => any; + channelEmpty: (queue: Queue) => any; + 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; + trackStart: (queue: Queue, track: Track) => any; + trackEnd: (queue: Queue, track: Track) => any; } /* eslint-enable @typescript-eslint/no-explicit-any */ @@ -350,10 +350,10 @@ export interface PlayerEvents { * @property {boolean} [immediate=false] If it should start playing the provided track immediately */ export interface PlayOptions { - filtersUpdate?: boolean; - encoderArgs?: string[]; - seek?: number; - immediate?: boolean; + filtersUpdate?: boolean; + encoderArgs?: string[]; + seek?: number; + immediate?: boolean; } /** @@ -363,9 +363,9 @@ export interface PlayOptions { * @property {boolean} [blockExtractor=false] If it should block custom extractors */ export interface SearchOptions { - requestedBy: UserResolvable; - searchEngine?: QueryType | string; - blockExtractor?: boolean; + requestedBy: UserResolvable; + searchEngine?: QueryType | string; + blockExtractor?: boolean; } /** @@ -377,10 +377,10 @@ export interface SearchOptions { * @typedef {number} QueueRepeatMode */ export enum QueueRepeatMode { - OFF = 0, - TRACK = 1, - QUEUE = 2, - AUTOPLAY = 3 + OFF = 0, + TRACK = 1, + QUEUE = 2, + AUTOPLAY = 3 } /** @@ -399,19 +399,19 @@ export enum QueueRepeatMode { * @property {any} [rawPlaylist] The raw playlist data */ export interface PlaylistInitData { - tracks: Track[]; - title: string; - description: string; - thumbnail: string; - type: "album" | "playlist"; - source: TrackSource; - author: { - name: string; - url: string; - }; - id: string; - url: string; - rawPlaylist?: any; // eslint-disable-line @typescript-eslint/no-explicit-any + tracks: Track[]; + title: string; + description: string; + thumbnail: string; + type: "album" | "playlist"; + source: TrackSource; + author: { + name: string; + url: string; + }; + id: string; + url: string; + rawPlaylist?: any; // eslint-disable-line @typescript-eslint/no-explicit-any } /** @@ -428,17 +428,17 @@ export interface PlaylistInitData { * @property {PlaylistJSON} [playlist] The playlist info (if any) */ export interface TrackJSON { - id: Snowflake; - title: string; - description: string; - author: string; - url: string; - thumbnail: string; - duration: string; - durationMS: number; - views: number; - requestedBy: Snowflake; - playlist?: PlaylistJSON; + id: Snowflake; + title: string; + description: string; + author: string; + url: string; + thumbnail: string; + duration: string; + durationMS: number; + views: number; + requestedBy: Snowflake; + playlist?: PlaylistJSON; } /** @@ -456,18 +456,18 @@ export interface TrackJSON { * @property {TrackJSON[]} tracks The tracks data (if any) */ export interface PlaylistJSON { - id: string; - url: string; - title: string; - description: string; - thumbnail: string; - type: "album" | "playlist"; - source: TrackSource; - author: { - name: string; - url: string; - }; - tracks: TrackJSON[]; + id: string; + url: string; + title: string; + description: string; + thumbnail: string; + type: "album" | "playlist"; + source: TrackSource; + author: { + name: string; + url: string; + }; + tracks: TrackJSON[]; } /** @@ -476,6 +476,6 @@ export interface PlaylistJSON { * @property {number} [connectionTimeout=20000] The voice connection timeout */ export interface PlayerInitOptions { - autoRegisterExtractor?: boolean; - connectionTimeout?: number; + autoRegisterExtractor?: boolean; + connectionTimeout?: number; } diff --git a/src/utils/AudioFilters.ts b/src/utils/AudioFilters.ts index b377064..f142c38 100644 --- a/src/utils/AudioFilters.ts +++ b/src/utils/AudioFilters.ts @@ -3,104 +3,104 @@ import { FiltersName } from "../types/types"; const bass = (g: number) => `bass=g=${g}:f=110:w=0.3`; class AudioFilters { - public constructor() { - return AudioFilters; - } + public constructor() { + return AudioFilters; + } - public static get filters(): Record { - return { - bassboost_low: bass(15), - bassboost: bass(20), - bassboost_high: bass(30), - "8D": "apulsator=hz=0.09", - vaporwave: "aresample=48000,asetrate=48000*0.8", - nightcore: "aresample=48000,asetrate=48000*1.25", - phaser: "aphaser=in_gain=0.4", - tremolo: "tremolo", - vibrato: "vibrato=f=6.5", - reverse: "areverse", - treble: "treble=g=5", - normalizer2: "dynaudnorm=g=101", - normalizer: "acompressor", - surrounding: "surround", - pulsator: "apulsator=hz=1", - subboost: "asubboost", - karaoke: "stereotools=mlev=0.03", - flanger: "flanger", - gate: "agate", - haas: "haas", - mcompand: "mcompand", - mono: "pan=mono|c0=.5*c0+.5*c1", - mstlr: "stereotools=mode=ms>lr", - mstrr: "stereotools=mode=ms>rr", - compressor: "compand=points=-80/-105|-62/-80|-15.4/-15.4|0/-12|20/-7.6", - expander: "compand=attacks=0:points=-80/-169|-54/-80|-49.5/-64.6|-41.1/-41.1|-25.8/-15|-10.8/-4.5|0/0|20/8.3", - softlimiter: "compand=attacks=0:points=-80/-80|-12.4/-12.4|-6/-8|0/-6.8|20/-2.8", - chorus: "chorus=0.7:0.9:55:0.4:0.25:2", - chorus2d: "chorus=0.6:0.9:50|60:0.4|0.32:0.25|0.4:2|1.3", - chorus3d: "chorus=0.5:0.9:50|60|40:0.4|0.32|0.3:0.25|0.4|0.3:2|2.3|1.3", - fadein: "afade=t=in:ss=0:d=10", - dim: `afftfilt="'real=re * (1-clip((b/nb)*b,0,1))':imag='im * (1-clip((b/nb)*b,0,1))'"`, - earrape: "channelsplit,sidechaingate=level_in=64" - }; - } + public static get filters(): Record { + return { + bassboost_low: bass(15), + bassboost: bass(20), + bassboost_high: bass(30), + "8D": "apulsator=hz=0.09", + vaporwave: "aresample=48000,asetrate=48000*0.8", + nightcore: "aresample=48000,asetrate=48000*1.25", + phaser: "aphaser=in_gain=0.4", + tremolo: "tremolo", + vibrato: "vibrato=f=6.5", + reverse: "areverse", + treble: "treble=g=5", + normalizer2: "dynaudnorm=g=101", + normalizer: "acompressor", + surrounding: "surround", + pulsator: "apulsator=hz=1", + subboost: "asubboost", + karaoke: "stereotools=mlev=0.03", + flanger: "flanger", + gate: "agate", + haas: "haas", + mcompand: "mcompand", + mono: "pan=mono|c0=.5*c0+.5*c1", + mstlr: "stereotools=mode=ms>lr", + mstrr: "stereotools=mode=ms>rr", + compressor: "compand=points=-80/-105|-62/-80|-15.4/-15.4|0/-12|20/-7.6", + expander: "compand=attacks=0:points=-80/-169|-54/-80|-49.5/-64.6|-41.1/-41.1|-25.8/-15|-10.8/-4.5|0/0|20/8.3", + softlimiter: "compand=attacks=0:points=-80/-80|-12.4/-12.4|-6/-8|0/-6.8|20/-2.8", + chorus: "chorus=0.7:0.9:55:0.4:0.25:2", + chorus2d: "chorus=0.6:0.9:50|60:0.4|0.32:0.25|0.4:2|1.3", + chorus3d: "chorus=0.5:0.9:50|60|40:0.4|0.32|0.3:0.25|0.4|0.3:2|2.3|1.3", + fadein: "afade=t=in:ss=0:d=10", + dim: `afftfilt="'real=re * (1-clip((b/nb)*b,0,1))':imag='im * (1-clip((b/nb)*b,0,1))'"`, + earrape: "channelsplit,sidechaingate=level_in=64" + }; + } - public static get(name: K) { - return this.filters[name]; - } + public static get(name: K) { + return this.filters[name]; + } - public static has(name: K) { - return name in this.filters; - } + public static has(name: K) { + return name in this.filters; + } - public static *[Symbol.iterator](): IterableIterator<{ name: FiltersName; value: string }> { - for (const [k, v] of Object.entries(this.filters)) { - yield { name: k as FiltersName, value: v as string }; - } - } + public static *[Symbol.iterator](): IterableIterator<{ name: FiltersName; value: string }> { + for (const [k, v] of Object.entries(this.filters)) { + yield { name: k as FiltersName, value: v as string }; + } + } - public static get names() { - return Object.keys(this.filters) as FiltersName[]; - } + public static get names() { + return Object.keys(this.filters) as FiltersName[]; + } - // @ts-expect-error AudioFilters.length - public static get length() { - return this.names.length; - } + // @ts-expect-error AudioFilters.length + public static get length() { + return this.names.length; + } - public static toString() { - return this.names.map((m) => (this as any)[m]).join(","); // eslint-disable-line @typescript-eslint/no-explicit-any - } + public static toString() { + return this.names.map((m) => (this as any)[m]).join(","); // eslint-disable-line @typescript-eslint/no-explicit-any + } - /** - * Create ffmpeg args from the specified filters name - * @param filter The filter name - * @returns - */ - public static create(filters?: K[]) { - if (!filters || !Array.isArray(filters)) return this.toString(); - return filters - .filter((predicate) => typeof predicate === "string") - .map((m) => this.get(m)) - .join(","); - } + /** + * Create ffmpeg args from the specified filters name + * @param filter The filter name + * @returns + */ + public static create(filters?: K[]) { + if (!filters || !Array.isArray(filters)) return this.toString(); + return filters + .filter((predicate) => typeof predicate === "string") + .map((m) => this.get(m)) + .join(","); + } - /** - * Defines audio filter - * @param filterName The name of the filter - * @param value The ffmpeg args - */ - public static define(filterName: string, value: string) { - this.filters[filterName as FiltersName] = value; - } + /** + * Defines audio filter + * @param filterName The name of the filter + * @param value The ffmpeg args + */ + public static define(filterName: string, value: string) { + this.filters[filterName as FiltersName] = value; + } - /** - * Defines multiple audio filters - * @param filtersArray Array of filters containing the filter name and ffmpeg args - */ - public static defineBulk(filtersArray: { name: string; value: string }[]) { - filtersArray.forEach((arr) => this.define(arr.name, arr.value)); - } + /** + * Defines multiple audio filters + * @param filtersArray Array of filters containing the filter name and ffmpeg args + */ + public static defineBulk(filtersArray: { name: string; value: string }[]) { + filtersArray.forEach((arr) => this.define(arr.name, arr.value)); + } } export default AudioFilters; diff --git a/src/utils/FFmpegStream.ts b/src/utils/FFmpegStream.ts index 9f49b28..97385e0 100644 --- a/src/utils/FFmpegStream.ts +++ b/src/utils/FFmpegStream.ts @@ -2,15 +2,15 @@ import { FFmpeg } from "prism-media"; import type { Duplex, Readable } from "stream"; export interface FFmpegStreamOptions { - fmt?: string; - encoderArgs?: string[]; - seek?: number; - skip?: boolean; + fmt?: string; + encoderArgs?: string[]; + seek?: number; + skip?: boolean; } export function FFMPEG_ARGS_STRING(stream: string, fmt?: string) { - // prettier-ignore - return [ + // prettier-ignore + return [ "-reconnect", "1", "-reconnect_streamed", "1", "-reconnect_delay_max", "5", @@ -24,8 +24,8 @@ export function FFMPEG_ARGS_STRING(stream: string, fmt?: string) { } export function FFMPEG_ARGS_PIPED(fmt?: string) { - // prettier-ignore - return [ + // prettier-ignore + return [ "-analyzeduration", "0", "-loglevel", "0", "-f", `${typeof fmt === "string" ? fmt : "s16le"}`, @@ -40,20 +40,20 @@ export function FFMPEG_ARGS_PIPED(fmt?: string) { * @param options FFmpeg stream options */ export function createFFmpegStream(stream: Readable | Duplex | string, options?: FFmpegStreamOptions) { - if (options.skip && typeof stream !== "string") return stream; - options ??= {}; - const args = typeof stream === "string" ? FFMPEG_ARGS_STRING(stream, options.fmt) : FFMPEG_ARGS_PIPED(options.fmt); + if (options.skip && typeof stream !== "string") return stream; + options ??= {}; + const args = typeof stream === "string" ? FFMPEG_ARGS_STRING(stream, options.fmt) : FFMPEG_ARGS_PIPED(options.fmt); - if (!Number.isNaN(options.seek)) args.unshift("-ss", String(options.seek)); - if (Array.isArray(options.encoderArgs)) args.push(...options.encoderArgs); + if (!Number.isNaN(options.seek)) args.unshift("-ss", String(options.seek)); + if (Array.isArray(options.encoderArgs)) args.push(...options.encoderArgs); - const transcoder = new FFmpeg({ shell: false, args }); - transcoder.on("close", () => transcoder.destroy()); + const transcoder = new FFmpeg({ shell: false, args }); + transcoder.on("close", () => transcoder.destroy()); - if (typeof stream !== "string") { - stream.on("error", () => transcoder.destroy()); - stream.pipe(transcoder); - } + if (typeof stream !== "string") { + stream.on("error", () => transcoder.destroy()); + stream.pipe(transcoder); + } - return transcoder; + return transcoder; } diff --git a/src/utils/QueryResolver.ts b/src/utils/QueryResolver.ts index 4b673b0..3ae4252 100644 --- a/src/utils/QueryResolver.ts +++ b/src/utils/QueryResolver.ts @@ -10,49 +10,49 @@ const vimeoRegex = /(http|https)?:\/\/(www\.|player\.)?vimeo\.com\/(?:channels\/ const facebookRegex = /(https?:\/\/)(www\.|m\.)?(facebook|fb).com\/.*\/videos\/.*/; const reverbnationRegex = /https:\/\/(www.)?reverbnation.com\/(.+)\/song\/(.+)/; const attachmentRegex = - /^(?:(?:https?|ftp):\/\/)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))(?::\d{2,5})?(?:\/\S*)?$/; + /^(?:(?:https?|ftp):\/\/)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))(?::\d{2,5})?(?:\/\S*)?$/; // scary things above *sigh* class QueryResolver { - /** - * Query resolver - */ - private constructor() {} // eslint-disable-line @typescript-eslint/no-empty-function + /** + * Query resolver + */ + private constructor() {} // eslint-disable-line @typescript-eslint/no-empty-function - /** - * Resolves the given search query - * @param {string} query The query - * @returns {QueryType} - */ - static async resolve(query: string): Promise { - if ((await play.so_validate(query)) === "track") return QueryType.SOUNDCLOUD_TRACK; - if ((await play.so_validate(query)) === "playlist" || query.includes("/sets/")) return QueryType.SOUNDCLOUD_PLAYLIST; - if (play.yt_validate(query) === "playlist") return QueryType.YOUTUBE_PLAYLIST; - if (play.yt_validate(query) === "video") return QueryType.YOUTUBE_VIDEO; - if (spotifySongRegex.test(query)) return QueryType.SPOTIFY_SONG; - if (spotifyPlaylistRegex.test(query)) return QueryType.SPOTIFY_PLAYLIST; - if (spotifyAlbumRegex.test(query)) return QueryType.SPOTIFY_ALBUM; - if (vimeoRegex.test(query)) return QueryType.VIMEO; - if (facebookRegex.test(query)) return QueryType.FACEBOOK; - if (reverbnationRegex.test(query)) return QueryType.REVERBNATION; - if (attachmentRegex.test(query)) return QueryType.ARBITRARY; + /** + * Resolves the given search query + * @param {string} query The query + * @returns {QueryType} + */ + static async resolve(query: string): Promise { + if ((await play.so_validate(query)) === "track") return QueryType.SOUNDCLOUD_TRACK; + if ((await play.so_validate(query)) === "playlist" || query.includes("/sets/")) return QueryType.SOUNDCLOUD_PLAYLIST; + if (play.yt_validate(query) === "playlist") return QueryType.YOUTUBE_PLAYLIST; + if (play.yt_validate(query) === "video") return QueryType.YOUTUBE_VIDEO; + if (spotifySongRegex.test(query)) return QueryType.SPOTIFY_SONG; + if (spotifyPlaylistRegex.test(query)) return QueryType.SPOTIFY_PLAYLIST; + if (spotifyAlbumRegex.test(query)) return QueryType.SPOTIFY_ALBUM; + if (vimeoRegex.test(query)) return QueryType.VIMEO; + if (facebookRegex.test(query)) return QueryType.FACEBOOK; + if (reverbnationRegex.test(query)) return QueryType.REVERBNATION; + if (attachmentRegex.test(query)) return QueryType.ARBITRARY; - return QueryType.YOUTUBE_SEARCH; - } + return QueryType.YOUTUBE_SEARCH; + } - /** - * Parses vimeo id from url - * @param {string} query The query - * @returns {string} - */ - static async getVimeoID(query: string): Promise { - return (await QueryResolver.resolve(query)) === QueryType.VIMEO - ? query - .split("/") - .filter((x) => !!x) - .pop() - : null; - } + /** + * Parses vimeo id from url + * @param {string} query The query + * @returns {string} + */ + static async getVimeoID(query: string): Promise { + return (await QueryResolver.resolve(query)) === QueryType.VIMEO + ? query + .split("/") + .filter((x) => !!x) + .pop() + : null; + } } export { QueryResolver }; diff --git a/src/utils/Util.ts b/src/utils/Util.ts index f173fee..84af49b 100644 --- a/src/utils/Util.ts +++ b/src/utils/Util.ts @@ -2,116 +2,116 @@ import { StageChannel, VoiceChannel } from "discord.js"; import { TimeData } from "../types/types"; class Util { - /** - * Utils - */ - private constructor() {} // eslint-disable-line @typescript-eslint/no-empty-function + /** + * Utils + */ + private constructor() {} // eslint-disable-line @typescript-eslint/no-empty-function - /** - * Creates duration string - * @param {object} durObj The duration object - * @returns {string} - */ - static durationString(durObj: Record) { - return Object.values(durObj) - .map((m) => (isNaN(m) ? 0 : m)) - .join(":"); - } + /** + * Creates duration string + * @param {object} durObj The duration object + * @returns {string} + */ + static durationString(durObj: Record) { + return Object.values(durObj) + .map((m) => (isNaN(m) ? 0 : m)) + .join(":"); + } - /** - * Parses milliseconds to consumable time object - * @param {number} milliseconds The time in ms - * @returns {TimeData} - */ - static parseMS(milliseconds: number) { - const round = milliseconds > 0 ? Math.floor : Math.ceil; + /** + * Parses milliseconds to consumable time object + * @param {number} milliseconds The time in ms + * @returns {TimeData} + */ + static parseMS(milliseconds: number) { + const round = milliseconds > 0 ? Math.floor : Math.ceil; - return { - days: round(milliseconds / 86400000), - hours: round(milliseconds / 3600000) % 24, - minutes: round(milliseconds / 60000) % 60, - seconds: round(milliseconds / 1000) % 60 - } as TimeData; - } + return { + days: round(milliseconds / 86400000), + hours: round(milliseconds / 3600000) % 24, + minutes: round(milliseconds / 60000) % 60, + seconds: round(milliseconds / 1000) % 60 + } as TimeData; + } - /** - * Builds time code - * @param {TimeData} duration The duration object - * @returns {string} - */ - static buildTimeCode(duration: TimeData) { - const items = Object.keys(duration); - const required = ["days", "hours", "minutes", "seconds"]; + /** + * Builds time code + * @param {TimeData} duration The duration object + * @returns {string} + */ + static buildTimeCode(duration: TimeData) { + const items = Object.keys(duration); + const required = ["days", "hours", "minutes", "seconds"]; - const parsed = items.filter((x) => required.includes(x)).map((m) => duration[m as keyof TimeData]); - const final = parsed - .slice(parsed.findIndex((x) => x !== 0)) - .map((x) => x.toString().padStart(2, "0")) - .join(":"); + const parsed = items.filter((x) => required.includes(x)).map((m) => duration[m as keyof TimeData]); + const final = parsed + .slice(parsed.findIndex((x) => x !== 0)) + .map((x) => x.toString().padStart(2, "0")) + .join(":"); - return final.length <= 3 ? `0:${final.padStart(2, "0") || 0}` : final; - } + return final.length <= 3 ? `0:${final.padStart(2, "0") || 0}` : final; + } - /** - * Picks last item of the given array - * @param {any[]} arr The array - * @returns {any} - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - static last(arr: T[]): T { - if (!Array.isArray(arr)) return; - return arr[arr.length - 1]; - } + /** + * Picks last item of the given array + * @param {any[]} arr The array + * @returns {any} + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + static last(arr: T[]): T { + if (!Array.isArray(arr)) return; + return arr[arr.length - 1]; + } - /** - * Checks if the voice channel is empty - * @param {VoiceChannel|StageChannel} channel The voice channel - * @returns {boolean} - */ - static isVoiceEmpty(channel: VoiceChannel | StageChannel) { - return channel.members.filter((member) => !member.user.bot).size === 0; - } + /** + * Checks if the voice channel is empty + * @param {VoiceChannel|StageChannel} channel The voice channel + * @returns {boolean} + */ + static isVoiceEmpty(channel: VoiceChannel | StageChannel) { + return channel.members.filter((member) => !member.user.bot).size === 0; + } - /** - * Safer require - * @param {string} id Node require id - * @returns {any} - */ - static require(id: string) { - try { - return require(id); - } catch { - return null; - } - } + /** + * Safer require + * @param {string} id Node require id + * @returns {any} + */ + static require(id: string) { + try { + return require(id); + } catch { + return null; + } + } - /** - * Asynchronous timeout - * @param {number} time The time in ms to wait - * @returns {Promise} - */ - static wait(time: number) { - return new Promise((r) => setTimeout(r, time).unref()); - } + /** + * Asynchronous timeout + * @param {number} time The time in ms to wait + * @returns {Promise} + */ + static wait(time: number) { + return new Promise((r) => setTimeout(r, time).unref()); + } - static noop() {} // eslint-disable-line @typescript-eslint/no-empty-function + static noop() {} // eslint-disable-line @typescript-eslint/no-empty-function - static async getFetch() { - if ("fetch" in globalThis) return globalThis.fetch; - for (const lib of ["undici", "node-fetch"]) { - try { - return await import(lib).then((res) => res.fetch || res.default?.fetch || res.default); - } catch { - try { - // eslint-disable-next-line - const res = require(lib); - if (res) return res.fetch || res.default?.fetch || res.default; - } catch { - // no? - } - } - } - } + static async getFetch() { + if ("fetch" in globalThis) return globalThis.fetch; + for (const lib of ["undici", "node-fetch"]) { + try { + return await import(lib).then((res) => res.fetch || res.default?.fetch || res.default); + } catch { + try { + // eslint-disable-next-line + const res = require(lib); + if (res) return res.fetch || res.default?.fetch || res.default; + } catch { + // no? + } + } + } + } } export { Util };