Я музыку переписал =) (#9)

This commit is contained in:
Jonny_Bro (Жоня) 2022-09-10 23:38:11 +05:00 committed by GitHub
parent 79a8647892
commit 6097de097d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
62 changed files with 11756 additions and 518 deletions

View file

@ -1,9 +1,9 @@
const { Client, Collection, SlashCommandBuilder, ContextMenuCommandBuilder } = require("discord.js"),
{ Player } = require("../helpers/Music/dist/index"),
{ DiscordTogether } = require("../helpers/discordTogether"),
{ GiveawaysManager } = require("discord-giveaways"),
{ Player } = require("discord-player"),
{ REST } = require("@discordjs/rest"),
{ Routes } = require("discord-api-types/v10"),
{ DiscordTogether } = require("../helpers/discordTogether");
{ Routes } = require("discord-api-types/v10");
const BaseEvent = require("./BaseEvent.js"),
BaseCommand = require("./BaseCommand.js"),
@ -20,7 +20,6 @@ moment.relativeTimeThreshold("h", 60);
moment.relativeTimeThreshold("d", 24);
moment.relativeTimeThreshold("M", 12);
// Creates JaBa class
class JaBa extends Client {
constructor(options) {
super(options);
@ -57,8 +56,7 @@ class JaBa extends Client {
this.player = new Player(this);
this.player
.on("trackStart", async (queue, track) => {
this.player.on("trackStart", async (queue, track) => {
const m = await queue.metadata.channel.send({ content: this.translate("music/play:NOW_PLAYING", { songName: track.title }, queue.metadata.channel.guild.data.language) });
if (track.durationMS > 1) {
setTimeout(() => {
@ -69,14 +67,14 @@ class JaBa extends Client {
if (m.deletable) m.delete();
}, (10 * 60 * 1000)); // m * s * ms
}
})
.on("queueEnd", queue => queue.metadata.channel.send(this.translate("music/play:QUEUE_ENDED", null, queue.metadata.channel.guild.data.language)))
.on("channelEmpty", queue => queue.metadata.channel.send(this.translate("music/play:STOP_EMPTY", null, queue.metadata.channel.guild.data.language)))
.on("connectionError", (queue, e) => {
});
this.player.on("queueEnd", queue => queue.metadata.channel.send(this.translate("music/play:QUEUE_ENDED", null, queue.metadata.channel.guild.data.language)));
this.player.on("channelEmpty", queue => queue.metadata.channel.send(this.translate("music/play:STOP_EMPTY", null, queue.metadata.channel.guild.data.language)));
this.player.on("connectionError", (queue, e) => {
console.error(e);
queue.metadata.channel.send({ content: this.translate("music/play:ERR_OCCURRED", { error: e.message }, queue.metadata.channel.guild.data.language) });
})
.on("error", (queue, e) => {
});
this.player.on("error", (queue, e) => {
console.error(e);
queue.metadata.channel.send({ content: this.translate("music/play:ERR_OCCURRED", { error: e.message }, queue.metadata.channel.guild.data.language) });
});

View file

@ -1,5 +1,5 @@
const { SlashCommandBuilder, ActionRowBuilder, SelectMenuBuilder } = require("discord.js"),
{ QueueRepeatMode } = require("discord-player");
{ QueueRepeatMode } = require("../../helpers/Music/dist/index");
const BaseCommand = require("../../base/BaseCommand");
class Loop extends BaseCommand {

View file

@ -1,5 +1,5 @@
const { SlashCommandBuilder, EmbedBuilder } = require("discord.js"),
{ QueueRepeatMode } = require("discord-player");
{ QueueRepeatMode } = require("../../helpers/Music/dist/index");
const BaseCommand = require("../../base/BaseCommand");
class Nowplaying extends BaseCommand {
@ -46,11 +46,24 @@ class Nowplaying extends BaseCommand {
.addFields([
{
name: interaction.translate("music/nowplaying:T_TITLE"),
value: `[${track.title}](${track.url})`
value: `[${track.title}](${track.url})`,
inline: true
},
{
name: interaction.translate("music/nowplaying:T_AUTHOR"),
value: track.author || interaction.translate("common:UNKNOWN")
value: track.author || interaction.translate("common:UNKNOWN"),
inline: true
},
{ name: "\u200B", value: "\u200B", inline: true },
{
name: interaction.translate("common:VIEWS"),
value: new Intl.NumberFormat(interaction.client.languages.find(language => language.name === interaction.guild.data.language).moment, { notation: "standard" }).format(track.views),
inline: true
},
{
name: interaction.translate("music/queue:ADDED"),
value: track.requestedBy.toString(),
inline: true
},
{
name: interaction.translate("music/nowplaying:T_DURATION"),

View file

@ -1,7 +1,6 @@
const { SlashCommandBuilder, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, PermissionsBitField } = require("discord.js"),
{ Track, Util } = require("discord-player");
const BaseCommand = require("../../base/BaseCommand"),
playdl = require("play-dl");
{ QueryType } = require("../../helpers/Music/dist/index");
const BaseCommand = require("../../base/BaseCommand");
class Play extends BaseCommand {
/**
@ -74,6 +73,7 @@ class Play extends BaseCommand {
return interaction.editReply({ content: interaction.translate("music/play:NO_RESULT", { query, error: "Unknown Error" }) });
}
} catch (error) {
console.log(error);
return interaction.editReply({
content: interaction.translate("music/play:NO_RESULT", {
query,
@ -82,23 +82,12 @@ class Play extends BaseCommand {
});
}
const queue = client.player.getQueue(interaction.guildId) || client.player.createQueue(interaction.guild, {
const queue = await client.player.getQueue(interaction.guildId) || client.player.createQueue(interaction.guild, {
metadata: { channel: interaction.channel },
autoSelfDeaf: true,
leaveOnEnd: true,
leaveOnStop: true,
bufferingTimeout: 1000,
disableVolume: false,
spotifyBridge: false,
/**
*
* @param {import("discord-player").Track} track
* @param {import("discord-player").TrackSource} source
* @param {import("discord-player").Queue} queue
*/
async onBeforeCreateStream(track, source) {
if (source === "youtube" || source === "soundcloud")
return (await playdl.stream(track.url, { discordPlayerCompatibility: true })).stream;
}
});
if (searchResult.searched) {
@ -163,7 +152,9 @@ class Play extends BaseCommand {
}))
.setColor(client.config.embed.color)
.setDescription(searchResult.tracks.map(track => {
const views = new Intl.NumberFormat(interaction.client.languages.find(language => language.name === interaction.guild.data.language).moment, {
var views;
if (track.raw.live) views = "🔴 LIVE";
else views = new Intl.NumberFormat(interaction.client.languages.find(language => language.name === interaction.guild.data.language).moment, {
notation: "compact", compactDisplay: "short"
}).format(track.views);

View file

@ -1,5 +1,5 @@
const { SlashCommandBuilder, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require("discord.js"),
{ QueueRepeatMode } = require("discord-player");
{ QueueRepeatMode } = require("../../helpers/Music/dist/index");
const BaseCommand = require("../../base/BaseCommand");
class Queue extends BaseCommand {
@ -151,7 +151,7 @@ class Queue extends BaseCommand {
/**
*
* @param {import("discord.js").ChatInputCommandInteraction} interaction
* @param {import("discord-player").Queue} queue
* @param {import("../../helpers/Music/dist/index").Queue} queue
* @returns
*/
function generateQueueEmbeds(interaction, queue) {

View file

@ -0,0 +1,4 @@
node_modules/
dist/
*.d.ts

View file

@ -0,0 +1,22 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"env": {
"node": true
},
"plugins": [
"@typescript-eslint"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
],
"rules": {
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/ban-ts-comment": "error",
"semi": "error",
"no-console": "error"
}
}

96
helpers/Music/dist/Player.d.ts vendored Normal file
View file

@ -0,0 +1,96 @@
import { Client, Collection, GuildResolvable } from "discord.js";
import { TypedEmitter as EventEmitter } from "tiny-typed-emitter";
import { Queue } from "./Structures/Queue";
import { VoiceUtils } from "./VoiceInterface/VoiceUtils";
import { PlayerEvents, PlayerOptions, SearchOptions, PlayerInitOptions, PlayerSearchResult, PlaylistInitData } from "./types/types";
import Track from "./Structures/Track";
import { Playlist } from "./Structures/Playlist";
import { ExtractorModel } from "./Structures/ExtractorModel";
declare class Player extends EventEmitter<PlayerEvents> {
readonly client: Client;
readonly options: PlayerInitOptions;
readonly queues: Collection<string, Queue<unknown>>;
readonly voiceUtils: VoiceUtils;
readonly extractors: Collection<string, ExtractorModel>;
requiredEvents: string[];
/**
* Creates new Discord Player
* @param {Client} client The Discord Client
* @param {PlayerInitOptions} [options] The player init options
*/
constructor(client: Client, options?: PlayerInitOptions);
/**
* Handles voice state update
* @param {VoiceState} oldState The old voice state
* @param {VoiceState} newState The new voice state
* @returns {void}
* @private
*/
private _handleVoiceState;
/**
* 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<T = unknown>(guild: GuildResolvable, queueInitOptions?: PlayerOptions & {
metadata?: T;
}): Queue<T>;
/**
* Returns the queue if available
* @param {GuildResolvable} guild The guild id
* @returns {Queue}
*/
getQueue<T = unknown>(guild: GuildResolvable): Queue<T>;
/**
* Deletes a queue and returns deleted queue object
* @param {GuildResolvable} guild The guild id to remove
* @returns {Queue}
*/
deleteQueue<T = unknown>(guild: GuildResolvable): Queue<T>;
/**
* @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<PlayerSearchResult>}
*/
search(query: string | Track, options: SearchOptions): Promise<PlayerSearchResult>;
/**
* 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}
*/
use(extractorName: string, extractor: ExtractorModel | any, force?: boolean): ExtractorModel;
/**
* Removes registered extractor
* @param {string} extractorName The extractor name
* @returns {ExtractorModel}
*/
unuse(extractorName: string): ExtractorModel;
/**
* Generates a report of the dependencies used by the `@discordjs/voice` module. Useful for debugging.
* @returns {string}
*/
scanDeps(): string;
emit<U extends keyof PlayerEvents>(eventName: U, ...args: Parameters<PlayerEvents[U]>): boolean;
/**
* Resolves queue
* @param {GuildResolvable|Queue} queueLike Queue like object
* @returns {Queue}
*/
resolveQueue<T>(queueLike: GuildResolvable | Queue): Queue<T>;
[Symbol.iterator](): Generator<Queue<unknown>, void, undefined>;
/**
* Creates `Playlist` instance
* @param data The data to initialize a playlist
*/
createPlaylist(data: PlaylistInitData): Playlist;
}
export { Player };

579
helpers/Music/dist/Player.js vendored Normal file
View file

@ -0,0 +1,579 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Player = void 0;
const tslib_1 = require("tslib");
const discord_js_1 = require("discord.js");
const tiny_typed_emitter_1 = require("tiny-typed-emitter");
const Queue_1 = require("./Structures/Queue");
const VoiceUtils_1 = require("./VoiceInterface/VoiceUtils");
const types_1 = require("./types/types");
const Track_1 = tslib_1.__importDefault(require("./Structures/Track"));
const play_dl_1 = tslib_1.__importDefault(require("play-dl"));
const spotify_url_info_1 = tslib_1.__importDefault(require("spotify-url-info"));
const QueryResolver_1 = require("./utils/QueryResolver");
const Util_1 = require("./utils/Util");
const PlayerError_1 = require("./Structures/PlayerError");
const Playlist_1 = require("./Structures/Playlist");
const ExtractorModel_1 = require("./Structures/ExtractorModel");
const voice_1 = require("@discordjs/voice");
class Player extends tiny_typed_emitter_1.TypedEmitter {
/**
* Creates new Discord Player
* @param {Client} client The Discord Client
* @param {PlayerInitOptions} [options] The player init options
*/
constructor(client, options = {}) {
super();
this.options = {
autoRegisterExtractor: true,
connectionTimeout: 20000
};
this.queues = new discord_js_1.Collection();
this.voiceUtils = new VoiceUtils_1.VoiceUtils();
this.extractors = new discord_js_1.Collection();
this.requiredEvents = ["error", "connectionError"];
/**
* The discord.js client
* @type {Client}
*/
this.client = client;
if (this.client?.options?.intents && !new discord_js_1.IntentsBitField(this.client?.options?.intents).has(discord_js_1.IntentsBitField.Flags.GuildVoiceStates)) {
throw new PlayerError_1.PlayerError('client is missing "GuildVoiceStates" intent');
}
/**
* The extractors collection
* @type {ExtractorModel}
*/
this.options = Object.assign(this.options, options);
this.client.on("voiceStateUpdate", this._handleVoiceState.bind(this));
if (this.options?.autoRegisterExtractor) {
let nv; // eslint-disable-line @typescript-eslint/no-explicit-any
if ((nv = Util_1.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
*/
_handleVoiceState(oldState, newState) {
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) {
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_1.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_1.Util.noop);
}
}
}
if (queue.connection && !newState.channelId && oldState.channelId === queue.connection.channel.id) {
if (!Util_1.Util.isVoiceEmpty(queue.connection.channel))
return;
const timeout = setTimeout(() => {
if (!Util_1.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_1.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_1.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_1.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);
}
}
}
/**
* 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, queueInitOptions = {}) {
guild = this.client.guilds.resolve(guild);
if (!guild)
throw new PlayerError_1.PlayerError("Unknown Guild", PlayerError_1.ErrorStatusCode.UNKNOWN_GUILD);
if (this.queues.has(guild.id))
return this.queues.get(guild.id);
const _meta = queueInitOptions.metadata;
delete queueInitOptions["metadata"];
queueInitOptions.volumeSmoothness ?? (queueInitOptions.volumeSmoothness = 0.08);
const queue = new Queue_1.Queue(this, guild, queueInitOptions);
queue.metadata = _meta;
this.queues.set(guild.id, queue);
return queue;
}
/**
* Returns the queue if available
* @param {GuildResolvable} guild The guild id
* @returns {Queue}
*/
getQueue(guild) {
guild = this.client.guilds.resolve(guild);
if (!guild)
throw new PlayerError_1.PlayerError("Unknown Guild", PlayerError_1.ErrorStatusCode.UNKNOWN_GUILD);
return this.queues.get(guild.id);
}
/**
* Deletes a queue and returns deleted queue object
* @param {GuildResolvable} guild The guild id to remove
* @returns {Queue}
*/
deleteQueue(guild) {
guild = this.client.guilds.resolve(guild);
if (!guild)
throw new PlayerError_1.PlayerError("Unknown Guild", PlayerError_1.ErrorStatusCode.UNKNOWN_GUILD);
const prev = this.getQueue(guild);
try {
prev.destroy();
}
catch { } // eslint-disable-line no-empty
this.queues.delete(guild.id);
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<PlayerSearchResult>}
*/
async search(query, options) {
if (query instanceof Track_1.default)
return { playlist: query.playlist || null, tracks: [query] };
if (!options)
throw new PlayerError_1.PlayerError("DiscordPlayer#search needs search options!", PlayerError_1.ErrorStatusCode.INVALID_ARG_TYPE);
options.requestedBy = this.client.users.resolve(options.requestedBy);
if (!("searchEngine" in options))
options.searchEngine = types_1.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_1.Playlist(this, {
...data.playlist,
tracks: []
});
const tracks = data.data.map((m) => new Track_1.default(this, {
...m,
requestedBy: options.requestedBy,
duration: Util_1.Util.buildTimeCode(Util_1.Util.parseMS(m.duration)),
playlist: playlist
}));
if (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_1.Playlist(this, {
...data.playlist,
tracks: []
});
const tracks = data.data.map((m) => new Track_1.default(this, {
...m,
requestedBy: options.requestedBy,
duration: Util_1.Util.buildTimeCode(Util_1.Util.parseMS(m.duration)),
playlist: playlist
}));
if (playlist)
playlist.tracks = tracks;
return { playlist: playlist, tracks: tracks };
}
}
const qt = options.searchEngine === types_1.QueryType.AUTO ? await QueryResolver_1.QueryResolver.resolve(query) : options.searchEngine;
switch (qt) {
case types_1.QueryType.YOUTUBE_VIDEO: {
const info = await play_dl_1.default.video_info(query).catch(Util_1.Util.noop);
if (!info)
return { playlist: null, tracks: [] };
const track = new Track_1.default(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,
thumbnail: Util_1.Util.last(info.video_details.thumbnails)?.url,
views: info.video_details.views || 0,
duration: Util_1.Util.buildTimeCode(Util_1.Util.parseMS(info.video_details.durationInSec * 1000)),
source: "youtube",
raw: info
});
return { playlist: null, tracks: [track] };
}
case types_1.QueryType.YOUTUBE_SEARCH: {
const videos = await play_dl_1.default.search(query, {
limit: 10,
source: { youtube: "video" }
}).catch(Util_1.Util.noop);
if (!videos)
return { playlist: null, tracks: [] };
const tracks = videos.map(m => {
m.source = "youtube"; // eslint-disable-line @typescript-eslint/no-explicit-any
return new Track_1.default(this, {
title: m.title,
description: m.description,
author: m.channel?.name,
url: m.url,
requestedBy: options.requestedBy,
thumbnail: Util_1.Util.last(m.thumbnails).url,
views: m.views,
duration: m.durationRaw,
source: "youtube",
raw: m
});
});
return { playlist: null, tracks, searched: true };
}
case types_1.QueryType.SOUNDCLOUD_TRACK:
case types_1.QueryType.SOUNDCLOUD_SEARCH: {
const result = await QueryResolver_1.QueryResolver.resolve(query) === types_1.QueryType.SOUNDCLOUD_TRACK ? [{ url: query }] : await play_dl_1.default.search(query, {
limit: 5,
source: { soundcloud: "tracks" }
}).catch(() => []);
if (!result || !result.length)
return { playlist: null, tracks: [] };
const res = [];
for (const r of result) {
const trackInfo = await play_dl_1.default.soundcloud(r.url).catch(Util_1.Util.noop);
if (!trackInfo)
continue;
const track = new Track_1.default(this, {
title: trackInfo.name,
url: trackInfo.url,
duration: Util_1.Util.buildTimeCode(Util_1.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);
}
return { playlist: null, tracks: res };
}
case types_1.QueryType.SPOTIFY_SONG: {
const spotifyData = await (0, spotify_url_info_1.default)(await Util_1.Util.getFetch())
.getData(query)
.catch(Util_1.Util.noop);
if (!spotifyData)
return { playlist: null, tracks: [] };
const spotifyTrack = new Track_1.default(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_1.Util.buildTimeCode(Util_1.Util.parseMS(spotifyData.duration_ms)),
views: 0,
requestedBy: options.requestedBy,
source: "spotify"
});
return { playlist: null, tracks: [spotifyTrack] };
}
case types_1.QueryType.SPOTIFY_PLAYLIST:
case types_1.QueryType.SPOTIFY_ALBUM: {
const spotifyPlaylist = await (0, spotify_url_info_1.default)(await Util_1.Util.getFetch())
.getData(query)
.catch(Util_1.Util.noop);
if (!spotifyPlaylist)
return { playlist: null, tracks: [] };
const playlist = new Playlist_1.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) => {
const data = new Track_1.default(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_1.Util.buildTimeCode(Util_1.Util.parseMS(m.duration_ms)),
views: 0,
requestedBy: options.requestedBy,
playlist,
source: "spotify"
});
return data;
});
}
else {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
playlist.tracks = spotifyPlaylist.tracks.items.map((m) => {
const data = new Track_1.default(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_1.Util.buildTimeCode(Util_1.Util.parseMS(m.track.duration_ms)),
views: 0,
requestedBy: options.requestedBy,
playlist,
source: "spotify"
});
return data;
});
}
return { playlist: playlist, tracks: playlist.tracks };
}
case types_1.QueryType.SOUNDCLOUD_PLAYLIST: {
const data = await play_dl_1.default.soundcloud(query).catch(Util_1.Util.noop);
if (!data)
return { playlist: null, tracks: [] };
const res = new Playlist_1.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}`,
url: data.url,
rawPlaylist: data
});
const songs = await data.all_tracks();
for (const song of songs) {
const track = new Track_1.default(this, {
title: song.name,
description: "",
author: song.publisher.name ?? "Unknown Publisher",
url: song.url,
thumbnail: song.thumbnail,
duration: Util_1.Util.buildTimeCode(Util_1.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 types_1.QueryType.YOUTUBE_PLAYLIST: {
const ytpl = await play_dl_1.default.playlist_info(query, { incomplete: true }).catch(Util_1.Util.noop);
if (!ytpl)
return { playlist: null, tracks: [] };
const playlist = new Playlist_1.Playlist(this, {
title: ytpl.title,
thumbnail: ytpl.thumbnail,
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_1.default(this, {
title: video.title,
description: video.description,
author: video.channel?.name,
url: video.url,
requestedBy: options.requestedBy,
thumbnail: Util_1.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: [] };
}
}
/**
* 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, extractor, force = false) {
if (!extractorName)
throw new PlayerError_1.PlayerError("Cannot use unknown extractor!", PlayerError_1.ErrorStatusCode.UNKNOWN_EXTRACTOR);
if (this.extractors.has(extractorName) && !force)
return this.extractors.get(extractorName);
if (extractor instanceof ExtractorModel_1.ExtractorModel) {
this.extractors.set(extractorName, extractor);
return extractor;
}
for (const method of ["validate", "getInfo"]) {
if (typeof extractor[method] !== "function")
throw new PlayerError_1.PlayerError("Invalid extractor data!", PlayerError_1.ErrorStatusCode.INVALID_EXTRACTOR);
}
const model = new ExtractorModel_1.ExtractorModel(extractorName, extractor);
this.extractors.set(model.name, model);
return model;
}
/**
* Removes registered extractor
* @param {string} extractorName The extractor name
* @returns {ExtractorModel}
*/
unuse(extractorName) {
if (!this.extractors.has(extractorName))
throw new PlayerError_1.PlayerError(`Cannot find extractor "${extractorName}"`, PlayerError_1.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 = (0, voice_1.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, ...args) {
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) {
return this.getQueue(queueLike instanceof Queue_1.Queue ? queueLike.guild : queueLike);
}
*[Symbol.iterator]() {
yield* Array.from(this.queues.values());
}
/**
* Creates `Playlist` instance
* @param data The data to initialize a playlist
*/
createPlaylist(data) {
return new Playlist_1.Playlist(this, data);
}
}
exports.Player = Player;

View file

@ -0,0 +1,29 @@
import { ExtractorModelData } from "../types/types";
declare class ExtractorModel {
name: string;
private _raw;
/**
* Model for raw Discord Player extractors
* @param {string} extractorName Name of the extractor
* @param {object} data Extractor object
*/
constructor(extractorName: string, data: any);
/**
* Method to handle requests from `Player.play()`
* @param {string} query Query to handle
* @returns {Promise<ExtractorModelData>}
*/
handle(query: string): Promise<ExtractorModelData>;
/**
* Method used by Discord Player to validate query with this extractor
* @param {string} query The query to validate
* @returns {boolean}
*/
validate(query: string): boolean;
/**
* The extractor version
* @type {string}
*/
get version(): string;
}
export { ExtractorModel };

View file

@ -0,0 +1,65 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ExtractorModel = void 0;
class ExtractorModel {
/**
* 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, data) {
/**
* 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 });
}
/**
* Method to handle requests from `Player.play()`
* @param {string} query Query to handle
* @returns {Promise<ExtractorModelData>}
*/
async handle(query) {
const data = await this._raw.getInfo(query);
if (!data)
return null;
return {
playlist: data.playlist ?? null,
data: data.info?.map((m) => ({
title: m.title,
duration: m.duration,
thumbnail: m.thumbnail,
engine: m.engine,
views: m.views,
author: m.author,
description: m.description,
url: m.url,
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) {
return Boolean(this._raw.validate(query));
}
/**
* The extractor version
* @type {string}
*/
get version() {
return this._raw.version ?? "0.0.0";
}
}
exports.ExtractorModel = ExtractorModel;

View file

@ -0,0 +1,31 @@
export declare enum ErrorStatusCode {
STREAM_ERROR = "StreamError",
AUDIO_PLAYER_ERROR = "AudioPlayerError",
PLAYER_ERROR = "PlayerError",
NO_AUDIO_RESOURCE = "NoAudioResource",
UNKNOWN_GUILD = "UnknownGuild",
INVALID_ARG_TYPE = "InvalidArgType",
UNKNOWN_EXTRACTOR = "UnknownExtractor",
INVALID_EXTRACTOR = "InvalidExtractor",
INVALID_CHANNEL_TYPE = "InvalidChannelType",
INVALID_TRACK = "InvalidTrack",
UNKNOWN_REPEAT_MODE = "UnknownRepeatMode",
TRACK_NOT_FOUND = "TrackNotFound",
NO_CONNECTION = "NoConnection",
DESTROYED_QUEUE = "DestroyedQueue"
}
export declare class PlayerError extends Error {
message: string;
statusCode: ErrorStatusCode;
createdAt: Date;
constructor(message: string, code?: ErrorStatusCode);
get createdTimestamp(): number;
valueOf(): ErrorStatusCode;
toJSON(): {
stack: string;
code: ErrorStatusCode;
message: string;
created: number;
};
toString(): string;
}

View file

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

View file

@ -0,0 +1,33 @@
import { Player } from "../Player";
import { Track } from "./Track";
import { PlaylistInitData, PlaylistJSON, TrackSource } from "../types/types";
declare class Playlist {
readonly player: Player;
tracks: Track[];
title: string;
description: string;
thumbnail: string;
type: "album" | "playlist";
source: TrackSource;
author: {
name: string;
url: string;
};
id: string;
url: string;
readonly rawPlaylist?: any;
/**
* Playlist constructor
* @param {Player} player The player
* @param {PlaylistInitData} data The data
*/
constructor(player: Player, data: PlaylistInitData);
[Symbol.iterator](): Generator<Track, void, undefined>;
/**
* JSON representation of this playlist
* @param {boolean} [withTracks=true] If it should build json with tracks
* @returns {PlaylistJSON}
*/
toJSON(withTracks?: boolean): PlaylistJSON;
}
export { Playlist };

View file

@ -0,0 +1,108 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Playlist = void 0;
class Playlist {
/**
* Playlist constructor
* @param {Player} player The player
* @param {PlaylistInitData} data The data
*/
constructor(player, data) {
/**
* 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 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 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 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 url
* @name Playlist#url
* @type {string}
*/
this.url = data.url;
/**
* The playlist title
* @type {string}
*/
this.title = data.title;
/**
* @name Playlist#rawPlaylist
* @type {any}
* @readonly
*/
}
*[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: []
};
if (withTracks)
payload.tracks = this.tracks.map((m) => m.toJSON(true));
return payload;
}
}
exports.Playlist = Playlist;

241
helpers/Music/dist/Structures/Queue.d.ts vendored Normal file
View file

@ -0,0 +1,241 @@
/// <reference types="node" />
/// <reference types="node" />
import { Collection, Guild, GuildChannelResolvable } from "discord.js";
import { Player } from "../Player";
import { StreamDispatcher } from "../VoiceInterface/StreamDispatcher";
import Track from "./Track";
import { PlayerOptions, PlayerProgressbarOptions, PlayOptions, QueueFilters, QueueRepeatMode, TrackSource } from "../types/types";
import type { Readable } from "stream";
declare class Queue<T = unknown> {
#private;
readonly guild: Guild;
readonly player: Player;
connection: StreamDispatcher;
tracks: Track[];
previousTracks: Track[];
options: PlayerOptions;
playing: boolean;
metadata?: T;
repeatMode: QueueRepeatMode;
readonly id: string;
private _streamTime;
_cooldownsTimeout: Collection<string, NodeJS.Timeout>;
private _activeFilters;
private _filtersUpdate;
onBeforeCreateStream: (track: Track, source: TrackSource, queue: Queue) => Promise<Readable | undefined>;
/**
* 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);
/**
* Returns current track
* @type {Track}
*/
get current(): Track;
/**
* If this queue is destroyed
* @type {boolean}
*/
get destroyed(): boolean;
/**
* Returns current track
* @returns {Track}
*/
nowPlaying(): Track;
/**
* Connects to a voice channel
* @param {GuildChannelResolvable} channel The voice/stage channel
* @returns {Promise<Queue>}
*/
connect(channel: GuildChannelResolvable): Promise<this>;
/**
* Destroys this queue
* @param {boolean} [disconnect=this.options.leaveOnStop] If it should leave on destroy
* @returns {void}
*/
destroy(disconnect?: boolean): void;
/**
* Skips current track
* @returns {boolean}
*/
skip(): boolean;
/**
* Adds single track to the queue
* @param {Track} track The track to add
* @returns {void}
*/
addTrack(track: Track): void;
/**
* Adds multiple tracks to the queue
* @param {Track[]} tracks Array of tracks to add
*/
addTracks(tracks: Track[]): void;
/**
* Sets paused state
* @param {boolean} paused The paused state
* @returns {boolean}
*/
setPaused(paused?: boolean): boolean;
/**
* Sets bitrate
* @param {number|auto} bitrate bitrate to set
* @returns {void}
*/
setBitrate(bitrate: number | "auto"): void;
/**
* Sets volume
* @param {number} amount The volume amount
* @returns {boolean}
*/
setVolume(amount: number): boolean;
/**
* Sets repeat mode
* @param {QueueRepeatMode} mode The repeat mode
* @returns {boolean}
*/
setRepeatMode(mode: QueueRepeatMode): boolean;
/**
* The current volume amount
* @type {number}
*/
get volume(): number;
set volume(amount: number);
/**
* The stream time of this queue
* @type {number}
*/
get streamTime(): number;
set streamTime(time: number);
/**
* Returns enabled filters
* @returns {AudioFilters}
*/
getFiltersEnabled(): (keyof QueueFilters)[];
/**
* Returns disabled filters
* @returns {AudioFilters}
*/
getFiltersDisabled(): (keyof QueueFilters)[];
/**
* Sets filters
* @param {QueueFilters} filters Queue filters
* @returns {Promise<void>}
*/
setFilters(filters?: QueueFilters): Promise<void>;
/**
* Seeks to the given time
* @param {number} position The position
* @returns {boolean}
*/
seek(position: number): Promise<boolean>;
/**
* Plays previous track
* @returns {Promise<void>}
*/
back(): Promise<void>;
/**
* Clear this queue
*/
clear(): void;
/**
* Stops the player
* @returns {void}
*/
stop(): void;
/**
* Shuffles this queue
* @returns {boolean}
*/
shuffle(): boolean;
/**
* Removes a track from the queue
* @param {Track|string|number} track The track to remove
* @returns {Track}
*/
remove(track: Track | string | number): Track;
/**
* 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): number;
/**
* Jumps to particular track
* @param {Track|number} track The track
* @returns {void}
*/
jump(track: Track | number): void;
/**
* Jumps to particular track, removing other tracks on the way
* @param {Track|number} track The track
* @returns {void}
*/
skipTo(track: Track | number): void;
/**
* 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?: number): void;
/**
* @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(): {
current: string;
end: string;
progress: number;
};
/**
* Creates progress bar string
* @param {PlayerProgressbarOptions} options The progress bar options
* @returns {string}
*/
createProgressBar(options?: PlayerProgressbarOptions): string;
/**
* Total duration
* @type {Number}
*/
get totalTime(): number;
/**
* 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<void>}
*/
play(src?: Track, options?: PlayOptions): Promise<void>;
/**
* Private method to handle autoplay
* @param {Track} track The source track to find its similar track for autoplay
* @returns {Promise<void>}
* @private
*/
private _handleAutoplay;
[Symbol.iterator](): Generator<Track, void, undefined>;
/**
* JSON representation of this queue
* @returns {object}
*/
toJSON(): {
id: string;
guild: string;
voiceChannel: string;
options: PlayerOptions;
tracks: import("../types/types").TrackJSON[];
};
/**
* String representation of this queue
* @returns {string}
*/
toString(): string;
}
export { Queue };

761
helpers/Music/dist/Structures/Queue.js vendored Normal file
View file

@ -0,0 +1,761 @@
"use strict";
var _Queue_instances, _Queue_lastVolume, _Queue_destroyed, _Queue_watchDestroyed, _Queue_getBufferingTimeout;
Object.defineProperty(exports, "__esModule", { value: true });
exports.Queue = void 0;
const tslib_1 = require("tslib");
const discord_js_1 = require("discord.js");
const Track_1 = tslib_1.__importDefault(require("./Track"));
const types_1 = require("../types/types");
const voice_1 = require("@discordjs/voice");
const play_dl_1 = tslib_1.__importDefault(require("play-dl"));
const Util_1 = require("../utils/Util");
const AudioFilters_1 = tslib_1.__importDefault(require("../utils/AudioFilters"));
const PlayerError_1 = require("./PlayerError");
const FFmpegStream_1 = require("../utils/FFmpegStream");
class Queue {
/**
* 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, guild, options = {}) {
_Queue_instances.add(this);
this.tracks = [];
this.previousTracks = [];
this.playing = false;
this.metadata = null;
this.repeatMode = 0;
this.id = discord_js_1.SnowflakeUtil.generate().toString();
this._streamTime = 0;
this._cooldownsTimeout = new discord_js_1.Collection();
this._activeFilters = []; // eslint-disable-line @typescript-eslint/no-explicit-any
this._filtersUpdate = false;
_Queue_lastVolume.set(this, 0);
_Queue_destroyed.set(this, false);
this.onBeforeCreateStream = null;
/**
* 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,
ytdlOptions: {
highWaterMark: 1 << 25
},
initialVolume: 100,
bufferingTimeout: 3000,
spotifyBridge: true,
disableVolume: false
}, 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 (tslib_1.__classPrivateFieldGet(this, _Queue_instances, "m", _Queue_watchDestroyed).call(this))
return;
return this.connection.audioResource?.metadata ?? this.tracks[0];
}
/**
* If this queue is destroyed
* @type {boolean}
*/
get destroyed() {
return tslib_1.__classPrivateFieldGet(this, _Queue_destroyed, "f");
}
/**
* Returns current track
* @returns {Track}
*/
nowPlaying() {
if (tslib_1.__classPrivateFieldGet(this, _Queue_instances, "m", _Queue_watchDestroyed).call(this))
return;
return this.current;
}
/**
* Connects to a voice channel
* @param {GuildChannelResolvable} channel The voice/stage channel
* @returns {Promise<Queue>}
*/
async connect(channel) {
if (tslib_1.__classPrivateFieldGet(this, _Queue_instances, "m", _Queue_watchDestroyed).call(this))
return;
const _channel = this.guild.channels.resolve(channel);
if (![discord_js_1.ChannelType.GuildStageVoice, discord_js_1.ChannelType.GuildVoice].includes(_channel?.type))
throw new PlayerError_1.PlayerError(`Channel type must be GuildVoice or GuildStageVoice, got ${_channel?.type}!`, PlayerError_1.ErrorStatusCode.INVALID_ARG_TYPE);
const connection = await this.player.voiceUtils.connect(_channel, {
deaf: this.options.autoSelfDeaf
});
this.connection = connection;
if (_channel.type === discord_js_1.ChannelType.GuildStageVoice) {
await _channel.guild.members.me.voice.setSuppressed(false).catch(async () => {
return await _channel.guild.members.me.voice.setRequestToSpeak(true).catch(Util_1.Util.noop);
});
}
this.connection.on("error", (err) => {
if (tslib_1.__classPrivateFieldGet(this, _Queue_instances, "m", _Queue_watchDestroyed).call(this, false))
return;
this.player.emit("connectionError", this, err);
});
this.connection.on("debug", (msg) => {
if (tslib_1.__classPrivateFieldGet(this, _Queue_instances, "m", _Queue_watchDestroyed).call(this, false))
return;
this.player.emit("debug", this, msg);
});
this.player.emit("connectionCreate", this, this.connection);
this.connection.on("start", (resource) => {
if (tslib_1.__classPrivateFieldGet(this, _Queue_instances, "m", _Queue_watchDestroyed).call(this, 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 (tslib_1.__classPrivateFieldGet(this, _Queue_instances, "m", _Queue_watchDestroyed).call(this, 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 === types_1.QueueRepeatMode.OFF) {
if (this.options.leaveOnEnd)
this.destroy();
this.player.emit("queueEnd", this);
}
else if (!this.tracks.length && this.repeatMode === types_1.QueueRepeatMode.AUTOPLAY) {
this._handleAutoplay(Util_1.Util.last(this.previousTracks));
}
else {
if (this.repeatMode === types_1.QueueRepeatMode.TRACK)
return void this.play(Util_1.Util.last(this.previousTracks), { immediate: true });
if (this.repeatMode === types_1.QueueRepeatMode.QUEUE)
this.tracks.push(Util_1.Util.last(this.previousTracks));
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 (tslib_1.__classPrivateFieldGet(this, _Queue_instances, "m", _Queue_watchDestroyed).call(this))
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);
tslib_1.__classPrivateFieldSet(this, _Queue_destroyed, true, "f");
}
/**
* Skips current track
* @returns {boolean}
*/
skip() {
if (tslib_1.__classPrivateFieldGet(this, _Queue_instances, "m", _Queue_watchDestroyed).call(this))
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) {
if (tslib_1.__classPrivateFieldGet(this, _Queue_instances, "m", _Queue_watchDestroyed).call(this))
return;
if (!(track instanceof Track_1.default))
throw new PlayerError_1.PlayerError("invalid track", PlayerError_1.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) {
if (tslib_1.__classPrivateFieldGet(this, _Queue_instances, "m", _Queue_watchDestroyed).call(this))
return;
if (!tracks.every((y) => y instanceof Track_1.default))
throw new PlayerError_1.PlayerError("invalid track", PlayerError_1.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) {
if (tslib_1.__classPrivateFieldGet(this, _Queue_instances, "m", _Queue_watchDestroyed).call(this))
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) {
if (tslib_1.__classPrivateFieldGet(this, _Queue_instances, "m", _Queue_watchDestroyed).call(this))
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) {
if (tslib_1.__classPrivateFieldGet(this, _Queue_instances, "m", _Queue_watchDestroyed).call(this))
return;
if (!this.connection)
return false;
tslib_1.__classPrivateFieldSet(this, _Queue_lastVolume, amount, "f");
this.options.initialVolume = amount;
return this.connection.setVolume(amount);
}
/**
* Sets repeat mode
* @param {QueueRepeatMode} mode The repeat mode
* @returns {boolean}
*/
setRepeatMode(mode) {
if (tslib_1.__classPrivateFieldGet(this, _Queue_instances, "m", _Queue_watchDestroyed).call(this))
return;
if (![types_1.QueueRepeatMode.OFF, types_1.QueueRepeatMode.QUEUE, types_1.QueueRepeatMode.TRACK, types_1.QueueRepeatMode.AUTOPLAY].includes(mode))
throw new PlayerError_1.PlayerError(`Unknown repeat mode "${mode}"!`, PlayerError_1.ErrorStatusCode.UNKNOWN_REPEAT_MODE);
if (mode === this.repeatMode)
return false;
this.repeatMode = mode;
return true;
}
/**
* The current volume amount
* @type {number}
*/
get volume() {
if (tslib_1.__classPrivateFieldGet(this, _Queue_instances, "m", _Queue_watchDestroyed).call(this))
return;
if (!this.connection)
return 100;
return this.connection.volume;
}
set volume(amount) {
this.setVolume(amount);
}
/**
* The stream time of this queue
* @type {number}
*/
get streamTime() {
if (tslib_1.__classPrivateFieldGet(this, _Queue_instances, "m", _Queue_watchDestroyed).call(this))
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) {
if (tslib_1.__classPrivateFieldGet(this, _Queue_instances, "m", _Queue_watchDestroyed).call(this))
return;
this.seek(time);
}
/**
* Returns enabled filters
* @returns {AudioFilters}
*/
getFiltersEnabled() {
if (tslib_1.__classPrivateFieldGet(this, _Queue_instances, "m", _Queue_watchDestroyed).call(this))
return;
return AudioFilters_1.default.names.filter((x) => this._activeFilters.includes(x));
}
/**
* Returns disabled filters
* @returns {AudioFilters}
*/
getFiltersDisabled() {
if (tslib_1.__classPrivateFieldGet(this, _Queue_instances, "m", _Queue_watchDestroyed).call(this))
return;
return AudioFilters_1.default.names.filter((x) => !this._activeFilters.includes(x));
}
/**
* Sets filters
* @param {QueueFilters} filters Queue filters
* @returns {Promise<void>}
*/
async setFilters(filters) {
if (tslib_1.__classPrivateFieldGet(this, _Queue_instances, "m", _Queue_watchDestroyed).call(this))
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 = []; // eslint-disable-line @typescript-eslint/no-explicit-any
for (const filter in filters) {
if (filters[filter] === true)
_filters.push(filter);
}
if (this._activeFilters.join("") === _filters.join(""))
return;
const newFilters = AudioFilters_1.default.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) {
if (tslib_1.__classPrivateFieldGet(this, _Queue_instances, "m", _Queue_watchDestroyed).call(this))
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,
seek: position
});
return true;
}
/**
* Plays previous track
* @returns {Promise<void>}
*/
async back() {
if (tslib_1.__classPrivateFieldGet(this, _Queue_instances, "m", _Queue_watchDestroyed).call(this))
return;
const prev = this.previousTracks[this.previousTracks.length - 2]; // because last item is the current track
if (!prev)
throw new PlayerError_1.PlayerError("Could not find previous track", PlayerError_1.ErrorStatusCode.TRACK_NOT_FOUND);
return await this.play(prev, { immediate: true });
}
/**
* Clear this queue
*/
clear() {
if (tslib_1.__classPrivateFieldGet(this, _Queue_instances, "m", _Queue_watchDestroyed).call(this))
return;
this.tracks = [];
this.previousTracks = [];
}
/**
* Stops the player
* @returns {void}
*/
stop() {
if (tslib_1.__classPrivateFieldGet(this, _Queue_instances, "m", _Queue_watchDestroyed).call(this))
return;
return this.destroy();
}
/**
* Shuffles this queue
* @returns {boolean}
*/
shuffle() {
if (tslib_1.__classPrivateFieldGet(this, _Queue_instances, "m", _Queue_watchDestroyed).call(this))
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) {
if (tslib_1.__classPrivateFieldGet(this, _Queue_instances, "m", _Queue_watchDestroyed).call(this))
return;
let trackFound = 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_1.default ? 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) {
if (tslib_1.__classPrivateFieldGet(this, _Queue_instances, "m", _Queue_watchDestroyed).call(this))
return;
if (typeof track === "number")
return this.tracks[track] != null ? track : -1;
return this.tracks.findIndex((pred) => pred.id === (track instanceof Track_1.default ? track.id : track));
}
/**
* Jumps to particular track
* @param {Track|number} track The track
* @returns {void}
*/
jump(track) {
if (tslib_1.__classPrivateFieldGet(this, _Queue_instances, "m", _Queue_watchDestroyed).call(this))
return;
const foundTrack = this.remove(track);
if (!foundTrack)
throw new PlayerError_1.PlayerError("Track not found", PlayerError_1.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) {
if (tslib_1.__classPrivateFieldGet(this, _Queue_instances, "m", _Queue_watchDestroyed).call(this))
return;
const trackIndex = this.getTrackPosition(track);
const removedTrack = this.remove(track);
if (!removedTrack)
throw new PlayerError_1.PlayerError("Track not found", PlayerError_1.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, index = 0) {
if (tslib_1.__classPrivateFieldGet(this, _Queue_instances, "m", _Queue_watchDestroyed).call(this))
return;
if (!track || !(track instanceof Track_1.default))
throw new PlayerError_1.PlayerError("track must be the instance of Track", PlayerError_1.ErrorStatusCode.INVALID_TRACK);
if (typeof index !== "number" || index < 0 || !Number.isFinite(index))
throw new PlayerError_1.PlayerError(`Invalid index "${index}"`, PlayerError_1.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 (tslib_1.__classPrivateFieldGet(this, _Queue_instances, "m", _Queue_watchDestroyed).call(this))
return;
const currentStreamTime = this.streamTime;
const totalTime = this.current.durationMS;
const currentTimecode = Util_1.Util.buildTimeCode(Util_1.Util.parseMS(currentStreamTime));
const endTimecode = Util_1.Util.buildTimeCode(Util_1.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 = { timecodes: true }) {
if (tslib_1.__classPrivateFieldGet(this, _Queue_instances, "m", _Queue_watchDestroyed).call(this))
return;
const length = typeof options.length === "number" ? (options.length <= 0 || options.length === Infinity ? 15 : options.length) : 15;
const index = Math.round((this.streamTime / this.current.durationMS) * length);
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() {
if (tslib_1.__classPrivateFieldGet(this, _Queue_instances, "m", _Queue_watchDestroyed).call(this))
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<void>}
*/
async play(src, options = {}) {
if (tslib_1.__classPrivateFieldGet(this, _Queue_instances, "m", _Queue_watchDestroyed).call(this, false))
return;
if (!this.connection || !this.connection.voiceConnection)
throw new PlayerError_1.PlayerError("Voice connection is not available, use <Queue>.connect()!", PlayerError_1.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);
this.previousTracks.push(track);
}
let stream = null;
const hasCustomDownloader = typeof this.onBeforeCreateStream === "function";
if (["youtube", "spotify"].includes(track.raw.source)) {
let spotifyResolved = false;
if (this.options.spotifyBridge && track.raw.source === "spotify" && !track.raw.engine) {
track.raw.engine = await play_dl_1.default.search(`${track.author} ${track.title}`, { source: { youtube: "video" } })
.then(res => res[0].url)
.catch(() => null);
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_dl_1.default.stream(url, { discordPlayerCompatibility: true })).stream;
}
}
else {
const arbitraryStream = (hasCustomDownloader && (await this.onBeforeCreateStream(track, track.raw.source || track.raw.engine, this))) || null;
stream =
arbitraryStream || (track.raw.source === "soundcloud" && typeof track.raw.engine?.downloadProgressive === "function")
? await track.raw.engine.downloadProgressive()
: typeof track.raw.engine === "function"
? await track.raw.engine()
: track.raw.engine;
}
const ffmpegStream = (0, FFmpegStream_1.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 = this.connection.createStream(ffmpegStream, {
type: voice_1.StreamType.Raw,
data: track,
disableVolume: Boolean(this.options.disableVolume)
});
if (options.seek)
this._streamTime = options.seek;
this._filtersUpdate = options.filtersUpdate;
const volumeTransformer = resource.volume;
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);
}, tslib_1.__classPrivateFieldGet(this, _Queue_instances, "m", _Queue_getBufferingTimeout).call(this)).unref();
}
/**
* Private method to handle autoplay
* @param {Track} track The source track to find its similar track for autoplay
* @returns {Promise<void>}
* @private
*/
async _handleAutoplay(track) {
if (tslib_1.__classPrivateFieldGet(this, _Queue_instances, "m", _Queue_watchDestroyed).call(this))
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_dl_1.default.video_info(track.url)
.catch(Util_1.Util.noop);
if (!info) {
if (this.options.leaveOnEnd)
this.destroy();
return void this.player.emit("queueEnd", this);
}
const randomRelated = await play_dl_1.default.video_info(info.related_videos[0]);
const nextTrack = new Track_1.default(this.player, {
title: randomRelated.video_details.title,
url: randomRelated.video_details.url,
duration: randomRelated.video_details.durationRaw ? Util_1.Util.buildTimeCode(Util_1.Util.parseMS(randomRelated.video_details.durationInSec * 1000)) : "0:00",
description: "",
thumbnail: Util_1.Util.last(randomRelated.video_details.thumbnails).url,
views: randomRelated.video_details.views,
author: randomRelated.video_details.channel.name,
requestedBy: track.requestedBy,
source: "youtube"
});
this.play(nextTrack, { immediate: true });
}
*[(_Queue_lastVolume = new WeakMap(), _Queue_destroyed = new WeakMap(), _Queue_instances = new WeakSet(), Symbol.iterator)]() {
if (tslib_1.__classPrivateFieldGet(this, _Queue_instances, "m", _Queue_watchDestroyed).call(this))
return;
yield* this.tracks;
}
/**
* JSON representation of this queue
* @returns {object}
*/
toJSON() {
if (tslib_1.__classPrivateFieldGet(this, _Queue_instances, "m", _Queue_watchDestroyed).call(this))
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 (tslib_1.__classPrivateFieldGet(this, _Queue_instances, "m", _Queue_watchDestroyed).call(this))
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")}`;
}
}
exports.Queue = Queue;
_Queue_watchDestroyed = function _Queue_watchDestroyed(emit = true) {
if (tslib_1.__classPrivateFieldGet(this, _Queue_destroyed, "f")) {
if (emit)
this.player.emit("error", this, new PlayerError_1.PlayerError("Cannot use destroyed queue", PlayerError_1.ErrorStatusCode.DESTROYED_QUEUE));
return true;
}
return false;
}, _Queue_getBufferingTimeout = function _Queue_getBufferingTimeout() {
const timeout = this.options.bufferingTimeout;
if (isNaN(timeout) || timeout < 0 || !Number.isFinite(timeout))
return 1000;
return timeout;
};

View file

@ -0,0 +1,53 @@
import { User } from "discord.js";
import { Player } from "../Player";
import { RawTrackData, TrackJSON } from "../types/types";
import { Playlist } from "./Playlist";
import { Queue } from "./Queue";
declare class Track {
player: Player;
title: string;
description: string;
author: string;
url: string;
thumbnail: string;
duration: string;
views: number;
requestedBy: User;
playlist?: Playlist;
readonly raw: RawTrackData;
readonly id: string;
/**
* Track constructor
* @param {Player} player The player that instantiated this Track
* @param {RawTrackData} data Track data
*/
constructor(player: Player, data: RawTrackData);
private _patch;
/**
* The queue in which this track is located
* @type {Queue}
*/
get queue(): Queue;
/**
* The track duration in millisecond
* @type {number}
*/
get durationMS(): number;
/**
* Returns source of this track
* @type {TrackSource}
*/
get source(): import("../types/types").TrackSource;
/**
* String representation of this track
* @returns {string}
*/
toString(): string;
/**
* Raw JSON representation of this track
* @returns {TrackJSON}
*/
toJSON(hidePlaylist?: boolean): TrackJSON;
}
export default Track;
export { Track };

156
helpers/Music/dist/Structures/Track.js vendored Normal file
View file

@ -0,0 +1,156 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Track = void 0;
const discord_js_1 = require("discord.js");
class Track {
/**
* Track constructor
* @param {Player} player The player that instantiated this Track
* @param {RawTrackData} data Track data
*/
constructor(player, data) {
this.raw = {};
this.id = discord_js_1.SnowflakeUtil.generate().toString();
/**
* 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}
*/
/**
* Description of this track
* @name Track#description
* @type {string}
*/
/**
* Author of this track
* @name Track#author
* @type {string}
*/
/**
* URL of this track
* @name Track#url
* @type {string}
*/
/**
* Thumbnail of this track
* @name Track#thumbnail
* @type {string}
*/
/**
* Duration of this track
* @name Track#duration
* @type {string}
*/
/**
* Views count of this track
* @name Track#views
* @type {number}
*/
/**
* Person who requested this track
* @name Track#requestedBy
* @type {User}
*/
/**
* If this track belongs to playlist
* @name Track#fromPlaylist
* @type {boolean}
*/
/**
* Raw track data
* @name Track#raw
* @type {RawTrackData}
*/
/**
* The track id
* @name Track#id
* @type {Snowflake}
* @readonly
*/
/**
* The playlist which track belongs
* @name Track#playlist
* @type {Playlist}
*/
void this._patch(data);
}
_patch(data) {
this.title = (0, discord_js_1.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 });
}
/**
* The queue in which this track is located
* @type {Queue}
*/
get queue() {
return this.player.queues.find((q) => q.tracks.some((ab) => ab.id === this.id));
}
/**
* The track duration in millisecond
* @type {number}
*/
get durationMS() {
const times = (n, t) => {
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);
}
/**
* Returns source of this track
* @type {TrackSource}
*/
get source() {
return this.raw.source ?? "arbitrary";
}
/**
* String representation of this track
* @returns {string}
*/
toString() {
return `${this.title} by ${this.author}`;
}
/**
* Raw JSON representation of this track
* @returns {TrackJSON}
*/
toJSON(hidePlaylist) {
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
};
}
}
exports.Track = Track;
exports.default = Track;

View file

@ -0,0 +1,88 @@
/// <reference types="node" />
import { AudioPlayer, AudioPlayerError, AudioPlayerStatus, AudioResource, StreamType, VoiceConnection } from "@discordjs/voice";
import { StageChannel, VoiceChannel } from "discord.js";
import { Duplex, Readable } from "stream";
import { TypedEmitter as EventEmitter } from "tiny-typed-emitter";
import Track from "../Structures/Track";
export interface VoiceEvents {
error: (error: AudioPlayerError) => any;
debug: (message: string) => any;
start: (resource: AudioResource<Track>) => any;
finish: (resource: AudioResource<Track>) => any;
}
declare class StreamDispatcher extends EventEmitter<VoiceEvents> {
readonly connectionTimeout: number;
readonly voiceConnection: VoiceConnection;
readonly audioPlayer: AudioPlayer;
channel: VoiceChannel | StageChannel;
audioResource?: AudioResource<Track>;
private readyLock;
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, connectionTimeout?: number);
/**
* Creates stream
* @param {Readable|Duplex|string} src The stream source
* @param {object} [ops] Options
* @returns {AudioResource}
*/
createStream(src: Readable | Duplex | string, ops?: {
type?: StreamType;
data?: any;
disableVolume?: boolean;
}): AudioResource<Track>;
/**
* The player status
* @type {AudioPlayerStatus}
*/
get status(): AudioPlayerStatus;
/**
* Disconnects from voice
* @returns {void}
*/
disconnect(): void;
/**
* Stops the player
* @returns {void}
*/
end(): void;
/**
* 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): boolean;
/**
* Resumes the stream playback
* @returns {boolean}
*/
resume(): boolean;
/**
* Play stream
* @param {AudioResource<Track>} [resource=this.audioResource] The audio resource to play
* @returns {Promise<StreamDispatcher>}
*/
playStream(resource?: AudioResource<Track>): Promise<this>;
/**
* Sets playback volume
* @param {number} value The volume amount
* @returns {boolean}
*/
setVolume(value: number): boolean;
/**
* The current volume
* @type {number}
*/
get volume(): number;
/**
* The playback time
* @type {number}
*/
get streamTime(): number;
}
export { StreamDispatcher as StreamDispatcher };

View file

@ -0,0 +1,225 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.StreamDispatcher = void 0;
const voice_1 = require("@discordjs/voice");
const tiny_typed_emitter_1 = require("tiny-typed-emitter");
const Util_1 = require("../utils/Util");
const PlayerError_1 = require("../Structures/PlayerError");
class StreamDispatcher extends tiny_typed_emitter_1.TypedEmitter {
/**
* Creates new connection object
* @param {VoiceConnection} connection The connection
* @param {VoiceChannel|StageChannel} channel The connected channel
* @private
*/
constructor(connection, channel, connectionTimeout = 20000) {
super();
this.connectionTimeout = connectionTimeout;
this.readyLock = false;
/**
* The voice connection
* @type {VoiceConnection}
*/
this.voiceConnection = connection;
/**
* The audio player
* @type {AudioPlayer}
*/
this.audioPlayer = (0, voice_1.createAudioPlayer)();
/**
* The voice channel
* @type {VoiceChannel|StageChannel}
*/
this.channel = channel;
/**
* The paused state
* @type {boolean}
*/
this.paused = false;
this.voiceConnection.on("stateChange", async (_, newState) => {
if (newState.status === voice_1.VoiceConnectionStatus.Disconnected) {
if (newState.reason === voice_1.VoiceConnectionDisconnectReason.WebSocketClose && newState.closeCode === 4014) {
try {
await (0, voice_1.entersState)(this.voiceConnection, voice_1.VoiceConnectionStatus.Connecting, this.connectionTimeout);
}
catch {
try {
this.voiceConnection.destroy();
}
catch (err) {
this.emit("error", err);
}
}
}
else if (this.voiceConnection.rejoinAttempts < 5) {
await Util_1.Util.wait((this.voiceConnection.rejoinAttempts + 1) * 5000);
this.voiceConnection.rejoin();
}
else {
try {
this.voiceConnection.destroy();
}
catch (err) {
this.emit("error", err);
}
}
}
else if (newState.status === voice_1.VoiceConnectionStatus.Destroyed) {
this.end();
}
else if (!this.readyLock && (newState.status === voice_1.VoiceConnectionStatus.Connecting || newState.status === voice_1.VoiceConnectionStatus.Signalling)) {
this.readyLock = true;
try {
await (0, voice_1.entersState)(this.voiceConnection, voice_1.VoiceConnectionStatus.Ready, this.connectionTimeout);
}
catch {
if (this.voiceConnection.state.status !== voice_1.VoiceConnectionStatus.Destroyed) {
try {
this.voiceConnection.destroy();
}
catch (err) {
this.emit("error", err);
}
}
}
finally {
this.readyLock = false;
}
}
});
this.audioPlayer.on("stateChange", (oldState, newState) => {
if (newState.status === voice_1.AudioPlayerStatus.Playing) {
if (!this.paused)
return void this.emit("start", this.audioResource);
}
else if (newState.status === voice_1.AudioPlayerStatus.Idle && oldState.status !== voice_1.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);
}
/**
* 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, ops) {
this.audioResource = (0, voice_1.createAudioResource)(src, {
inputType: ops?.type ?? voice_1.StreamType.Arbitrary,
metadata: ops?.data,
// eslint-disable-next-line no-extra-boolean-cast
inlineVolume: !Boolean(ops?.disableVolume)
});
return this.audioResource;
}
/**
* 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
}
/**
* 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) {
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;
}
/**
* Play stream
* @param {AudioResource<Track>} [resource=this.audioResource] The audio resource to play
* @returns {Promise<StreamDispatcher>}
*/
async playStream(resource = this.audioResource) {
if (!resource)
throw new PlayerError_1.PlayerError("Audio resource is not available!", PlayerError_1.ErrorStatusCode.NO_AUDIO_RESOURCE);
if (resource.ended)
return void this.emit("error", new PlayerError_1.PlayerError("Cannot play a resource that has already ended."));
if (!this.audioResource)
this.audioResource = resource;
if (this.voiceConnection.state.status !== voice_1.VoiceConnectionStatus.Ready) {
try {
await (0, voice_1.entersState)(this.voiceConnection, voice_1.VoiceConnectionStatus.Ready, this.connectionTimeout);
}
catch (err) {
return void this.emit("error", err);
}
}
try {
this.audioPlayer.play(resource);
}
catch (e) {
this.emit("error", e);
}
return this;
}
/**
* Sets playback volume
* @param {number} value The volume amount
* @returns {boolean}
*/
setVolume(value) {
if (!this.audioResource?.volume || isNaN(value) || value < 0 || value > Infinity)
return false;
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 playback time
* @type {number}
*/
get streamTime() {
if (!this.audioResource)
return 0;
return this.audioResource.playbackDuration;
}
}
exports.StreamDispatcher = StreamDispatcher;

View file

@ -0,0 +1,44 @@
import { VoiceChannel, StageChannel, Collection, Snowflake } from "discord.js";
import { VoiceConnection } from "@discordjs/voice";
import { StreamDispatcher } from "./StreamDispatcher";
declare class VoiceUtils {
cache: Collection<Snowflake, StreamDispatcher>;
/**
* The voice utils
* @private
*/
constructor();
/**
* Joins a voice channel, creating basic stream dispatch manager
* @param {StageChannel|VoiceChannel} channel The voice channel
* @param {object} [options] Join options
* @returns {Promise<StreamDispatcher>}
*/
connect(channel: VoiceChannel | StageChannel, options?: {
deaf?: boolean;
maxTime?: number;
}): Promise<StreamDispatcher>;
/**
* Joins a voice channel
* @param {StageChannel|VoiceChannel} [channel] The voice/stage channel to join
* @param {object} [options] Join options
* @returns {VoiceConnection}
*/
join(channel: VoiceChannel | StageChannel, options?: {
deaf?: boolean;
maxTime?: number;
}): Promise<VoiceConnection>;
/**
* Disconnects voice connection
* @param {VoiceConnection} connection The voice connection
* @returns {void}
*/
disconnect(connection: VoiceConnection | StreamDispatcher): void;
/**
* Returns Discord Player voice connection
* @param {Snowflake} guild The guild id
* @returns {StreamDispatcher}
*/
getConnection(guild: Snowflake): StreamDispatcher;
}
export { VoiceUtils };

View file

@ -0,0 +1,65 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.VoiceUtils = void 0;
const discord_js_1 = require("discord.js");
const voice_1 = require("@discordjs/voice");
const StreamDispatcher_1 = require("./StreamDispatcher");
class VoiceUtils {
/**
* The voice utils
* @private
*/
constructor() {
/**
* The cache where voice utils stores stream managers
* @type {Collection<Snowflake, StreamDispatcher>}
*/
this.cache = new discord_js_1.Collection();
}
/**
* Joins a voice channel, creating basic stream dispatch manager
* @param {StageChannel|VoiceChannel} channel The voice channel
* @param {object} [options] Join options
* @returns {Promise<StreamDispatcher>}
*/
async connect(channel, options) {
const conn = await this.join(channel, options);
const sub = new StreamDispatcher_1.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}
*/
async join(channel, options) {
const conn = (0, voice_1.joinVoiceChannel)({
guildId: channel.guild.id,
channelId: channel.id,
adapterCreator: channel.guild.voiceAdapterCreator,
selfDeaf: Boolean(options.deaf)
});
return conn;
}
/**
* Disconnects voice connection
* @param {VoiceConnection} connection The voice connection
* @returns {void}
*/
disconnect(connection) {
if (connection instanceof StreamDispatcher_1.StreamDispatcher)
return connection.voiceConnection.destroy();
return connection.destroy();
}
/**
* Returns Discord Player voice connection
* @param {Snowflake} guild The guild id
* @returns {StreamDispatcher}
*/
getConnection(guild) {
return this.cache.get(guild);
}
}
exports.VoiceUtils = VoiceUtils;

View file

@ -0,0 +1,34 @@
/// <reference types="node" />
/// <reference types="node" />
import { Transform, TransformOptions } from "stream";
export interface VolumeTransformerOptions extends TransformOptions {
type?: "s16le" | "s16be" | "s32le" | "s32be";
smoothness?: number;
volume?: number;
}
export declare class VolumeTransformer extends Transform {
private _bits;
private _smoothing;
private _bytes;
private _extremum;
private _chunk;
volume: number;
private _targetVolume;
type: "s16le" | "s32le" | "s16be" | "s32be";
constructor(options?: VolumeTransformerOptions);
_readInt(buffer: Buffer, index: number): number;
_writeInt(buffer: Buffer, int: number, index: number): number;
_applySmoothness(): void;
_transform(chunk: Buffer, encoding: BufferEncoding, done: () => unknown): unknown;
_destroy(err: Error, cb: (error: Error) => void): void;
setVolume(volume: number): void;
setVolumeDecibels(db: number): void;
setVolumeLogarithmic(value: number): void;
get volumeDecibels(): number;
get volumeLogarithmic(): number;
get smoothness(): number;
setSmoothness(smoothness: number): void;
smoothingEnabled(): boolean;
get hasSmoothness(): boolean;
static get hasSmoothing(): boolean;
}

View file

@ -0,0 +1,120 @@
"use strict";
// prism's volume transformer with smooth volume support
Object.defineProperty(exports, "__esModule", { value: true });
exports.VolumeTransformer = void 0;
const stream_1 = require("stream");
class VolumeTransformer extends stream_1.Transform {
constructor(options = {}) {
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, index) {
return index;
}
_writeInt(buffer, int, index) {
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;
}
}
_transform(chunk, encoding, done) {
if (this.smoothingEnabled() && this.volume !== this._targetVolume)
this._applySmoothness();
if (this.volume === 1) {
this.push(chunk);
return done();
}
const { _bytes, _extremum } = this;
chunk = this._chunk = Buffer.concat([this._chunk, chunk]);
if (chunk.length < _bytes)
return done();
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);
}
this._chunk = chunk.slice(complete);
this.push(chunk.slice(0, complete));
return done();
}
_destroy(err, cb) {
super._destroy(err, cb);
this._chunk = null;
}
setVolume(volume) {
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) {
this.setVolume(Math.pow(10, db / 20));
}
setVolumeLogarithmic(value) {
this.setVolume(Math.pow(value, 1.660964));
}
get volumeDecibels() {
return Math.log10(this.volume) * 20;
}
get volumeLogarithmic() {
return Math.pow(this.volume, 1 / 1.660964);
}
get smoothness() {
return this._smoothing;
}
setSmoothness(smoothness) {
this._smoothing = smoothness;
}
smoothingEnabled() {
return Number.isFinite(this._smoothing) && this._smoothing > 0;
}
get hasSmoothness() {
return true;
}
static get hasSmoothing() {
return true;
}
}
exports.VolumeTransformer = VolumeTransformer;

15
helpers/Music/dist/index.d.ts vendored Normal file
View file

@ -0,0 +1,15 @@
import "./smoothVolume";
export { AudioFilters } from "./utils/AudioFilters";
export { ExtractorModel } from "./Structures/ExtractorModel";
export { Playlist } from "./Structures/Playlist";
export { Player } from "./Player";
export { PlayerError, ErrorStatusCode } from "./Structures/PlayerError";
export { QueryResolver } from "./utils/QueryResolver";
export { Queue } from "./Structures/Queue";
export { Track } from "./Structures/Track";
export { VoiceUtils } from "./VoiceInterface/VoiceUtils";
export { VoiceEvents, StreamDispatcher } from "./VoiceInterface/StreamDispatcher";
export { Util } from "./utils/Util";
export * from "./types/types";
export * from "./utils/FFmpegStream";
export declare const version: string;

33
helpers/Music/dist/index.js vendored Normal file
View file

@ -0,0 +1,33 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.version = exports.Util = exports.StreamDispatcher = exports.VoiceUtils = exports.Track = exports.Queue = exports.QueryResolver = exports.ErrorStatusCode = exports.PlayerError = exports.Player = exports.Playlist = exports.ExtractorModel = exports.AudioFilters = void 0;
const tslib_1 = require("tslib");
// try applying smooth volume patch on load
require("./smoothVolume");
var AudioFilters_1 = require("./utils/AudioFilters");
Object.defineProperty(exports, "AudioFilters", { enumerable: true, get: function () { return AudioFilters_1.AudioFilters; } });
var ExtractorModel_1 = require("./Structures/ExtractorModel");
Object.defineProperty(exports, "ExtractorModel", { enumerable: true, get: function () { return ExtractorModel_1.ExtractorModel; } });
var Playlist_1 = require("./Structures/Playlist");
Object.defineProperty(exports, "Playlist", { enumerable: true, get: function () { return Playlist_1.Playlist; } });
var Player_1 = require("./Player");
Object.defineProperty(exports, "Player", { enumerable: true, get: function () { return Player_1.Player; } });
var PlayerError_1 = require("./Structures/PlayerError");
Object.defineProperty(exports, "PlayerError", { enumerable: true, get: function () { return PlayerError_1.PlayerError; } });
Object.defineProperty(exports, "ErrorStatusCode", { enumerable: true, get: function () { return PlayerError_1.ErrorStatusCode; } });
var QueryResolver_1 = require("./utils/QueryResolver");
Object.defineProperty(exports, "QueryResolver", { enumerable: true, get: function () { return QueryResolver_1.QueryResolver; } });
var Queue_1 = require("./Structures/Queue");
Object.defineProperty(exports, "Queue", { enumerable: true, get: function () { return Queue_1.Queue; } });
var Track_1 = require("./Structures/Track");
Object.defineProperty(exports, "Track", { enumerable: true, get: function () { return Track_1.Track; } });
var VoiceUtils_1 = require("./VoiceInterface/VoiceUtils");
Object.defineProperty(exports, "VoiceUtils", { enumerable: true, get: function () { return VoiceUtils_1.VoiceUtils; } });
var StreamDispatcher_1 = require("./VoiceInterface/StreamDispatcher");
Object.defineProperty(exports, "StreamDispatcher", { enumerable: true, get: function () { return StreamDispatcher_1.StreamDispatcher; } });
var Util_1 = require("./utils/Util");
Object.defineProperty(exports, "Util", { enumerable: true, get: function () { return Util_1.Util; } });
tslib_1.__exportStar(require("./types/types"), exports);
tslib_1.__exportStar(require("./utils/FFmpegStream"), exports);
// eslint-disable-next-line @typescript-eslint/no-var-requires
exports.version = require(`${__dirname}/../package.json`).version;

21
helpers/Music/dist/index.mjs vendored Normal file
View file

@ -0,0 +1,21 @@
import mod from "./index.js";
export default mod;
export const AudioFilters = mod.AudioFilters;
export const ErrorStatusCode = mod.ErrorStatusCode;
export const ExtractorModel = mod.ExtractorModel;
export const FFMPEG_ARGS_PIPED = mod.FFMPEG_ARGS_PIPED;
export const FFMPEG_ARGS_STRING = mod.FFMPEG_ARGS_STRING;
export const Player = mod.Player;
export const PlayerError = mod.PlayerError;
export const Playlist = mod.Playlist;
export const QueryResolver = mod.QueryResolver;
export const QueryType = mod.QueryType;
export const Queue = mod.Queue;
export const QueueRepeatMode = mod.QueueRepeatMode;
export const StreamDispatcher = mod.StreamDispatcher;
export const Track = mod.Track;
export const Util = mod.Util;
export const VoiceUtils = mod.VoiceUtils;
export const createFFmpegStream = mod.createFFmpegStream;
export const version = mod.version;

1
helpers/Music/dist/smoothVolume.d.ts vendored Normal file
View file

@ -0,0 +1 @@
export {};

13
helpers/Music/dist/smoothVolume.js vendored Normal file
View file

@ -0,0 +1,13 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const VolumeTransformer_1 = require("./VoiceInterface/VolumeTransformer");
try {
// eslint-disable-next-line
const mod = require("prism-media");
if (typeof mod.VolumeTransformer.hasSmoothing !== "boolean") {
Reflect.set(mod, "VolumeTransformer", VolumeTransformer_1.VolumeTransformer);
}
}
catch {
/* do nothing */
}

453
helpers/Music/dist/types/types.d.ts vendored Normal file
View file

@ -0,0 +1,453 @@
/// <reference types="node" />
import { Snowflake, User, UserResolvable } from "discord.js";
import { Readable, Duplex } from "stream";
import { Queue } from "../Structures/Queue";
import Track from "../Structures/Track";
import { Playlist } from "../Structures/Playlist";
import { StreamDispatcher } from "../VoiceInterface/StreamDispatcher";
export declare type FiltersName = keyof QueueFilters;
export interface PlayerSearchResult {
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;
}
/**
* The track source:
* - soundcloud
* - youtube
* - spotify
* - arbitrary
* @typedef {string} TrackSource
*/
export declare type TrackSource = "soundcloud" | "youtube" | "spotify" | "arbitrary";
/**
* @typedef {object} RawTrackData
* @property {string} title The title
* @property {string} description The description
* @property {string} author The author
* @property {string} url The url
* @property {string} thumbnail The thumbnail
* @property {string} duration The duration
* @property {number | boolean} views The views
* @property {User} requestedBy The user who requested this track
* @property {Playlist} [playlist] The playlist
* @property {TrackSource} [source="arbitrary"] The source
* @property {any} [engine] The engine
* @property {boolean} [live] If this track is live
* @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;
live?: boolean;
raw?: any;
}
/**
* @typedef {object} TimeData
* @property {number} days Time in days
* @property {number} hours Time in hours
* @property {number} minutes Time in minutes
* @property {number} seconds Time in seconds
*/
export interface TimeData {
days: number;
hours: number;
minutes: number;
seconds: number;
}
/**
* @typedef {object} PlayerProgressbarOptions
* @property {boolean} [timecodes] If it should render time codes
* @property {boolean} [queue] If it should create progress bar for the whole queue
* @property {number} [length] The bar length
* @property {string} [line] The bar track
* @property {string} [indicator] The indicator
*/
export interface PlayerProgressbarOptions {
timecodes?: boolean;
length?: number;
line?: string;
indicator?: string;
queue?: boolean;
}
/**
* @typedef {object} PlayerOptions
* @property {boolean} [leaveOnEnd=true] If it should leave on end
* @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 {YTDLDownloadOptions} [ytdlOptions] The youtube download options
* @property {number} [initialVolume=100] The initial player volume
* @property {number} [bufferingTimeout=3000] Buffering timeout for the stream
* @property {boolean} [spotifyBridge=true] If player should bridge spotify source to youtube
* @property {boolean} [disableVolume=false] If player should disable inline volume
* @property {number} [volumeSmoothness=0] The volume transition smoothness between volume changes (lower the value to get better result)
* Setting this or leaving this empty will disable this effect. Example: `volumeSmoothness: 0.1`
* @property {Function} [onBeforeCreateStream] Runs before creating stream
*/
export interface PlayerOptions {
leaveOnEnd?: boolean;
leaveOnStop?: boolean;
leaveOnEmpty?: boolean;
leaveOnEmptyCooldown?: number;
autoSelfDeaf?: boolean;
initialVolume?: number;
bufferingTimeout?: number;
spotifyBridge?: boolean;
disableVolume?: boolean;
volumeSmoothness?: number;
onBeforeCreateStream?: (track: Track, source: TrackSource, queue: Queue) => Promise<Readable>;
}
/**
* @typedef {object} ExtractorModelData
* @property {object} [playlist] The playlist info (if any)
* @property {string} [playlist.title] The playlist title
* @property {string} [playlist.description] The playlist description
* @property {string} [playlist.thumbnail] The playlist thumbnail
* @property {album|playlist} [playlist.type] The playlist type: `album` | `playlist`
* @property {TrackSource} [playlist.source] The playlist source
* @property {object} [playlist.author] The playlist author
* @property {string} [playlist.author.name] The author name
* @property {string} [playlist.author.url] The author url
* @property {string} [playlist.id] The playlist id
* @property {string} [playlist.url] The playlist url
* @property {any} [playlist.rawPlaylist] The raw data
* @property {ExtractorData[]} data The data
*/
/**
* @typedef {object} ExtractorData
* @property {string} title The title
* @property {number} duration The duration
* @property {string} thumbnail The thumbnail
* @property {string|Readable|Duplex} engine The stream engine
* @property {number} views The views count
* @property {string} author The author
* @property {string} description The description
* @property {string} url The url
* @property {string} [version] The extractor version
* @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;
};
data: {
title: string;
duration: number;
thumbnail: string;
engine: string | Readable | Duplex;
views: number;
author: string;
description: string;
url: string;
version?: string;
source?: TrackSource;
}[];
}
/**
* The search query type
* This can be one of:
* - 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
* @typedef {number} QueryType
*/
export declare enum QueryType {
AUTO = 0,
YOUTUBE = 1,
YOUTUBE_PLAYLIST = 2,
SOUNDCLOUD_TRACK = 3,
SOUNDCLOUD_PLAYLIST = 4,
SOUNDCLOUD = 5,
SPOTIFY_SONG = 6,
SPOTIFY_ALBUM = 7,
SPOTIFY_PLAYLIST = 8,
FACEBOOK = 9,
VIMEO = 10,
ARBITRARY = 11,
REVERBNATION = 12,
YOUTUBE_SEARCH = 13,
YOUTUBE_VIDEO = 14,
SOUNDCLOUD_SEARCH = 15
}
/**
* Emitted when bot gets disconnected from a voice channel
* @event Player#botDisconnect
* @param {Queue} queue The queue
*/
/**
* Emitted when the voice channel is empty
* @event Player#channelEmpty
* @param {Queue} queue The queue
*/
/**
* Emitted when bot connects to a voice channel
* @event Player#connectionCreate
* @param {Queue} queue The queue
* @param {StreamDispatcher} connection The discord player connection object
*/
/**
* Debug information
* @event Player#debug
* @param {Queue} queue The queue
* @param {string} message The message
*/
/**
* Emitted on error
* <warn>This event should handled properly otherwise it may crash your process!</warn>
* @event Player#error
* @param {Queue} queue The queue
* @param {Error} error The error
*/
/**
* Emitted on connection error. Sometimes stream errors are emitted here as well.
* @event Player#connectionError
* @param {Queue} queue The queue
* @param {Error} error The error
*/
/**
* Emitted when queue ends
* @event Player#queueEnd
* @param {Queue} queue The queue
*/
/**
* Emitted when a single track is added
* @event Player#trackAdd
* @param {Queue} queue The queue
* @param {Track} track The track
*/
/**
* Emitted when multiple tracks are added
* @event Player#tracksAdd
* @param {Queue} queue The queue
* @param {Track[]} tracks The tracks
*/
/**
* Emitted when a track starts playing
* @event Player#trackStart
* @param {Queue} queue The queue
* @param {Track} track The track
*/
/**
* Emitted when a track ends
* @event Player#trackEnd
* @param {Queue} queue The queue
* @param {Track} track The track
*/
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;
}
/**
* @typedef {object} PlayOptions
* @property {boolean} [filtersUpdate=false] If this play was triggered for filters update
* @property {string[]} [encoderArgs=[]] FFmpeg args passed to encoder
* @property {number} [seek] Time to seek to before playing
* @property {boolean} [immediate=false] If it should start playing the provided track immediately
*/
export interface PlayOptions {
filtersUpdate?: boolean;
encoderArgs?: string[];
seek?: number;
immediate?: boolean;
}
/**
* @typedef {object} SearchOptions
* @property {UserResolvable} requestedBy The user who requested this search
* @property {QueryType|string} [searchEngine=QueryType.AUTO] The query search engine, can be extractor name to target specific one (custom)
* @property {boolean} [blockExtractor=false] If it should block custom extractors
*/
export interface SearchOptions {
requestedBy: UserResolvable;
searchEngine?: QueryType | string;
blockExtractor?: boolean;
}
/**
* The queue repeat mode. This can be one of:
* - OFF
* - TRACK
* - QUEUE
* - AUTOPLAY
* @typedef {number} QueueRepeatMode
*/
export declare enum QueueRepeatMode {
OFF = 0,
TRACK = 1,
QUEUE = 2,
AUTOPLAY = 3
}
/**
* @typedef {object} PlaylistInitData
* @property {Track[]} tracks The tracks of this playlist
* @property {string} title The playlist title
* @property {string} description The description
* @property {string} thumbnail The thumbnail
* @property {album|playlist} type The playlist type: `album` | `playlist`
* @property {TrackSource} source The playlist source
* @property {object} author The playlist author
* @property {string} [author.name] The author name
* @property {string} [author.url] The author url
* @property {string} id The playlist id
* @property {string} url The playlist url
* @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;
}
/**
* @typedef {object} TrackJSON
* @property {string} title The track title
* @property {string} description The track description
* @property {string} author The author
* @property {string} url The url
* @property {string} thumbnail The thumbnail
* @property {string} duration The duration
* @property {number} durationMS The duration in ms
* @property {number} views The views count
* @property {Snowflake} requestedBy The id of the user who requested this track
* @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;
}
/**
* @typedef {object} PlaylistJSON
* @property {string} id The playlist id
* @property {string} url The playlist url
* @property {string} title The playlist title
* @property {string} description The playlist description
* @property {string} thumbnail The thumbnail
* @property {album|playlist} type The playlist type: `album` | `playlist`
* @property {TrackSource} source The track source
* @property {object} author The playlist author
* @property {string} [author.name] The author name
* @property {string} [author.url] The author url
* @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[];
}
/**
* @typedef {object} PlayerInitOptions
* @property {boolean} [autoRegisterExtractor=true] If it should automatically register `@discord-player/extractor`
* @property {YTDLDownloadOptions} [ytdlOptions] The options passed to `ytdl-core`
* @property {number} [connectionTimeout=20000] The voice connection timeout
*/
export interface PlayerInitOptions {
autoRegisterExtractor?: boolean;
connectionTimeout?: number;
}

58
helpers/Music/dist/types/types.js vendored Normal file
View file

@ -0,0 +1,58 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.QueueRepeatMode = exports.QueryType = void 0;
/**
* The search query type
* This can be one of:
* - 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
* @typedef {number} QueryType
*/
var QueryType;
(function (QueryType) {
QueryType[QueryType["AUTO"] = 0] = "AUTO";
QueryType[QueryType["YOUTUBE"] = 1] = "YOUTUBE";
QueryType[QueryType["YOUTUBE_PLAYLIST"] = 2] = "YOUTUBE_PLAYLIST";
QueryType[QueryType["SOUNDCLOUD_TRACK"] = 3] = "SOUNDCLOUD_TRACK";
QueryType[QueryType["SOUNDCLOUD_PLAYLIST"] = 4] = "SOUNDCLOUD_PLAYLIST";
QueryType[QueryType["SOUNDCLOUD"] = 5] = "SOUNDCLOUD";
QueryType[QueryType["SPOTIFY_SONG"] = 6] = "SPOTIFY_SONG";
QueryType[QueryType["SPOTIFY_ALBUM"] = 7] = "SPOTIFY_ALBUM";
QueryType[QueryType["SPOTIFY_PLAYLIST"] = 8] = "SPOTIFY_PLAYLIST";
QueryType[QueryType["FACEBOOK"] = 9] = "FACEBOOK";
QueryType[QueryType["VIMEO"] = 10] = "VIMEO";
QueryType[QueryType["ARBITRARY"] = 11] = "ARBITRARY";
QueryType[QueryType["REVERBNATION"] = 12] = "REVERBNATION";
QueryType[QueryType["YOUTUBE_SEARCH"] = 13] = "YOUTUBE_SEARCH";
QueryType[QueryType["YOUTUBE_VIDEO"] = 14] = "YOUTUBE_VIDEO";
QueryType[QueryType["SOUNDCLOUD_SEARCH"] = 15] = "SOUNDCLOUD_SEARCH";
})(QueryType = exports.QueryType || (exports.QueryType = {}));
/**
* The queue repeat mode. This can be one of:
* - OFF
* - TRACK
* - QUEUE
* - AUTOPLAY
* @typedef {number} QueueRepeatMode
*/
var QueueRepeatMode;
(function (QueueRepeatMode) {
QueueRepeatMode[QueueRepeatMode["OFF"] = 0] = "OFF";
QueueRepeatMode[QueueRepeatMode["TRACK"] = 1] = "TRACK";
QueueRepeatMode[QueueRepeatMode["QUEUE"] = 2] = "QUEUE";
QueueRepeatMode[QueueRepeatMode["AUTOPLAY"] = 3] = "AUTOPLAY";
})(QueueRepeatMode = exports.QueueRepeatMode || (exports.QueueRepeatMode = {}));

View file

@ -0,0 +1,36 @@
import { FiltersName } from "../types/types";
declare class AudioFilters {
constructor();
static get filters(): Record<FiltersName, string>;
static get<K extends FiltersName>(name: K): Record<keyof import("../types/types").QueueFilters, string>[K];
static has<K extends FiltersName>(name: K): boolean;
static [Symbol.iterator](): IterableIterator<{
name: FiltersName;
value: string;
}>;
static get names(): (keyof import("../types/types").QueueFilters)[];
static get length(): number;
static toString(): string;
/**
* Create ffmpeg args from the specified filters name
* @param filter The filter name
* @returns
*/
static create<K extends FiltersName>(filters?: K[]): string;
/**
* Defines audio filter
* @param filterName The name of the filter
* @param value The ffmpeg args
*/
static define(filterName: string, value: string): void;
/**
* Defines multiple audio filters
* @param filtersArray Array of filters containing the filter name and ffmpeg args
*/
static defineBulk(filtersArray: {
name: string;
value: string;
}[]): void;
}
export default AudioFilters;
export { AudioFilters };

View file

@ -0,0 +1,97 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.AudioFilters = void 0;
const bass = (g) => `bass=g=${g}:f=110:w=0.3`;
class AudioFilters {
constructor() {
return AudioFilters;
}
static get filters() {
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"
};
}
static get(name) {
return this.filters[name];
}
static has(name) {
return name in this.filters;
}
static *[Symbol.iterator]() {
for (const [k, v] of Object.entries(this.filters)) {
yield { name: k, value: v };
}
}
static get names() {
return Object.keys(this.filters);
}
// @ts-expect-error AudioFilters.length
static get length() {
return this.names.length;
}
static toString() {
return this.names.map((m) => this[m]).join(","); // eslint-disable-line @typescript-eslint/no-explicit-any
}
/**
* Create ffmpeg args from the specified filters name
* @param filter The filter name
* @returns
*/
static create(filters) {
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
*/
static define(filterName, value) {
this.filters[filterName] = value;
}
/**
* Defines multiple audio filters
* @param filtersArray Array of filters containing the filter name and ffmpeg args
*/
static defineBulk(filtersArray) {
filtersArray.forEach((arr) => this.define(arr.name, arr.value));
}
}
exports.AudioFilters = AudioFilters;
exports.default = AudioFilters;

View file

@ -0,0 +1,16 @@
/// <reference types="node" />
import type { Duplex, Readable } from "stream";
export interface FFmpegStreamOptions {
fmt?: string;
encoderArgs?: string[];
seek?: number;
skip?: boolean;
}
export declare function FFMPEG_ARGS_STRING(stream: string, fmt?: string): string[];
export declare function FFMPEG_ARGS_PIPED(fmt?: string): string[];
/**
* Creates FFmpeg stream
* @param stream The source stream
* @param options FFmpeg stream options
*/
export declare function createFFmpegStream(stream: Readable | Duplex | string, options?: FFmpegStreamOptions): Readable | Duplex;

View file

@ -0,0 +1,53 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createFFmpegStream = exports.FFMPEG_ARGS_PIPED = exports.FFMPEG_ARGS_STRING = void 0;
const prism_media_1 = require("prism-media");
function FFMPEG_ARGS_STRING(stream, fmt) {
// prettier-ignore
return [
"-reconnect", "1",
"-reconnect_streamed", "1",
"-reconnect_delay_max", "5",
"-i", stream,
"-analyzeduration", "0",
"-loglevel", "0",
"-f", `${typeof fmt === "string" ? fmt : "s16le"}`,
"-ar", "48000",
"-ac", "2"
];
}
exports.FFMPEG_ARGS_STRING = FFMPEG_ARGS_STRING;
function FFMPEG_ARGS_PIPED(fmt) {
// prettier-ignore
return [
"-analyzeduration", "0",
"-loglevel", "0",
"-f", `${typeof fmt === "string" ? fmt : "s16le"}`,
"-ar", "48000",
"-ac", "2"
];
}
exports.FFMPEG_ARGS_PIPED = FFMPEG_ARGS_PIPED;
/**
* Creates FFmpeg stream
* @param stream The source stream
* @param options FFmpeg stream options
*/
function createFFmpegStream(stream, options) {
if (options.skip && typeof stream !== "string")
return stream;
options ?? (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);
const transcoder = new prism_media_1.FFmpeg({ shell: false, args });
transcoder.on("close", () => transcoder.destroy());
if (typeof stream !== "string") {
stream.on("error", () => transcoder.destroy());
stream.pipe(transcoder);
}
return transcoder;
}
exports.createFFmpegStream = createFFmpegStream;

View file

@ -0,0 +1,20 @@
import { QueryType } from "../types/types";
declare class QueryResolver {
/**
* Query resolver
*/
private constructor();
/**
* Resolves the given search query
* @param {string} query The query
* @returns {QueryType}
*/
static resolve(query: string): Promise<QueryType>;
/**
* Parses vimeo id from url
* @param {string} query The query
* @returns {string}
*/
static getVimeoID(query: string): Promise<string>;
}
export { QueryResolver };

View file

@ -0,0 +1,66 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.QueryResolver = void 0;
const tslib_1 = require("tslib");
const types_1 = require("../types/types");
const play_dl_1 = tslib_1.__importDefault(require("play-dl"));
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// scary things below *sigh*
const spotifySongRegex = /https?:\/\/(?:embed\.|open\.)(?:spotify\.com\/)(?:track\/|\?uri=spotify:track:)((\w|-){22})/;
const spotifyPlaylistRegex = /https?:\/\/(?:embed\.|open\.)(?:spotify\.com\/)(?:playlist\/|\?uri=spotify:playlist:)((\w|-){22})/;
const spotifyAlbumRegex = /https?:\/\/(?:embed\.|open\.)(?:spotify\.com\/)(?:album\/|\?uri=spotify:album:)((\w|-){22})/;
const vimeoRegex = /(http|https)?:\/\/(www\.|player\.)?vimeo\.com\/(?:channels\/(?:\w+\/)?|groups\/([^/]*)\/videos\/|video\/|)(\d+)(?:|\/\?)/;
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*)?$/;
// scary things above *sigh*
class QueryResolver {
/**
* Query resolver
*/
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) {
if (await play_dl_1.default.so_validate(query) === "track")
return types_1.QueryType.SOUNDCLOUD_TRACK;
if (await play_dl_1.default.so_validate(query) === "playlist" || query.includes("/sets/"))
return types_1.QueryType.SOUNDCLOUD_PLAYLIST;
if (play_dl_1.default.yt_validate(query) === "playlist")
return types_1.QueryType.YOUTUBE_PLAYLIST;
if (play_dl_1.default.yt_validate(query) === "video")
return types_1.QueryType.YOUTUBE_VIDEO;
if (spotifySongRegex.test(query))
return types_1.QueryType.SPOTIFY_SONG;
if (spotifyPlaylistRegex.test(query))
return types_1.QueryType.SPOTIFY_PLAYLIST;
if (spotifyAlbumRegex.test(query))
return types_1.QueryType.SPOTIFY_ALBUM;
if (vimeoRegex.test(query))
return types_1.QueryType.VIMEO;
if (facebookRegex.test(query))
return types_1.QueryType.FACEBOOK;
if (reverbnationRegex.test(query))
return types_1.QueryType.REVERBNATION;
if (attachmentRegex.test(query))
return types_1.QueryType.ARBITRARY;
return types_1.QueryType.YOUTUBE_SEARCH;
}
/**
* Parses vimeo id from url
* @param {string} query The query
* @returns {string}
*/
static async getVimeoID(query) {
return await QueryResolver.resolve(query) === types_1.QueryType.VIMEO
? query
.split("/")
.filter((x) => !!x)
.pop()
: null;
}
}
exports.QueryResolver = QueryResolver;

53
helpers/Music/dist/utils/Util.d.ts vendored Normal file
View file

@ -0,0 +1,53 @@
import { StageChannel, VoiceChannel } from "discord.js";
import { TimeData } from "../types/types";
declare class Util {
/**
* Utils
*/
private constructor();
/**
* Creates duration string
* @param {object} durObj The duration object
* @returns {string}
*/
static durationString(durObj: Record<string, number>): string;
/**
* Parses milliseconds to consumable time object
* @param {number} milliseconds The time in ms
* @returns {TimeData}
*/
static parseMS(milliseconds: number): TimeData;
/**
* Builds time code
* @param {TimeData} duration The duration object
* @returns {string}
*/
static buildTimeCode(duration: TimeData): string;
/**
* Picks last item of the given array
* @param {any[]} arr The array
* @returns {any}
*/
static last<T = any>(arr: T[]): T;
/**
* Checks if the voice channel is empty
* @param {VoiceChannel|StageChannel} channel The voice channel
* @returns {boolean}
*/
static isVoiceEmpty(channel: VoiceChannel | StageChannel): boolean;
/**
* Safer require
* @param {string} id Node require id
* @returns {any}
*/
static require(id: string): any;
/**
* Asynchronous timeout
* @param {number} time The time in ms to wait
* @returns {Promise<unknown>}
*/
static wait(time: number): Promise<unknown>;
static noop(): void;
static getFetch(): Promise<any>;
}
export { Util };

133
helpers/Music/dist/utils/Util.js vendored Normal file
View file

@ -0,0 +1,133 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Util = void 0;
class Util {
/**
* Utils
*/
constructor() { } // eslint-disable-line @typescript-eslint/no-empty-function
/**
* Creates duration string
* @param {object} durObj The duration object
* @returns {string}
*/
static durationString(durObj) {
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) {
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
};
}
/**
* Builds time code
* @param {TimeData} duration The duration object
* @returns {string}
*/
static buildTimeCode(duration) {
const items = Object.keys(duration);
const required = ["days", "hours", "minutes", "seconds"];
const parsed = items.filter((x) => required.includes(x)).map((m) => duration[m]);
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;
}
/**
* 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) {
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) {
return channel.members.filter((member) => !member.user.bot).size === 0;
}
/**
* Safer require
* @param {string} id Node require id
* @returns {any}
*/
static require(id) {
try {
return require(id);
}
catch {
return null;
}
}
/**
* Asynchronous timeout
* @param {number} time The time in ms to wait
* @returns {Promise<unknown>}
*/
static wait(time) {
return new Promise((r) => setTimeout(r, time).unref());
}
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 Promise.resolve().then(() => __importStar(require(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?
}
}
}
}
}
exports.Util = Util;

4577
helpers/Music/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,61 @@
{
"name": "music-player",
"version": "1.0.0",
"description": "music player",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist/"
],
"module": "dist/index.mjs",
"exports": {
".": {
"require": "./dist/index.js",
"import": "./dist/index.mjs"
},
"./smoothVolume": "./dist/smoothVolume.js",
"./src/*": "./dist/*",
"./dist/*": "./dist/*"
},
"scripts": {
"dev": "cd examples/test && ts-node index.ts",
"build": "rimraf dist && tsc && npm run build:esm",
"build:check": "tsc --noEmit --incremental false",
"prepublishOnly": "rollup-type-bundler -e stream",
"build:esm": "gen-esm-wrapper ./dist/index.js ./dist/index.mjs",
"format": "prettier --write \"src/**/*.ts\"",
"docs": "typedoc --json docs/typedoc.json src/index.ts",
"postdocs": "node scripts/docgen.js",
"lint": "eslint src --ext .ts",
"prepare": "husky install",
"lint:fix": "eslint src --ext .ts --fix"
},
"author": "Jonny_Bro",
"license": "NO LICENSE",
"dependencies": {
"@discordjs/voice": "^0.11.0",
"libsodium-wrappers": "^0.7.10",
"spotify-url-info": "^3.1.2",
"tiny-typed-emitter": "^2.1.0",
"tslib": "^2.4.0"
},
"devDependencies": {
"@discordjs/ts-docgen": "^0.4.1",
"@favware/rollup-type-bundler": "^1.0.10",
"@types/node": "^18.6.3",
"@types/ws": "^8.5.3",
"@typescript-eslint/eslint-plugin": "^5.32.0",
"@typescript-eslint/parser": "^5.32.0",
"discord-api-types": "^0.37.0",
"discord.js": "^14.1.2",
"eslint": "^8.21.0",
"gen-esm-wrapper": "^1.1.3",
"husky": "^8.0.1",
"opusscript": "^0.0.8",
"prettier": "^2.7.1",
"rimraf": "^3.0.2",
"ts-node": "^10.9.1",
"typedoc": "^0.23.10",
"typescript": "^4.7.4"
}
}

607
helpers/Music/src/Player.ts Normal file
View file

@ -0,0 +1,607 @@
import { Client, Collection, GuildResolvable, Snowflake, User, VoiceState, IntentsBitField } from "discord.js";
import { TypedEmitter as EventEmitter } from "tiny-typed-emitter";
import { Queue } from "./Structures/Queue";
import { VoiceUtils } from "./VoiceInterface/VoiceUtils";
import { PlayerEvents, PlayerOptions, QueryType, SearchOptions, PlayerInitOptions, PlayerSearchResult, PlaylistInitData } from "./types/types";
import Track from "./Structures/Track";
import play, { SoundCloudPlaylist, YouTubePlayList } from "play-dl";
import Spotify from "spotify-url-info";
import { QueryResolver } from "./utils/QueryResolver";
import { Util } from "./utils/Util";
import { PlayerError, ErrorStatusCode } from "./Structures/PlayerError";
import { Playlist } from "./Structures/Playlist";
import { ExtractorModel } from "./Structures/ExtractorModel";
import { generateDependencyReport } from "@discordjs/voice";
class Player extends EventEmitter<PlayerEvents> {
public readonly client: Client;
public readonly options: PlayerInitOptions = {
autoRegisterExtractor: true,
connectionTimeout: 20000
};
public readonly queues = new Collection<Snowflake, Queue>();
public readonly voiceUtils = new VoiceUtils();
public readonly extractors = new Collection<string, ExtractorModel>();
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();
/**
* 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');
}
/**
* The extractors collection
* @type {ExtractorModel}
*/
this.options = Object.assign(this.options, options);
this.client.on("voiceStateUpdate", this._handleVoiceState.bind(this));
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]));
}
}
}
/**
* 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) {
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 && 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);
}
}
}
/**
* 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<T = unknown>(guild: GuildResolvable, queueInitOptions: PlayerOptions & { metadata?: T } = {}): Queue<T> {
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<T>;
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<T>;
}
/**
* Returns the queue if available
* @param {GuildResolvable} guild The guild id
* @returns {Queue}
*/
getQueue<T = unknown>(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<T>;
}
/**
* Deletes a queue and returns deleted queue object
* @param {GuildResolvable} guild The guild id to remove
* @returns {Queue}
*/
deleteQueue<T = unknown>(guild: GuildResolvable) {
guild = this.client.guilds.resolve(guild);
if (!guild) throw new PlayerError("Unknown Guild", ErrorStatusCode.UNKNOWN_GUILD);
const prev = this.getQueue<T>(guild);
try {
prev.destroy();
} catch {} // eslint-disable-line no-empty
this.queues.delete(guild.id);
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<PlayerSearchResult>}
*/
async search(query: string | Track, options: SearchOptions): Promise<PlayerSearchResult> {
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
})
);
if (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: []
});
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;
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 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: [] };
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[] = [];
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
});
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: [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
});
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[];
}
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 songs = await data.all_tracks();
for (const song of songs) {
const track = new Track(this, {
title: song.name,
description: "",
author: song.publisher.name ?? "Unknown Publisher",
url: song.url,
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: [] };
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"
}));
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;
}
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);
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;
}
/**
* 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<U extends keyof PlayerEvents>(eventName: U, ...args: Parameters<PlayerEvents[U]>): 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<T>(queueLike: GuildResolvable | Queue): Queue<T> {
return this.getQueue(queueLike instanceof Queue ? queueLike.guild : queueLike);
}
*[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);
}
}
export { Player };

View file

@ -0,0 +1,73 @@
import { ExtractorModelData } from "../types/types";
class ExtractorModel {
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;
/**
* 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<ExtractorModelData>}
*/
async handle(query: string): Promise<ExtractorModelData> {
const data = await this._raw.getInfo(query);
if (!data) return null;
return {
playlist: data.playlist ?? null,
data:
(data.info as Omit<ExtractorModelData, "playlist">["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));
}
/**
* The extractor version
* @type {string}
*/
get version(): string {
return this._raw.version ?? "0.0.0";
}
}
export { ExtractorModel };

View file

@ -0,0 +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"
}
export class PlayerError extends Error {
message: string;
statusCode: ErrorStatusCode;
createdAt = new Date();
constructor(message: string, code: ErrorStatusCode = ErrorStatusCode.PLAYER_ERROR) {
super();
this.message = `[${code}] ${message}`;
this.statusCode = code;
this.name = code;
Error.captureStackTrace(this);
}
get createdTimestamp() {
return this.createdAt.getTime();
}
valueOf() {
return this.statusCode;
}
toJSON() {
return {
stack: this.stack,
code: this.statusCode,
message: this.message,
created: this.createdTimestamp
};
}
toString() {
return this.stack;
}
}

View file

@ -0,0 +1,138 @@
import { Player } from "../Player";
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
/**
* 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 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 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 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 url
* @name Playlist#url
* @type {string}
*/
this.url = data.url;
/**
* The playlist title
* @type {string}
*/
this.title = data.title;
/**
* @name Playlist#rawPlaylist
* @type {any}
* @readonly
*/
}
*[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[]
};
if (withTracks) payload.tracks = this.tracks.map((m) => m.toJSON(true));
return payload as PlaylistJSON;
}
}
export { Playlist };

View file

@ -0,0 +1,776 @@
import { Collection, Guild, StageChannel, VoiceChannel, SnowflakeUtil, GuildChannelResolvable, ChannelType } from "discord.js";
import { Player } from "../Player";
import { StreamDispatcher } from "../VoiceInterface/StreamDispatcher";
import Track from "./Track";
import { PlayerOptions, PlayerProgressbarOptions, PlayOptions, QueueFilters, QueueRepeatMode, TrackSource } from "../types/types";
import { AudioResource, StreamType } from "@discordjs/voice";
import play from "play-dl";
import { Util } from "../utils/Util";
import AudioFilters from "../utils/AudioFilters";
import { PlayerError, ErrorStatusCode } from "./PlayerError";
import type { Readable } from "stream";
import { VolumeTransformer } from "../VoiceInterface/VolumeTransformer";
import { createFFmpegStream } from "../utils/FFmpegStream";
class Queue<T = unknown> {
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<string, NodeJS.Timeout>();
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<Readable | undefined> = 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,
ytdlOptions: {
highWaterMark: 1 << 25
},
initialVolume: 100,
bufferingTimeout: 3000,
spotifyBridge: true,
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<Queue>}
*/
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(Util.last(this.previousTracks));
} else {
if (this.repeatMode === QueueRepeatMode.TRACK) return void this.play(Util.last(this.previousTracks), { immediate: true });
if (this.repeatMode === QueueRepeatMode.QUEUE) this.tracks.push(Util.last(this.previousTracks));
const nextTrack = this.tracks.shift();
this.play(nextTrack, { immediate: true });
return;
}
});
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<void>}
*/
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<void>}
*/
async back() {
if (this.#watchDestroyed()) return;
const prev = this.previousTracks[this.previousTracks.length - 2]; // because last item is the current track
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.round((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<void>}
*/
async play(src?: Track, options: PlayOptions = {}): Promise<void> {
if (this.#watchDestroyed(false)) return;
if (!this.connection || !this.connection.voiceConnection) throw new PlayerError("Voice connection is not available, use <Queue>.connect()!", ErrorStatusCode.NO_CONNECTION);
if (src && (this.playing || this.tracks.length) && !options.immediate) return this.addTrack(src);
const track = options.filtersUpdate && !options.immediate ? src || this.current : src ?? this.tracks.shift();
if (!track) return;
this.player.emit("debug", this, "Received play request");
if (!options.filtersUpdate) {
this.previousTracks = this.previousTracks.filter((x) => x.id !== track.id);
this.previousTracks.push(track);
}
let stream = null;
const hasCustomDownloader = typeof this.onBeforeCreateStream === "function";
if (["youtube", "spotify"].includes(track.raw.source)) {
let spotifyResolved = false;
if (this.options.spotifyBridge && track.raw.source === "spotify" && !track.raw.engine) {
track.raw.engine = await play.search(`${track.author} ${track.title}`, { source: { youtube: "video" } })
.then(res => res[0].url)
.catch(() => null);
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 || (track.raw.source === "soundcloud" && typeof track.raw.engine?.downloadProgressive === "function")
? 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<Track> = 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<void>}
* @private
*/
private async _handleAutoplay(track: Track): Promise<void> {
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 randomRelated = await play.video_info(info.related_videos[0]);
const nextTrack = new Track(this.player, {
title: randomRelated.video_details.title,
url: randomRelated.video_details.url,
duration: randomRelated.video_details.durationRaw ? Util.buildTimeCode(Util.parseMS(randomRelated.video_details.durationInSec * 1000)) : "0:00",
description: "",
thumbnail: Util.last(randomRelated.video_details.thumbnails).url,
views: randomRelated.video_details.views,
author: randomRelated.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 };

View file

@ -0,0 +1,191 @@
import { User, escapeMarkdown, SnowflakeUtil } from "discord.js";
import { Player } from "../Player";
import { RawTrackData, TrackJSON } from "../types/types";
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();
/**
* 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}
*/
/**
* Description of this track
* @name Track#description
* @type {string}
*/
/**
* Author of this track
* @name Track#author
* @type {string}
*/
/**
* URL of this track
* @name Track#url
* @type {string}
*/
/**
* Thumbnail of this track
* @name Track#thumbnail
* @type {string}
*/
/**
* Duration of this track
* @name Track#duration
* @type {string}
*/
/**
* Views count of this track
* @name Track#views
* @type {number}
*/
/**
* Person who requested this track
* @name Track#requestedBy
* @type {User}
*/
/**
* If this track belongs to playlist
* @name Track#fromPlaylist
* @type {boolean}
*/
/**
* Raw track data
* @name Track#raw
* @type {RawTrackData}
*/
/**
* The track id
* @name Track#id
* @type {Snowflake}
* @readonly
*/
/**
* The playlist which track belongs
* @name Track#playlist
* @type {Playlist}
*/
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;
// 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 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);
}
/**
* 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}`;
}
/**
* 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;
export { Track };

View file

@ -0,0 +1,253 @@
import {
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";
import { TypedEmitter as EventEmitter } from "tiny-typed-emitter";
import Track from "../Structures/Track";
import { Util } from "../utils/Util";
import { PlayerError, ErrorStatusCode } from "../Structures/PlayerError";
export interface VoiceEvents {
/* eslint-disable @typescript-eslint/no-explicit-any */
error: (error: AudioPlayerError) => any;
debug: (message: string) => any;
start: (resource: AudioResource<Track>) => any;
finish: (resource: AudioResource<Track>) => any;
/* eslint-enable @typescript-eslint/no-explicit-any */
}
class StreamDispatcher extends EventEmitter<VoiceEvents> {
public readonly voiceConnection: VoiceConnection;
public readonly audioPlayer: AudioPlayer;
public channel: VoiceChannel | StageChannel;
public audioResource?: AudioResource<Track>;
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();
/**
* The voice connection
* @type {VoiceConnection}
*/
this.voiceConnection = connection;
/**
* The audio player
* @type {AudioPlayer}
*/
this.audioPlayer = createAudioPlayer();
/**
* The voice channel
* @type {VoiceChannel|StageChannel}
*/
this.channel = channel;
/**
* 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.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);
}
/**
* 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;
}
/**
* 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
}
/**
* 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;
}
/**
* Resumes the stream playback
* @returns {boolean}
*/
resume() {
const success = this.audioPlayer.unpause();
this.paused = !success;
return success;
}
/**
* Play stream
* @param {AudioResource<Track>} [resource=this.audioResource] The audio resource to play
* @returns {Promise<StreamDispatcher>}
*/
async playStream(resource: AudioResource<Track> = 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);
}
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;
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 playback time
* @type {number}
*/
get streamTime() {
if (!this.audioResource) return 0;
return this.audioResource.playbackDuration;
}
}
export { StreamDispatcher as StreamDispatcher };

View file

@ -0,0 +1,82 @@
import { VoiceChannel, StageChannel, Collection, Snowflake } from "discord.js";
import { DiscordGatewayAdapterCreator, joinVoiceChannel, VoiceConnection } from "@discordjs/voice";
import { StreamDispatcher } from "./StreamDispatcher";
class VoiceUtils {
public cache: Collection<Snowflake, StreamDispatcher>;
/**
* The voice utils
* @private
*/
constructor() {
/**
* The cache where voice utils stores stream managers
* @type {Collection<Snowflake, StreamDispatcher>}
*/
this.cache = new Collection<Snowflake, StreamDispatcher>();
}
/**
* Joins a voice channel, creating basic stream dispatch manager
* @param {StageChannel|VoiceChannel} channel The voice channel
* @param {object} [options] Join options
* @returns {Promise<StreamDispatcher>}
*/
public async connect(
channel: VoiceChannel | StageChannel,
options?: {
deaf?: boolean;
maxTime?: number;
}
): Promise<StreamDispatcher> {
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)
});
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();
}
/**
* Returns Discord Player voice connection
* @param {Snowflake} guild The guild id
* @returns {StreamDispatcher}
*/
public getConnection(guild: Snowflake) {
return this.cache.get(guild);
}
}
export { VoiceUtils };

View file

@ -0,0 +1,144 @@
// prism's volume transformer with smooth volume support
import { Transform, TransformOptions } from "stream";
export interface VolumeTransformerOptions extends TransformOptions {
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;
}
_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;
}
}
_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();
}
const { _bytes, _extremum } = this;
chunk = this._chunk = Buffer.concat([this._chunk, chunk]);
if (chunk.length < _bytes) return done();
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);
}
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;
}
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));
}
setVolumeLogarithmic(value: number) {
this.setVolume(Math.pow(value, 1.660964));
}
get volumeDecibels() {
return Math.log10(this.volume) * 20;
}
get volumeLogarithmic() {
return Math.pow(this.volume, 1 / 1.660964);
}
get smoothness() {
return this._smoothing;
}
setSmoothness(smoothness: number) {
this._smoothing = smoothness;
}
smoothingEnabled() {
return Number.isFinite(this._smoothing) && this._smoothing > 0;
}
get hasSmoothness() {
return true;
}
static get hasSmoothing() {
return true;
}
}

View file

@ -0,0 +1,19 @@
// try applying smooth volume patch on load
import "./smoothVolume";
export { AudioFilters } from "./utils/AudioFilters";
export { ExtractorModel } from "./Structures/ExtractorModel";
export { Playlist } from "./Structures/Playlist";
export { Player } from "./Player";
export { PlayerError, ErrorStatusCode } from "./Structures/PlayerError";
export { QueryResolver } from "./utils/QueryResolver";
export { Queue } from "./Structures/Queue";
export { Track } from "./Structures/Track";
export { VoiceUtils } from "./VoiceInterface/VoiceUtils";
export { VoiceEvents, StreamDispatcher } from "./VoiceInterface/StreamDispatcher";
export { Util } from "./utils/Util";
export * from "./types/types";
export * from "./utils/FFmpegStream";
// eslint-disable-next-line @typescript-eslint/no-var-requires
export const version: string = require(`${__dirname}/../package.json`).version;

View file

@ -0,0 +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 };
if (typeof mod.VolumeTransformer.hasSmoothing !== "boolean") {
Reflect.set(mod, "VolumeTransformer", VolumeTransformerMock);
}
} catch {
/* do nothing */
}

View file

@ -0,0 +1,485 @@
import { Snowflake, User, UserResolvable } from "discord.js";
import { Readable, Duplex } from "stream";
import { Queue } from "../Structures/Queue";
import Track from "../Structures/Track";
import { Playlist } from "../Structures/Playlist";
import { StreamDispatcher } from "../VoiceInterface/StreamDispatcher";
export type FiltersName = keyof QueueFilters;
export interface PlayerSearchResult {
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;
}
/**
* The track source:
* - soundcloud
* - youtube
* - spotify
* - arbitrary
* @typedef {string} TrackSource
*/
export type TrackSource = "soundcloud" | "youtube" | "spotify" | "arbitrary";
/**
* @typedef {object} RawTrackData
* @property {string} title The title
* @property {string} description The description
* @property {string} author The author
* @property {string} url The url
* @property {string} thumbnail The thumbnail
* @property {string} duration The duration
* @property {number | boolean} views The views
* @property {User} requestedBy The user who requested this track
* @property {Playlist} [playlist] The playlist
* @property {TrackSource} [source="arbitrary"] The source
* @property {any} [engine] The engine
* @property {boolean} [live] If this track is live
* @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
}
/**
* @typedef {object} TimeData
* @property {number} days Time in days
* @property {number} hours Time in hours
* @property {number} minutes Time in minutes
* @property {number} seconds Time in seconds
*/
export interface TimeData {
days: number;
hours: number;
minutes: number;
seconds: number;
}
/**
* @typedef {object} PlayerProgressbarOptions
* @property {boolean} [timecodes] If it should render time codes
* @property {boolean} [queue] If it should create progress bar for the whole queue
* @property {number} [length] The bar length
* @property {string} [line] The bar track
* @property {string} [indicator] The indicator
*/
export interface PlayerProgressbarOptions {
timecodes?: boolean;
length?: number;
line?: string;
indicator?: string;
queue?: boolean;
}
/**
* @typedef {object} PlayerOptions
* @property {boolean} [leaveOnEnd=true] If it should leave on end
* @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 {YTDLDownloadOptions} [ytdlOptions] The youtube download options
* @property {number} [initialVolume=100] The initial player volume
* @property {number} [bufferingTimeout=3000] Buffering timeout for the stream
* @property {boolean} [spotifyBridge=true] If player should bridge spotify source to youtube
* @property {boolean} [disableVolume=false] If player should disable inline volume
* @property {number} [volumeSmoothness=0] The volume transition smoothness between volume changes (lower the value to get better result)
* Setting this or leaving this empty will disable this effect. Example: `volumeSmoothness: 0.1`
* @property {Function} [onBeforeCreateStream] Runs before creating stream
*/
export interface PlayerOptions {
leaveOnEnd?: boolean;
leaveOnStop?: boolean;
leaveOnEmpty?: boolean;
leaveOnEmptyCooldown?: number;
autoSelfDeaf?: boolean;
initialVolume?: number;
bufferingTimeout?: number;
spotifyBridge?: boolean;
disableVolume?: boolean;
volumeSmoothness?: number;
onBeforeCreateStream?: (track: Track, source: TrackSource, queue: Queue) => Promise<Readable>;
}
/**
* @typedef {object} ExtractorModelData
* @property {object} [playlist] The playlist info (if any)
* @property {string} [playlist.title] The playlist title
* @property {string} [playlist.description] The playlist description
* @property {string} [playlist.thumbnail] The playlist thumbnail
* @property {album|playlist} [playlist.type] The playlist type: `album` | `playlist`
* @property {TrackSource} [playlist.source] The playlist source
* @property {object} [playlist.author] The playlist author
* @property {string} [playlist.author.name] The author name
* @property {string} [playlist.author.url] The author url
* @property {string} [playlist.id] The playlist id
* @property {string} [playlist.url] The playlist url
* @property {any} [playlist.rawPlaylist] The raw data
* @property {ExtractorData[]} data The data
*/
/**
* @typedef {object} ExtractorData
* @property {string} title The title
* @property {number} duration The duration
* @property {string} thumbnail The thumbnail
* @property {string|Readable|Duplex} engine The stream engine
* @property {number} views The views count
* @property {string} author The author
* @property {string} description The description
* @property {string} url The url
* @property {string} [version] The extractor version
* @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;
}[];
}
/**
* The search query type
* This can be one of:
* - 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
* @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
}
/**
* Emitted when bot gets disconnected from a voice channel
* @event Player#botDisconnect
* @param {Queue} queue The queue
*/
/**
* Emitted when the voice channel is empty
* @event Player#channelEmpty
* @param {Queue} queue The queue
*/
/**
* Emitted when bot connects to a voice channel
* @event Player#connectionCreate
* @param {Queue} queue The queue
* @param {StreamDispatcher} connection The discord player connection object
*/
/**
* Debug information
* @event Player#debug
* @param {Queue} queue The queue
* @param {string} message The message
*/
/**
* Emitted on error
* <warn>This event should handled properly otherwise it may crash your process!</warn>
* @event Player#error
* @param {Queue} queue The queue
* @param {Error} error The error
*/
/**
* Emitted on connection error. Sometimes stream errors are emitted here as well.
* @event Player#connectionError
* @param {Queue} queue The queue
* @param {Error} error The error
*/
/**
* Emitted when queue ends
* @event Player#queueEnd
* @param {Queue} queue The queue
*/
/**
* Emitted when a single track is added
* @event Player#trackAdd
* @param {Queue} queue The queue
* @param {Track} track The track
*/
/**
* Emitted when multiple tracks are added
* @event Player#tracksAdd
* @param {Queue} queue The queue
* @param {Track[]} tracks The tracks
*/
/**
* Emitted when a track starts playing
* @event Player#trackStart
* @param {Queue} queue The queue
* @param {Track} track The track
*/
/**
* Emitted when a track ends
* @event Player#trackEnd
* @param {Queue} queue The queue
* @param {Track} track The track
*/
/* 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;
}
/* eslint-enable @typescript-eslint/no-explicit-any */
/**
* @typedef {object} PlayOptions
* @property {boolean} [filtersUpdate=false] If this play was triggered for filters update
* @property {string[]} [encoderArgs=[]] FFmpeg args passed to encoder
* @property {number} [seek] Time to seek to before playing
* @property {boolean} [immediate=false] If it should start playing the provided track immediately
*/
export interface PlayOptions {
filtersUpdate?: boolean;
encoderArgs?: string[];
seek?: number;
immediate?: boolean;
}
/**
* @typedef {object} SearchOptions
* @property {UserResolvable} requestedBy The user who requested this search
* @property {QueryType|string} [searchEngine=QueryType.AUTO] The query search engine, can be extractor name to target specific one (custom)
* @property {boolean} [blockExtractor=false] If it should block custom extractors
*/
export interface SearchOptions {
requestedBy: UserResolvable;
searchEngine?: QueryType | string;
blockExtractor?: boolean;
}
/**
* The queue repeat mode. This can be one of:
* - OFF
* - TRACK
* - QUEUE
* - AUTOPLAY
* @typedef {number} QueueRepeatMode
*/
export enum QueueRepeatMode {
OFF = 0,
TRACK = 1,
QUEUE = 2,
AUTOPLAY = 3
}
/**
* @typedef {object} PlaylistInitData
* @property {Track[]} tracks The tracks of this playlist
* @property {string} title The playlist title
* @property {string} description The description
* @property {string} thumbnail The thumbnail
* @property {album|playlist} type The playlist type: `album` | `playlist`
* @property {TrackSource} source The playlist source
* @property {object} author The playlist author
* @property {string} [author.name] The author name
* @property {string} [author.url] The author url
* @property {string} id The playlist id
* @property {string} url The playlist url
* @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
}
/**
* @typedef {object} TrackJSON
* @property {string} title The track title
* @property {string} description The track description
* @property {string} author The author
* @property {string} url The url
* @property {string} thumbnail The thumbnail
* @property {string} duration The duration
* @property {number} durationMS The duration in ms
* @property {number} views The views count
* @property {Snowflake} requestedBy The id of the user who requested this track
* @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;
}
/**
* @typedef {object} PlaylistJSON
* @property {string} id The playlist id
* @property {string} url The playlist url
* @property {string} title The playlist title
* @property {string} description The playlist description
* @property {string} thumbnail The thumbnail
* @property {album|playlist} type The playlist type: `album` | `playlist`
* @property {TrackSource} source The track source
* @property {object} author The playlist author
* @property {string} [author.name] The author name
* @property {string} [author.url] The author url
* @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[];
}
/**
* @typedef {object} PlayerInitOptions
* @property {boolean} [autoRegisterExtractor=true] If it should automatically register `@discord-player/extractor`
* @property {YTDLDownloadOptions} [ytdlOptions] The options passed to `ytdl-core`
* @property {number} [connectionTimeout=20000] The voice connection timeout
*/
export interface PlayerInitOptions {
autoRegisterExtractor?: boolean;
connectionTimeout?: number;
}

View file

@ -0,0 +1,107 @@
import { FiltersName } from "../types/types";
const bass = (g: number) => `bass=g=${g}:f=110:w=0.3`;
class AudioFilters {
public constructor() {
return AudioFilters;
}
public static get filters(): Record<FiltersName, string> {
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<K extends FiltersName>(name: K) {
return this.filters[name];
}
public static has<K extends FiltersName>(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 get names() {
return Object.keys(this.filters) as FiltersName[];
}
// @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
}
/**
* Create ffmpeg args from the specified filters name
* @param filter The filter name
* @returns
*/
public static create<K extends FiltersName>(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 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;
export { AudioFilters };

View file

@ -0,0 +1,59 @@
import { FFmpeg } from "prism-media";
import type { Duplex, Readable } from "stream";
export interface FFmpegStreamOptions {
fmt?: string;
encoderArgs?: string[];
seek?: number;
skip?: boolean;
}
export function FFMPEG_ARGS_STRING(stream: string, fmt?: string) {
// prettier-ignore
return [
"-reconnect", "1",
"-reconnect_streamed", "1",
"-reconnect_delay_max", "5",
"-i", stream,
"-analyzeduration", "0",
"-loglevel", "0",
"-f", `${typeof fmt === "string" ? fmt : "s16le"}`,
"-ar", "48000",
"-ac", "2"
];
}
export function FFMPEG_ARGS_PIPED(fmt?: string) {
// prettier-ignore
return [
"-analyzeduration", "0",
"-loglevel", "0",
"-f", `${typeof fmt === "string" ? fmt : "s16le"}`,
"-ar", "48000",
"-ac", "2"
];
}
/**
* Creates FFmpeg stream
* @param stream The source stream
* @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 (!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());
if (typeof stream !== "string") {
stream.on("error", () => transcoder.destroy());
stream.pipe(transcoder);
}
return transcoder;
}

View file

@ -0,0 +1,58 @@
import { QueryType } from "../types/types";
import play from "play-dl";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// scary things below *sigh*
const spotifySongRegex = /https?:\/\/(?:embed\.|open\.)(?:spotify\.com\/)(?:track\/|\?uri=spotify:track:)((\w|-){22})/;
const spotifyPlaylistRegex = /https?:\/\/(?:embed\.|open\.)(?:spotify\.com\/)(?:playlist\/|\?uri=spotify:playlist:)((\w|-){22})/;
const spotifyAlbumRegex = /https?:\/\/(?:embed\.|open\.)(?:spotify\.com\/)(?:album\/|\?uri=spotify:album:)((\w|-){22})/;
const vimeoRegex = /(http|https)?:\/\/(www\.|player\.)?vimeo\.com\/(?:channels\/(?:\w+\/)?|groups\/([^/]*)\/videos\/|video\/|)(\d+)(?:|\/\?)/;
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*)?$/;
// scary things above *sigh*
class QueryResolver {
/**
* 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<QueryType> {
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;
}
/**
* Parses vimeo id from url
* @param {string} query The query
* @returns {string}
*/
static async getVimeoID(query: string): Promise<string> {
return await QueryResolver.resolve(query) === QueryType.VIMEO
? query
.split("/")
.filter((x) => !!x)
.pop()
: null;
}
}
export { QueryResolver };

View file

@ -0,0 +1,117 @@
import { StageChannel, VoiceChannel } from "discord.js";
import { TimeData } from "../types/types";
class Util {
/**
* 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<string, number>) {
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;
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"];
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;
}
/**
* 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<T = any>(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;
}
/**
* 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<unknown>}
*/
static wait(time: number) {
return new Promise((r) => setTimeout(r, time).unref());
}
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?
}
}
}
}
}
export { Util };

View file

@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"declaration": true,
"outDir": "./dist",
"strict": true,
"strictNullChecks": false,
"esModuleInterop": true,
"pretty": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"importHelpers": true
},
"include": [
"src/**/*"
]
}

View file

@ -1,274 +0,0 @@
const playdl = require("play-dl"),
fallbackImage = "https://cdn.discordapp.com/attachments/708642702602010684/1012418217660121089/noimage.png";
// const fetch = require("node-fetch");
// const { getData, getPreview, getTracks } = require("spotify-url-info")(fetch);
/*
Thanks to: https://github.com/nizewn/Dodong
hello stranger,
as a result of my headaches while dealing with the discord-player extractor API,
what you will see here will mostly be poorly-written code.
this could use modularisation or some cleanup (i might do it once i have free time)
thanks :)
-nize
*/
module.exports = {
important: true,
validate: () => true, // true, since we're also using this for searches when query isnt a valid link
/**
*
* @param {String} query
* @returns
*/
getInfo: async (query) => {
// eslint-disable-next-line no-async-promise-executor, no-unused-vars
return new Promise(async (resolve, reject) => {
try {
// ---- start soundcloud ----
if (["track", "playlist"].includes(await playdl.so_validate(query))) {
const info = await playdl.soundcloud(query);
if (info.type === "track") {
const track = {
title: info.name,
duration: info.durationInMs,
thumbnail: info.thumbnail || fallbackImage,
async engine() {
return (await playdl.stream(info.url, { discordPlayerCompatibility: true })).stream;
},
views: 0,
author: info.publisher ? info.publisher.name ?? info.publisher.artist ?? info.publisher.writer_composer ?? null : null,
description: "",
url: info.url,
source: "soundcloud-custom"
};
return resolve({ playlist: null, info: [track] });
} else if (info.type === "playlist") {
const trackList = await info.all_tracks();
const tracks = await trackList.map(track => {
return {
title: track.name,
duration: track.durationInMs,
thumbnail: track.thumbnail || fallbackImage,
async engine() {
return (await playdl.stream(track.url, { discordPlayerCompatibility: true })).stream;
},
views: 0,
author: track.publisher ? track.publisher.name ?? track.publisher.artist ?? track.publisher.writer_composer ?? null : null,
description: "",
url: track.url,
source: "soundcloud-custom"
};
});
const playlist = {
title: info.name,
description: "",
thumbnail: info.user.thumbnail || fallbackImage,
type: "playlist",
source: "soundcloud-custom",
author: info.user.name,
id: info.id,
url: info.url,
rawPlaylist: info
};
return resolve({ playlist: playlist, info: tracks });
}
}
// ---- end soundcloud ----
/*
// ---- start spotify ----
if (query.includes("open.spotify.com") || query.includes("play.spotify.com")) {
const info = await getPreview(query);
if (info.type === "track") {
const spotifyTrack = await getData(query);
const track = {
title: info.title,
duration: spotifyTrack.duration_ms,
thumbnail: info.image,
async engine() {
return (await playdl.stream(await Youtube.search(`${info.artist} ${info.title} lyric`, { limit: 1, type: "video", safeSearch: true }).then(x => x[0] ? `https://youtu.be/${x[0].id}` : "https://youtu.be/Wch3gJG2GJ4"), { discordPlayerCompatibility: true })).stream;
},
views: 0,
author: info.artist,
description: "",
url: info.link,
source: "spotify-custom"
};
return resolve({ playlist: null, info: [track] });
} else if (["album", "artist", "playlist"].includes(info.type)) {
const trackList = await getTracks(query);
const tracks = trackList.map(track => {
return {
title: track.name,
duration: track.duration_ms,
thumbnail: track.album && track.album.images.length ? track.album.images[0].url : null,
async engine() {
return (await playdl.stream(await Youtube.search(`${track.artists[0].name} ${track.name} lyric`, { limit: 1, type: "video", safeSearch: true }).then(x => x[0] ? `https://youtu.be/${x[0].id}` : "https://youtu.be/Wch3gJG2GJ4"), { discordPlayerCompatibility: true })).stream;
},
views: 0,
author: track.artists ? track.artists[0].name : null,
description: "",
url: track.external_urls.spotify,
source: "spotify-custom"
};
});
const playlist = {
title: info.title,
description: "",
thumbnail: info.image,
type: info.type === "album" ? "album" : "playlist",
source: "spotify-custom",
author: info.artist,
id: null,
url: info.link,
rawPlaylist: info
};
return resolve({ playlist: playlist, info: tracks });
}
}
// ---- end spotify ----
*/
if (query.startsWith("http") && !query.includes("&list")) query = query.split("&")[0];
if (query.startsWith("http") && playdl.yt_validate(query) === "video") {
if (query.includes("music.youtube")) {
const info = await playdl.video_info(query);
if (!info) return resolve({ playlist: null, info: null });
const track = {
title: info.video_details.title,
duration: info.video_details.durationInSec * 1000,
thumbnail: info.video_details.thumbnails[0].url || fallbackImage,
async engine() {
return (await playdl.stream(`https://music.youtube.com/watch?v=${info.video_details.id}`, { discordPlayerCompatibility: true })).stream;
},
views: info.video_details.views,
author: info.video_details.channel.name,
description: "",
url: `https://music.youtube.com/watch?v=${info.video_details.id}`,
raw: info,
source: "youtube"
};
return resolve({ playlist: null, info: [track] });
}
const info = await playdl.video_info(query);
if (!info) return resolve({ playlist: null, info: null });
const track = {
title: info.video_details.title,
duration: info.video_details.durationInSec * 1000,
thumbnail: info.video_details.thumbnails[0].url || fallbackImage,
async engine() {
return (await playdl.stream(info.video_details.url, { discordPlayerCompatibility: true })).stream;
},
views: info.video_details.views,
author: info.video_details.channel.name,
description: "",
url: info.video_details.url,
raw: info,
source: "youtube"
};
return resolve({ playlist: null, info: [track] });
} else if (playdl.yt_validate(query) === "playlist") {
if (query.includes("music.youtube")) {
const info = await playdl.playlist_info(query, { incomplete: true });
const trackList = await info.videos;
const tracks = trackList.map(track => {
return {
title: track.title,
duration: track.durationInSec * 1000,
thumbnail: track.thumbnails[0].url || fallbackImage,
async engine() {
return (await playdl.stream(`https://music.youtube.com/watch?v=${track.id}`, { discordPlayerCompatibility: true })).stream;
},
views: track.views,
author: track.channel.name,
description: "",
url: track.url,
raw: info,
source: "youtube"
};
});
const playlist = {
title: info.title,
description: "",
thumbnail: info.thumbnail ? info.thumbnail.url : fallbackImage,
type: "playlist",
author: info.channel.name,
id: info.id,
url: info.url,
source: "youtube",
rawPlaylist: info
};
return resolve({ playlist: playlist, info: tracks });
}
const info = await playdl.playlist_info(query, { incomplete: true });
const trackList = await info.all_videos();
const tracks = trackList.map(track => {
return {
title: track.title,
duration: track.durationInSec * 1000,
thumbnail: track.thumbnails[0].url || fallbackImage,
async engine() {
return (await playdl.stream(track.url, { discordPlayerCompatibility: true })).stream;
},
views: track.views,
author: track.channel.name,
description: "",
url: track.url,
raw: info,
source: "youtube"
};
});
const playlist = {
title: info.title,
description: "",
thumbnail: info.thumbnail ? info.thumbnail.url : null,
type: "playlist",
source: "youtube",
author: info.channel.name,
id: info.id,
url: info.url,
rawPlaylist: info
};
return resolve({ playlist: playlist, info: tracks });
}
// search on youtube
const search = await playdl.search(query, { limit: 10 });
if (search) {
const found = search.map(track => {
return {
title: track.title,
duration: track.durationInSec * 1000,
thumbnail: track.thumbnails[0].url || fallbackImage,
async engine() {
return (await playdl.stream(track.url, { discordPlayerCompatibility: true })).stream;
},
views: track.views,
author: track.channel.name,
description: "search",
url: track.url,
raw: track,
source: "youtube"
};
});
return resolve({ playlist: null, info: found });
}
return resolve({ playlist: null, info: null });
} catch (error) {
console.log(`Extractor: An error occurred while attempting to resolve ${query} :\n${error}`);
return reject(error);
}
});
}
};

194
package-lock.json generated
View file

@ -20,7 +20,6 @@
"cron": "^2.1.0",
"discord-api-types": "^0.37.5",
"discord-giveaways": "^6.0.1",
"discord-player": "^5.3.0-dev.3",
"discord.js": "^14.3.0",
"ejs": "^3.1.3",
"express": "^4.17.1",
@ -33,7 +32,8 @@
"moment": "^2.26.0",
"mongoose": "^5.13.15",
"ms": "^2.1.3",
"play-dl": "^1.9.5"
"play-dl": "^1.9.5",
"tiny-typed-emitter": "^2.1.0"
},
"devDependencies": {
"eslint": "^8.23.0"
@ -1478,24 +1478,6 @@
"discord.js": ">=14.0.0"
}
},
"node_modules/discord-player": {
"version": "5.3.0-dev.3",
"resolved": "https://registry.npmjs.org/discord-player/-/discord-player-5.3.0-dev.3.tgz",
"integrity": "sha512-rrbEBS4mzCyIGk5S9E5O0XWnC4nub2cmBAFr9pZLH7RWBX4+z2nhAt5VwemgenmW/RJy1rseuZZ75WqJDnrHnw==",
"dependencies": {
"@discordjs/voice": "^0.11.0",
"libsodium-wrappers": "^0.7.10",
"soundcloud-scraper": "^5.0.3",
"spotify-url-info": "^3.1.2",
"tiny-typed-emitter": "^2.1.0",
"tslib": "^2.4.0",
"youtube-sr": "^4.2.0",
"ytdl-core": "^4.11.0"
},
"funding": {
"url": "https://github.com/Androz2091/discord-player?sponsor=1"
}
},
"node_modules/discord.js": {
"version": "14.3.0",
"resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.3.0.tgz",
@ -2503,11 +2485,6 @@
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
"integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ=="
},
"node_modules/himalaya": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/himalaya/-/himalaya-1.1.0.tgz",
"integrity": "sha512-LLase1dHCRMel68/HZTFft0N0wti0epHr3nNY7ynpLbyZpmrKMQ8YIpiOV77TM97cNpC8Wb2n6f66IRggwdWPw=="
},
"node_modules/html-encoding-sniffer": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz",
@ -2909,19 +2886,6 @@
"node": ">= 0.8.0"
}
},
"node_modules/libsodium": {
"version": "0.7.10",
"resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.7.10.tgz",
"integrity": "sha512-eY+z7hDrDKxkAK+QKZVNv92A5KYkxfvIshtBJkmg5TSiCnYqZP3i9OO9whE79Pwgm4jGaoHgkM4ao/b9Cyu4zQ=="
},
"node_modules/libsodium-wrappers": {
"version": "0.7.10",
"resolved": "https://registry.npmjs.org/libsodium-wrappers/-/libsodium-wrappers-0.7.10.tgz",
"integrity": "sha512-pO3F1Q9NPLB/MWIhehim42b/Fwb30JNScCNh8TcQ/kIc+qGLQch8ag8wb0keK3EP5kbGakk1H8Wwo7v+36rNQg==",
"dependencies": {
"libsodium": "^0.7.0"
}
},
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@ -2993,18 +2957,6 @@
"node": "*"
}
},
"node_modules/m3u8stream": {
"version": "0.8.6",
"resolved": "https://registry.npmjs.org/m3u8stream/-/m3u8stream-0.8.6.tgz",
"integrity": "sha512-LZj8kIVf9KCphiHmH7sbFQTVe4tOemb202fWwvJwR9W5ENW/1hxJN6ksAWGhQgSBSa3jyWhnjKU1Fw1GaOdbyA==",
"dependencies": {
"miniget": "^4.2.2",
"sax": "^1.2.4"
},
"engines": {
"node": ">=12"
}
},
"node_modules/make-dir": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
@ -3136,14 +3088,6 @@
"node": ">=4"
}
},
"node_modules/miniget": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/miniget/-/miniget-4.2.2.tgz",
"integrity": "sha512-a7voNL1N5lDMxvTMExOkg+Fq89jM2vY8pAi9ZEWzZtfNmdfP6RXkvUtFnCAXoCv2T9k1v/fUJVaAEuepGcvLYA==",
"engines": {
"node": ">=12"
}
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@ -4268,16 +4212,6 @@
"resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz",
"integrity": "sha512-VZBmZP8WU3sMOZm1bdgTadsQbcscK0UM8oKxKVBs4XAhUo2Xxzm/OFMGBkPusxw9xL3Uy8LrzEqGqJhclsr0yA=="
},
"node_modules/soundcloud-scraper": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/soundcloud-scraper/-/soundcloud-scraper-5.0.3.tgz",
"integrity": "sha512-AmS9KmK7mMaPVzHzBk40rANpAttZila3+iAet6EA47EeiTBUzVwjq4B+1LCOLtgPmzDSGk0qn+LZOEd5UhnZTQ==",
"dependencies": {
"cheerio": "^1.0.0-rc.10",
"m3u8stream": "^0.8.4",
"node-fetch": "^2.6.1"
}
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@ -4296,26 +4230,6 @@
"memory-pager": "^1.0.2"
}
},
"node_modules/spotify-uri": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/spotify-uri/-/spotify-uri-3.0.3.tgz",
"integrity": "sha512-mMstJ4dAMki6GbUjg94kp/h9ZH+7T7+ro/KUC00WVh+WKoLgMRrTKLkWMIwCZNO53Xa8DRHQw/6jwYtRZrVI3g==",
"engines": {
"node": ">= 12"
}
},
"node_modules/spotify-url-info": {
"version": "3.1.6",
"resolved": "https://registry.npmjs.org/spotify-url-info/-/spotify-url-info-3.1.6.tgz",
"integrity": "sha512-v8Hz8lNXB9DzwlgetiIoKHh9t0hc7/J2vi0s82qa3/5kiU0cs/LDYY7nooqFcQpGz5y+okvqU7+FFMnieA+S7w==",
"dependencies": {
"himalaya": "~1.1.0",
"spotify-uri": "~3.0.3"
},
"engines": {
"node": ">= 12"
}
},
"node_modules/statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
@ -4807,24 +4721,6 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/youtube-sr": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/youtube-sr/-/youtube-sr-4.3.4.tgz",
"integrity": "sha512-olSYcR80XigutCrePEXBX3/RJJrWfonJQj7+/ggBiWU0CzTDLE1q8+lpWTWCG0JpzhzILp/IB/Bq/glGqqr1TQ=="
},
"node_modules/ytdl-core": {
"version": "4.11.1",
"resolved": "https://registry.npmjs.org/ytdl-core/-/ytdl-core-4.11.1.tgz",
"integrity": "sha512-0H2hl8kv9JA50qEUewPVpDCloyEVkXZfKJ6o2RNmkngfiY99pGVqE7jQbMT6Rs1QwZpF8GiMB50VWXqivpzlgQ==",
"dependencies": {
"m3u8stream": "^0.8.6",
"miniget": "^4.2.2",
"sax": "^1.1.3"
},
"engines": {
"node": ">=12"
}
}
},
"dependencies": {
@ -5888,21 +5784,6 @@
"serialize-javascript": "^6.0.0"
}
},
"discord-player": {
"version": "5.3.0-dev.3",
"resolved": "https://registry.npmjs.org/discord-player/-/discord-player-5.3.0-dev.3.tgz",
"integrity": "sha512-rrbEBS4mzCyIGk5S9E5O0XWnC4nub2cmBAFr9pZLH7RWBX4+z2nhAt5VwemgenmW/RJy1rseuZZ75WqJDnrHnw==",
"requires": {
"@discordjs/voice": "^0.11.0",
"libsodium-wrappers": "^0.7.10",
"soundcloud-scraper": "^5.0.3",
"spotify-url-info": "^3.1.2",
"tiny-typed-emitter": "^2.1.0",
"tslib": "^2.4.0",
"youtube-sr": "^4.2.0",
"ytdl-core": "^4.11.0"
}
},
"discord.js": {
"version": "14.3.0",
"resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.3.0.tgz",
@ -6680,11 +6561,6 @@
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
"integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ=="
},
"himalaya": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/himalaya/-/himalaya-1.1.0.tgz",
"integrity": "sha512-LLase1dHCRMel68/HZTFft0N0wti0epHr3nNY7ynpLbyZpmrKMQ8YIpiOV77TM97cNpC8Wb2n6f66IRggwdWPw=="
},
"html-encoding-sniffer": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz",
@ -6971,19 +6847,6 @@
"type-check": "~0.4.0"
}
},
"libsodium": {
"version": "0.7.10",
"resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.7.10.tgz",
"integrity": "sha512-eY+z7hDrDKxkAK+QKZVNv92A5KYkxfvIshtBJkmg5TSiCnYqZP3i9OO9whE79Pwgm4jGaoHgkM4ao/b9Cyu4zQ=="
},
"libsodium-wrappers": {
"version": "0.7.10",
"resolved": "https://registry.npmjs.org/libsodium-wrappers/-/libsodium-wrappers-0.7.10.tgz",
"integrity": "sha512-pO3F1Q9NPLB/MWIhehim42b/Fwb30JNScCNh8TcQ/kIc+qGLQch8ag8wb0keK3EP5kbGakk1H8Wwo7v+36rNQg==",
"requires": {
"libsodium": "^0.7.0"
}
},
"locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@ -7037,15 +6900,6 @@
"resolved": "https://registry.npmjs.org/luxon/-/luxon-1.28.0.tgz",
"integrity": "sha512-TfTiyvZhwBYM/7QdAVDh+7dBTBA29v4ik0Ce9zda3Mnf8on1S5KJI8P2jKFZ8+5C0jhmr0KwJEO/Wdpm0VeWJQ=="
},
"m3u8stream": {
"version": "0.8.6",
"resolved": "https://registry.npmjs.org/m3u8stream/-/m3u8stream-0.8.6.tgz",
"integrity": "sha512-LZj8kIVf9KCphiHmH7sbFQTVe4tOemb202fWwvJwR9W5ENW/1hxJN6ksAWGhQgSBSa3jyWhnjKU1Fw1GaOdbyA==",
"requires": {
"miniget": "^4.2.2",
"sax": "^1.2.4"
}
},
"make-dir": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
@ -7139,11 +6993,6 @@
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
"integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="
},
"miniget": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/miniget/-/miniget-4.2.2.tgz",
"integrity": "sha512-a7voNL1N5lDMxvTMExOkg+Fq89jM2vY8pAi9ZEWzZtfNmdfP6RXkvUtFnCAXoCv2T9k1v/fUJVaAEuepGcvLYA=="
},
"minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@ -7938,16 +7787,6 @@
"resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz",
"integrity": "sha512-VZBmZP8WU3sMOZm1bdgTadsQbcscK0UM8oKxKVBs4XAhUo2Xxzm/OFMGBkPusxw9xL3Uy8LrzEqGqJhclsr0yA=="
},
"soundcloud-scraper": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/soundcloud-scraper/-/soundcloud-scraper-5.0.3.tgz",
"integrity": "sha512-AmS9KmK7mMaPVzHzBk40rANpAttZila3+iAet6EA47EeiTBUzVwjq4B+1LCOLtgPmzDSGk0qn+LZOEd5UhnZTQ==",
"requires": {
"cheerio": "^1.0.0-rc.10",
"m3u8stream": "^0.8.4",
"node-fetch": "^2.6.1"
}
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@ -7963,20 +7802,6 @@
"memory-pager": "^1.0.2"
}
},
"spotify-uri": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/spotify-uri/-/spotify-uri-3.0.3.tgz",
"integrity": "sha512-mMstJ4dAMki6GbUjg94kp/h9ZH+7T7+ro/KUC00WVh+WKoLgMRrTKLkWMIwCZNO53Xa8DRHQw/6jwYtRZrVI3g=="
},
"spotify-url-info": {
"version": "3.1.6",
"resolved": "https://registry.npmjs.org/spotify-url-info/-/spotify-url-info-3.1.6.tgz",
"integrity": "sha512-v8Hz8lNXB9DzwlgetiIoKHh9t0hc7/J2vi0s82qa3/5kiU0cs/LDYY7nooqFcQpGz5y+okvqU7+FFMnieA+S7w==",
"requires": {
"himalaya": "~1.1.0",
"spotify-uri": "~3.0.3"
}
},
"statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
@ -8348,21 +8173,6 @@
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
"dev": true
},
"youtube-sr": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/youtube-sr/-/youtube-sr-4.3.4.tgz",
"integrity": "sha512-olSYcR80XigutCrePEXBX3/RJJrWfonJQj7+/ggBiWU0CzTDLE1q8+lpWTWCG0JpzhzILp/IB/Bq/glGqqr1TQ=="
},
"ytdl-core": {
"version": "4.11.1",
"resolved": "https://registry.npmjs.org/ytdl-core/-/ytdl-core-4.11.1.tgz",
"integrity": "sha512-0H2hl8kv9JA50qEUewPVpDCloyEVkXZfKJ6o2RNmkngfiY99pGVqE7jQbMT6Rs1QwZpF8GiMB50VWXqivpzlgQ==",
"requires": {
"m3u8stream": "^0.8.6",
"miniget": "^4.2.2",
"sax": "^1.1.3"
}
}
}
}

View file

@ -23,7 +23,6 @@
"cron": "^2.1.0",
"discord-api-types": "^0.37.5",
"discord-giveaways": "^6.0.1",
"discord-player": "^5.3.0-dev.3",
"discord.js": "^14.3.0",
"ejs": "^3.1.3",
"express": "^4.17.1",
@ -36,7 +35,8 @@
"moment": "^2.26.0",
"mongoose": "^5.13.15",
"ms": "^2.1.3",
"play-dl": "^1.9.5"
"play-dl": "^1.9.5",
"tiny-typed-emitter": "^2.1.0"
},
"devDependencies": {
"eslint": "^8.23.0"