Merge pull request #1210 from Androz2091/next
✏️ discord.js v14 support
This commit is contained in:
commit
42ef4d1436
30 changed files with 1050 additions and 4145 deletions
|
@ -1,4 +1,4 @@
|
||||||
example/
|
examples/
|
||||||
node_modules/
|
node_modules/
|
||||||
dist/
|
dist/
|
||||||
.github/
|
.github/
|
||||||
|
|
1
.gitattributes
vendored
1
.gitattributes
vendored
|
@ -1 +1,2 @@
|
||||||
* text=auto eol=lf
|
* text=auto eol=lf
|
||||||
|
*.js,*.mjs linguist-language=TypeScript
|
4
.github/workflows/docs-deploy.yml
vendored
4
.github/workflows/docs-deploy.yml
vendored
|
@ -15,10 +15,10 @@ jobs:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@master
|
uses: actions/checkout@master
|
||||||
|
|
||||||
- name: Install Node v14
|
- name: Install Node v18
|
||||||
uses: actions/setup-node@master
|
uses: actions/setup-node@master
|
||||||
with:
|
with:
|
||||||
node-version: 14
|
node-version: 18
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm install
|
run: npm install
|
||||||
|
|
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -9,8 +9,4 @@ dist
|
||||||
# Yarn logs
|
# Yarn logs
|
||||||
yarn*.log
|
yarn*.log
|
||||||
|
|
||||||
# example
|
dev/
|
||||||
example/test
|
|
||||||
example/music-bot/node_modules
|
|
||||||
example/music-bot/package-lock.json
|
|
||||||
example/music-bot/.env
|
|
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
[submodule "examples/music-bot"]
|
||||||
|
path = examples/music-bot
|
||||||
|
url = https://github.com/Androz2091/discord-music-bot
|
42
README.md
42
README.md
|
@ -47,34 +47,33 @@ First of all, you will need to register slash commands:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const { REST } = require("@discordjs/rest");
|
const { REST } = require("@discordjs/rest");
|
||||||
const { Routes } = require("discord-api-types/v9");
|
const { Routes, ApplicationCommandOptionType } = require("discord.js");
|
||||||
|
|
||||||
const commands = [{
|
const commands = [
|
||||||
|
{
|
||||||
name: "play",
|
name: "play",
|
||||||
description: "Plays a song!",
|
description: "Plays a song!",
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
name: "query",
|
name: "query",
|
||||||
type: "STRING",
|
type: ApplicationCommandOptionType.String,
|
||||||
description: "The song you want to play",
|
description: "The song you want to play",
|
||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}];
|
}
|
||||||
|
];
|
||||||
|
|
||||||
const rest = new REST({ version: "9" }).setToken(process.env.DISCORD_TOKEN);
|
const rest = new REST({ version: "10" }).setToken("BOT_TOKEN");
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
console.log("Started refreshing application [/] commands.");
|
console.log("Started refreshing application [/] commands.");
|
||||||
|
|
||||||
await rest.put(
|
await rest.put(Routes.applicationGuildCommands(CLIENT_ID, GUILD_ID), { body: commands });
|
||||||
Routes.applicationGuildCommands(CLIENT_ID, GUILD_ID),
|
|
||||||
{ body: commands },
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log("Successfully reloaded application [/] commands.");
|
console.log("Successfully reloaded application [/] commands.");
|
||||||
} catch (error) {
|
} catch(error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
@ -83,8 +82,13 @@ const rest = new REST({ version: "9" }).setToken(process.env.DISCORD_TOKEN);
|
||||||
Now you can implement your bot's logic:
|
Now you can implement your bot's logic:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const { Client, Intents } = require("discord.js");
|
const { Client } = require("discord.js");
|
||||||
const client = new Discord.Client({ intents: [Intents.FLAGS.GUILDS, Intents.FLAGS.GUILD_MESSAGES, Intents.FLAGS.GUILD_VOICE_STATES] });
|
const client = new Discord.Client({
|
||||||
|
intents: [
|
||||||
|
"Guilds",
|
||||||
|
"GuildVoiceStates"
|
||||||
|
]
|
||||||
|
});
|
||||||
const { Player } = require("discord-player");
|
const { Player } = require("discord-player");
|
||||||
|
|
||||||
// Create a new Player (you don't need any API Key)
|
// Create a new Player (you don't need any API Key)
|
||||||
|
@ -98,14 +102,14 @@ client.once("ready", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
client.on("interactionCreate", async (interaction) => {
|
client.on("interactionCreate", async (interaction) => {
|
||||||
if (!interaction.isCommand()) return;
|
if (!interaction.isChatInputCommand()) return;
|
||||||
|
|
||||||
// /play track:Despacito
|
// /play track:Despacito
|
||||||
// will play "Despacito" in the voice channel
|
// will play "Despacito" in the voice channel
|
||||||
if (interaction.commandName === "play") {
|
if (interaction.commandName === "play") {
|
||||||
if (!interaction.member.voice.channelId) return await interaction.reply({ content: "You are not in a voice channel!", ephemeral: true });
|
if (!interaction.member.voice.channelId) return await interaction.reply({ content: "You are not in a voice channel!", ephemeral: true });
|
||||||
if (interaction.guild.me.voice.channelId && interaction.member.voice.channelId !== interaction.guild.me.voice.channelId) return await interaction.reply({ content: "You are not in my voice channel!", ephemeral: true });
|
if (interaction.guild.members.me.voice.channelId && interaction.member.voice.channelId !== interaction.guild.members.me.voice.channelId) return await interaction.reply({ content: "You are not in my voice channel!", ephemeral: true });
|
||||||
const query = interaction.options.get("query").value;
|
const query = interaction.options.getString("query");
|
||||||
const queue = player.createQueue(interaction.guild, {
|
const queue = player.createQueue(interaction.guild, {
|
||||||
metadata: {
|
metadata: {
|
||||||
channel: interaction.channel
|
channel: interaction.channel
|
||||||
|
@ -132,7 +136,7 @@ client.on("interactionCreate", async (interaction) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
client.login(process.env.DISCORD_TOKEN);
|
client.login("BOT_TOKEN");
|
||||||
```
|
```
|
||||||
|
|
||||||
## Supported websites
|
## Supported websites
|
||||||
|
@ -143,14 +147,14 @@ By default, discord-player supports **YouTube**, **Spotify** and **SoundCloud**
|
||||||
|
|
||||||
Discord Player provides an **Extractor API** that enables you to use your custom stream extractor with it. Some packages have been made by the community to add new features using this API.
|
Discord Player provides an **Extractor API** that enables you to use your custom stream extractor with it. Some packages have been made by the community to add new features using this API.
|
||||||
|
|
||||||
#### [@discord-player/extractor](https://github.com/Snowflake107/discord-player-extractors) (optional)
|
#### [@discord-player/extractor](https://github.com/DevAndromeda/discord-player-extractors) (optional)
|
||||||
|
|
||||||
Optional package that adds support for `vimeo`, `reverbnation`, `facebook`, `attachment links` and `lyrics`.
|
Optional package that adds support for `vimeo`, `reverbnation`, `facebook`, `attachment links` and `lyrics`.
|
||||||
You just need to install it using `npm i --save @discord-player/extractor` (discord-player will automatically detect and use it).
|
You just need to install it using `npm i --save @discord-player/extractor` (discord-player will automatically detect and use it).
|
||||||
|
|
||||||
#### [@discord-player/downloader](https://github.com/DevSnowflake/discord-player-downloader) (optional)
|
#### [@discord-player/downloader](https://github.com/DevAndromeda/discord-player-downloader) (optional)
|
||||||
|
|
||||||
`@discord-player/downloader` is an optional package that brings support for +700 websites. The documentation is available [here](https://github.com/DevSnowflake/discord-player-downloader).
|
`@discord-player/downloader` is an optional package that brings support for +700 websites. The documentation is available [here](https://github.com/DevAndromeda/discord-player-downloader).
|
||||||
|
|
||||||
## Examples of bots made with Discord Player
|
## Examples of bots made with Discord Player
|
||||||
|
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
# Discord Player Examples
|
|
||||||
This section contains example bot(s) made with Discord Player.
|
|
|
@ -1,2 +0,0 @@
|
||||||
# Music Bot
|
|
||||||
Slash commands music bot backed by **[Discord Player](https://discord-player.js.org)**.
|
|
|
@ -1,3 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
token: process.env.DISCORD_TOKEN
|
|
||||||
};
|
|
|
@ -1,331 +0,0 @@
|
||||||
require("dotenv").config({
|
|
||||||
path: __dirname+"/.env"
|
|
||||||
});
|
|
||||||
const { Client, GuildMember, Intents } = require("discord.js");
|
|
||||||
const config = require("./config");
|
|
||||||
const { Player, QueryType, QueueRepeatMode } = require("discord-player");
|
|
||||||
const client = new Client({
|
|
||||||
intents: [Intents.FLAGS.GUILD_VOICE_STATES, Intents.FLAGS.GUILD_MESSAGES, Intents.FLAGS.GUILDS]
|
|
||||||
});
|
|
||||||
|
|
||||||
client.on("ready", () => {
|
|
||||||
console.log("Bot is online!");
|
|
||||||
client.user.setActivity({
|
|
||||||
name: "🎶 | Music Time",
|
|
||||||
type: "LISTENING"
|
|
||||||
});
|
|
||||||
});
|
|
||||||
client.on("error", console.error);
|
|
||||||
client.on("warn", console.warn);
|
|
||||||
|
|
||||||
// instantiate the player
|
|
||||||
const player = new Player(client, {
|
|
||||||
ytdlOptions: {
|
|
||||||
headers: {
|
|
||||||
cookie: process.env.YT_COOKIE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
player.on("error", (queue, error) => {
|
|
||||||
console.log(`[${queue.guild.name}] Error emitted from the queue: ${error.message}`);
|
|
||||||
});
|
|
||||||
player.on("connectionError", (queue, error) => {
|
|
||||||
console.log(`[${queue.guild.name}] Error emitted from the connection: ${error.message}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
player.on("trackStart", (queue, track) => {
|
|
||||||
queue.metadata.send(`🎶 | Started playing: **${track.title}** in **${queue.connection.channel.name}**!`);
|
|
||||||
});
|
|
||||||
|
|
||||||
player.on("trackAdd", (queue, track) => {
|
|
||||||
queue.metadata.send(`🎶 | Track **${track.title}** queued!`);
|
|
||||||
});
|
|
||||||
|
|
||||||
player.on("botDisconnect", (queue) => {
|
|
||||||
queue.metadata.send("❌ | I was manually disconnected from the voice channel, clearing queue!");
|
|
||||||
});
|
|
||||||
|
|
||||||
player.on("channelEmpty", (queue) => {
|
|
||||||
queue.metadata.send("❌ | Nobody is in the voice channel, leaving...");
|
|
||||||
});
|
|
||||||
|
|
||||||
player.on("queueEnd", (queue) => {
|
|
||||||
queue.metadata.send("✅ | Queue finished!");
|
|
||||||
});
|
|
||||||
|
|
||||||
client.on("messageCreate", async (message) => {
|
|
||||||
if (message.author.bot || !message.guild) return;
|
|
||||||
if (!client.application?.owner) await client.application?.fetch();
|
|
||||||
|
|
||||||
if (message.content === "!deploy" && message.author.id === client.application?.owner?.id) {
|
|
||||||
await message.guild.commands.set([
|
|
||||||
{
|
|
||||||
name: "play",
|
|
||||||
description: "Plays a song from youtube",
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
name: "query",
|
|
||||||
type: "STRING",
|
|
||||||
description: "The song you want to play",
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "soundcloud",
|
|
||||||
description: "Plays a song from soundcloud",
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
name: "query",
|
|
||||||
type: "STRING",
|
|
||||||
description: "The song you want to play",
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "volume",
|
|
||||||
description: "Sets music volume",
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
name: "amount",
|
|
||||||
type: "INTEGER",
|
|
||||||
description: "The volume amount to set (0-100)",
|
|
||||||
required: false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "loop",
|
|
||||||
description: "Sets loop mode",
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
name: "mode",
|
|
||||||
type: "INTEGER",
|
|
||||||
description: "Loop type",
|
|
||||||
required: true,
|
|
||||||
choices: [
|
|
||||||
{
|
|
||||||
name: "Off",
|
|
||||||
value: QueueRepeatMode.OFF
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Track",
|
|
||||||
value: QueueRepeatMode.TRACK
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Queue",
|
|
||||||
value: QueueRepeatMode.QUEUE
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Autoplay",
|
|
||||||
value: QueueRepeatMode.AUTOPLAY
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "skip",
|
|
||||||
description: "Skip to the current song"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "queue",
|
|
||||||
description: "See the queue"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "pause",
|
|
||||||
description: "Pause the current song"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "resume",
|
|
||||||
description: "Resume the current song"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "stop",
|
|
||||||
description: "Stop the player"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "np",
|
|
||||||
description: "Now Playing"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "bassboost",
|
|
||||||
description: "Toggles bassboost filter"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "ping",
|
|
||||||
description: "Shows bot latency"
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
await message.reply("Deployed!");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
client.on("interactionCreate", async (interaction) => {
|
|
||||||
if (!interaction.isCommand() || !interaction.guildId) return;
|
|
||||||
|
|
||||||
if (interaction.commandName === "ping") {
|
|
||||||
await interaction.deferReply();
|
|
||||||
const queue = player.getQueue(interaction.guild);
|
|
||||||
|
|
||||||
return void interaction.followUp({
|
|
||||||
embeds: [
|
|
||||||
{
|
|
||||||
title: "⏱️ | Latency",
|
|
||||||
fields: [
|
|
||||||
{ name: "Bot Latency", value: `\`${Math.round(client.ws.ping)}ms\`` },
|
|
||||||
{ name: "Voice Latency", value: !queue ? "N/A" : `UDP: \`${queue.connection.voiceConnection.ping.udp ?? "N/A"}\`ms\nWebSocket: \`${queue.connection.voiceConnection.ping.ws ?? "N/A"}\`ms` }
|
|
||||||
],
|
|
||||||
color: 0xFFFFFF
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(interaction.member instanceof GuildMember) || !interaction.member.voice.channel) {
|
|
||||||
return void interaction.reply({ content: "You are not in a voice channel!", ephemeral: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (interaction.guild.me.voice.channelId && interaction.member.voice.channelId !== interaction.guild.me.voice.channelId) {
|
|
||||||
return void interaction.reply({ content: "You are not in my voice channel!", ephemeral: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (interaction.commandName === "play" || interaction.commandName === "soundcloud") {
|
|
||||||
await interaction.deferReply();
|
|
||||||
|
|
||||||
const query = interaction.options.get("query").value;
|
|
||||||
const searchResult = await player
|
|
||||||
.search(query, {
|
|
||||||
requestedBy: interaction.user,
|
|
||||||
searchEngine: interaction.commandName === "soundcloud" ? QueryType.SOUNDCLOUD_SEARCH : QueryType.AUTO
|
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
if (!searchResult || !searchResult.tracks.length) return void interaction.followUp({ content: "No results were found!" });
|
|
||||||
|
|
||||||
const queue = await player.createQueue(interaction.guild, {
|
|
||||||
metadata: interaction.channel
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!queue.connection) await queue.connect(interaction.member.voice.channel);
|
|
||||||
} catch {
|
|
||||||
void player.deleteQueue(interaction.guildId);
|
|
||||||
return void interaction.followUp({ content: "Could not join your voice channel!" });
|
|
||||||
}
|
|
||||||
|
|
||||||
await interaction.followUp({ content: `⏱ | Loading your ${searchResult.playlist ? "playlist" : "track"}...` });
|
|
||||||
searchResult.playlist ? queue.addTracks(searchResult.tracks) : queue.addTrack(searchResult.tracks[0]);
|
|
||||||
if (!queue.playing) await queue.play();
|
|
||||||
} else if (interaction.commandName === "volume") {
|
|
||||||
await interaction.deferReply();
|
|
||||||
const queue = player.getQueue(interaction.guildId);
|
|
||||||
if (!queue || !queue.playing) return void interaction.followUp({ content: "❌ | No music is being played!" });
|
|
||||||
const vol = interaction.options.get("amount");
|
|
||||||
if (!vol) return void interaction.followUp({ content: `🎧 | Current volume is **${queue.volume}**%!` });
|
|
||||||
if ((vol.value) < 0 || (vol.value) > 100) return void interaction.followUp({ content: "❌ | Volume range must be 0-100" });
|
|
||||||
const success = queue.setVolume(vol.value);
|
|
||||||
return void interaction.followUp({
|
|
||||||
content: success ? `✅ | Volume set to **${vol.value}%**!` : "❌ | Something went wrong!"
|
|
||||||
});
|
|
||||||
} else if (interaction.commandName === "skip") {
|
|
||||||
await interaction.deferReply();
|
|
||||||
const queue = player.getQueue(interaction.guildId);
|
|
||||||
if (!queue || !queue.playing) return void interaction.followUp({ content: "❌ | No music is being played!" });
|
|
||||||
const currentTrack = queue.current;
|
|
||||||
const success = queue.skip();
|
|
||||||
return void interaction.followUp({
|
|
||||||
content: success ? `✅ | Skipped **${currentTrack}**!` : "❌ | Something went wrong!"
|
|
||||||
});
|
|
||||||
} else if (interaction.commandName === "queue") {
|
|
||||||
await interaction.deferReply();
|
|
||||||
const queue = player.getQueue(interaction.guildId);
|
|
||||||
if (!queue || !queue.playing) return void interaction.followUp({ content: "❌ | No music is being played!" });
|
|
||||||
const currentTrack = queue.current;
|
|
||||||
const tracks = queue.tracks.slice(0, 10).map((m, i) => {
|
|
||||||
return `${i + 1}. **${m.title}** ([link](${m.url}))`;
|
|
||||||
});
|
|
||||||
|
|
||||||
return void interaction.followUp({
|
|
||||||
embeds: [
|
|
||||||
{
|
|
||||||
title: "Server Queue",
|
|
||||||
description: `${tracks.join("\n")}${
|
|
||||||
queue.tracks.length > tracks.length
|
|
||||||
? `\n...${queue.tracks.length - tracks.length === 1 ? `${queue.tracks.length - tracks.length} more track` : `${queue.tracks.length - tracks.length} more tracks`}`
|
|
||||||
: ""
|
|
||||||
}`,
|
|
||||||
color: 0xff0000,
|
|
||||||
fields: [{ name: "Now Playing", value: `🎶 | **${currentTrack.title}** ([link](${currentTrack.url}))` }]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
} else if (interaction.commandName === "pause") {
|
|
||||||
await interaction.deferReply();
|
|
||||||
const queue = player.getQueue(interaction.guildId);
|
|
||||||
if (!queue || !queue.playing) return void interaction.followUp({ content: "❌ | No music is being played!" });
|
|
||||||
const paused = queue.setPaused(true);
|
|
||||||
return void interaction.followUp({ content: paused ? "⏸ | Paused!" : "❌ | Something went wrong!" });
|
|
||||||
} else if (interaction.commandName === "resume") {
|
|
||||||
await interaction.deferReply();
|
|
||||||
const queue = player.getQueue(interaction.guildId);
|
|
||||||
if (!queue || !queue.playing) return void interaction.followUp({ content: "❌ | No music is being played!" });
|
|
||||||
const paused = queue.setPaused(false);
|
|
||||||
return void interaction.followUp({ content: !paused ? "❌ | Something went wrong!" : "▶ | Resumed!" });
|
|
||||||
} else if (interaction.commandName === "stop") {
|
|
||||||
await interaction.deferReply();
|
|
||||||
const queue = player.getQueue(interaction.guildId);
|
|
||||||
if (!queue || !queue.playing) return void interaction.followUp({ content: "❌ | No music is being played!" });
|
|
||||||
queue.destroy();
|
|
||||||
return void interaction.followUp({ content: "🛑 | Stopped the player!" });
|
|
||||||
} else if (interaction.commandName === "np") {
|
|
||||||
await interaction.deferReply();
|
|
||||||
const queue = player.getQueue(interaction.guildId);
|
|
||||||
if (!queue || !queue.playing) return void interaction.followUp({ content: "❌ | No music is being played!" });
|
|
||||||
const progress = queue.createProgressBar();
|
|
||||||
const perc = queue.getPlayerTimestamp();
|
|
||||||
|
|
||||||
return void interaction.followUp({
|
|
||||||
embeds: [
|
|
||||||
{
|
|
||||||
title: "Now Playing",
|
|
||||||
description: `🎶 | **${queue.current.title}**! (\`${perc.progress}%\`)`,
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
name: "\u200b",
|
|
||||||
value: progress
|
|
||||||
}
|
|
||||||
],
|
|
||||||
color: 0xffffff
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
} else if (interaction.commandName === "loop") {
|
|
||||||
await interaction.deferReply();
|
|
||||||
const queue = player.getQueue(interaction.guildId);
|
|
||||||
if (!queue || !queue.playing) return void interaction.followUp({ content: "❌ | No music is being played!" });
|
|
||||||
const loopMode = interaction.options.get("mode").value;
|
|
||||||
const success = queue.setRepeatMode(loopMode);
|
|
||||||
const mode = loopMode === QueueRepeatMode.TRACK ? "🔂" : loopMode === QueueRepeatMode.QUEUE ? "🔁" : "▶";
|
|
||||||
return void interaction.followUp({ content: success ? `${mode} | Updated loop mode!` : "❌ | Could not update loop mode!" });
|
|
||||||
} else if (interaction.commandName === "bassboost") {
|
|
||||||
await interaction.deferReply();
|
|
||||||
const queue = player.getQueue(interaction.guildId);
|
|
||||||
if (!queue || !queue.playing) return void interaction.followUp({ content: "❌ | No music is being played!" });
|
|
||||||
await queue.setFilters({
|
|
||||||
bassboost: !queue.getFiltersEnabled().includes("bassboost"),
|
|
||||||
normalizer2: !queue.getFiltersEnabled().includes("bassboost") // because we need to toggle it with bass
|
|
||||||
});
|
|
||||||
|
|
||||||
return void interaction.followUp({ content: `🎵 | Bassboost ${queue.getFiltersEnabled().includes("bassboost") ? "Enabled" : "Disabled"}!` });
|
|
||||||
} else {
|
|
||||||
interaction.reply({
|
|
||||||
content: "Unknown command!",
|
|
||||||
ephemeral: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
client.login(config.token);
|
|
|
@ -1,17 +0,0 @@
|
||||||
{
|
|
||||||
"name": "music-bot",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "Simple music bot created with discord-player",
|
|
||||||
"main": "index.js",
|
|
||||||
"scripts": {
|
|
||||||
"start": "node index.js"
|
|
||||||
},
|
|
||||||
"author": "Snowflake107",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@discordjs/opus": "^0.5.3",
|
|
||||||
"discord-player": "^5.2.1",
|
|
||||||
"discord.js": "^13.0.1",
|
|
||||||
"dotenv": "^10.0.0"
|
|
||||||
}
|
|
||||||
}
|
|
3
examples/README.md
Normal file
3
examples/README.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# Discord Player Examples
|
||||||
|
|
||||||
|
Examples for **[discord-player](https://github.com/Androz2091/discord-player)**.
|
1
examples/music-bot
Submodule
1
examples/music-bot
Submodule
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit cae67e4e466b5884435572b9968859bc88220408
|
17
jsdoc.json
17
jsdoc.json
|
@ -1,17 +0,0 @@
|
||||||
{
|
|
||||||
"source": {
|
|
||||||
"includePattern": ".+\\.ts(doc|x)?$"
|
|
||||||
},
|
|
||||||
"plugins": [
|
|
||||||
"plugins/markdown",
|
|
||||||
"node_modules/jsdoc-babel"
|
|
||||||
],
|
|
||||||
"babel": {
|
|
||||||
"extensions": ["ts"],
|
|
||||||
"babelrc": false,
|
|
||||||
"presets": [
|
|
||||||
["@babel/preset-env", { "targets": { "node": true } }],
|
|
||||||
"@babel/preset-typescript"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
57
package.json
57
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "discord-player",
|
"name": "discord-player",
|
||||||
"version": "5.2.2",
|
"version": "5.3.0-dev",
|
||||||
"description": "Complete framework to facilitate music commands using discord.js",
|
"description": "Complete framework to facilitate music commands using discord.js",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
|
@ -23,9 +23,9 @@
|
||||||
"build:check": "tsc --noEmit --incremental false",
|
"build:check": "tsc --noEmit --incremental false",
|
||||||
"prepublishOnly": "rollup-type-bundler -e stream",
|
"prepublishOnly": "rollup-type-bundler -e stream",
|
||||||
"build:esm": "gen-esm-wrapper ./dist/index.js ./dist/index.mjs",
|
"build:esm": "gen-esm-wrapper ./dist/index.js ./dist/index.mjs",
|
||||||
"format": "prettier --write \"src/**/*.ts\" \"example/**/*.ts\"",
|
"format": "prettier --write \"src/**/*.ts\"",
|
||||||
"docs": "docgen --jsdoc jsdoc.json --source src/*.ts src/**/*.ts --custom docs/index.yml --output docs/docs.json",
|
"docs": "typedoc --json docs/typedoc.json src/index.ts",
|
||||||
"docs:test": "docgen --jsdoc jsdoc.json --source src/*.ts src/**/*.ts --custom docs/index.yml",
|
"postdocs": "node scripts/docgen.js",
|
||||||
"lint": "eslint src --ext .ts",
|
"lint": "eslint src --ext .ts",
|
||||||
"prepare": "husky install",
|
"prepare": "husky install",
|
||||||
"lint:fix": "eslint src --ext .ts --fix"
|
"lint:fix": "eslint src --ext .ts --fix"
|
||||||
|
@ -65,37 +65,32 @@
|
||||||
},
|
},
|
||||||
"homepage": "https://discord-player.js.org",
|
"homepage": "https://discord-player.js.org",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@discordjs/voice": "^0.8.0",
|
"@discordjs/voice": "^0.11.0",
|
||||||
"discord-ytdl-core": "^5.0.4",
|
"libsodium-wrappers": "^0.7.10",
|
||||||
"libsodium-wrappers": "^0.7.9",
|
"soundcloud-scraper": "^5.0.3",
|
||||||
"soundcloud-scraper": "^5.0.2",
|
"spotify-url-info": "^3.1.2",
|
||||||
"spotify-url-info": "^2.2.5",
|
|
||||||
"tiny-typed-emitter": "^2.1.0",
|
"tiny-typed-emitter": "^2.1.0",
|
||||||
"youtube-sr": "^4.1.13",
|
"tslib": "^2.4.0",
|
||||||
"ytdl-core": "^4.10.0"
|
"youtube-sr": "^4.3.0",
|
||||||
|
"ytdl-core": "^4.11.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/cli": "^7.16.0",
|
"@discordjs/ts-docgen": "^0.4.1",
|
||||||
"@babel/core": "^7.16.0",
|
"@favware/rollup-type-bundler": "^1.0.10",
|
||||||
"@babel/preset-env": "^7.16.4",
|
"@types/node": "^18.6.3",
|
||||||
"@babel/preset-typescript": "^7.16.0",
|
"@types/ws": "^8.5.3",
|
||||||
"@devsnowflake/docgen": "devsnowflake/docgen#ts-patch",
|
"@typescript-eslint/eslint-plugin": "^5.32.0",
|
||||||
"@discord-player/extractor": "^3.0.2",
|
"@typescript-eslint/parser": "^5.32.0",
|
||||||
"@discordjs/opus": "github:discordjs/opus",
|
"discord-api-types": "^0.37.0",
|
||||||
"@favware/rollup-type-bundler": "^1.0.6",
|
"discord.js": "^14.1.2",
|
||||||
"@types/node": "^16.11.10",
|
"eslint": "^8.21.0",
|
||||||
"@types/ws": "^8.2.0",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^5.4.0",
|
|
||||||
"@typescript-eslint/parser": "^5.4.0",
|
|
||||||
"discord-api-types": "^0.24.0",
|
|
||||||
"discord.js": "^13.6.0",
|
|
||||||
"eslint": "^8.3.0",
|
|
||||||
"gen-esm-wrapper": "^1.1.3",
|
"gen-esm-wrapper": "^1.1.3",
|
||||||
"husky": "^7.0.4",
|
"husky": "^8.0.1",
|
||||||
"jsdoc-babel": "^0.5.0",
|
"opusscript": "^0.0.8",
|
||||||
"prettier": "^2.5.0",
|
"prettier": "^2.7.1",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"ts-node": "^10.4.0",
|
"ts-node": "^10.9.1",
|
||||||
"typescript": "^4.5.2"
|
"typedoc": "^0.23.10",
|
||||||
|
"typescript": "^4.7.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
8
scripts/docgen.js
Normal file
8
scripts/docgen.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
/* eslint-disable */
|
||||||
|
const { runGenerator } = require("@discordjs/ts-docgen");
|
||||||
|
|
||||||
|
runGenerator({
|
||||||
|
existingOutput: "docs/typedoc.json",
|
||||||
|
custom: "docs/config.yml",
|
||||||
|
output: "docs/docs.json"
|
||||||
|
});
|
117
src/Player.ts
117
src/Player.ts
|
@ -1,8 +1,8 @@
|
||||||
import { Client, Collection, GuildResolvable, Snowflake, User, VoiceState, Intents } from "discord.js";
|
import { Client, Collection, GuildResolvable, Snowflake, User, VoiceState, IntentsBitField } from "discord.js";
|
||||||
import { TypedEmitter as EventEmitter } from "tiny-typed-emitter";
|
import { TypedEmitter as EventEmitter } from "tiny-typed-emitter";
|
||||||
import { Queue } from "./Structures/Queue";
|
import { Queue } from "./Structures/Queue";
|
||||||
import { VoiceUtils } from "./VoiceInterface/VoiceUtils";
|
import { VoiceUtils } from "./VoiceInterface/VoiceUtils";
|
||||||
import { PlayerEvents, PlayerOptions, QueryType, SearchOptions, PlayerInitOptions, PlayerSearchResult } from "./types/types";
|
import { PlayerEvents, PlayerOptions, QueryType, SearchOptions, PlayerInitOptions, PlayerSearchResult, PlaylistInitData } from "./types/types";
|
||||||
import Track from "./Structures/Track";
|
import Track from "./Structures/Track";
|
||||||
import { QueryResolver } from "./utils/QueryResolver";
|
import { QueryResolver } from "./utils/QueryResolver";
|
||||||
import YouTube from "youtube-sr";
|
import YouTube from "youtube-sr";
|
||||||
|
@ -34,7 +34,7 @@ class Player extends EventEmitter<PlayerEvents> {
|
||||||
/**
|
/**
|
||||||
* Creates new Discord Player
|
* Creates new Discord Player
|
||||||
* @param {Client} client The Discord Client
|
* @param {Client} client The Discord Client
|
||||||
* @param {PlayerInitOptions} [options={}] The player init options
|
* @param {PlayerInitOptions} [options] The player init options
|
||||||
*/
|
*/
|
||||||
constructor(client: Client, options: PlayerInitOptions = {}) {
|
constructor(client: Client, options: PlayerInitOptions = {}) {
|
||||||
super();
|
super();
|
||||||
|
@ -45,8 +45,8 @@ class Player extends EventEmitter<PlayerEvents> {
|
||||||
*/
|
*/
|
||||||
this.client = client;
|
this.client = client;
|
||||||
|
|
||||||
if (this.client?.options?.intents && !new Intents(this.client?.options?.intents).has(Intents.FLAGS.GUILD_VOICE_STATES)) {
|
if (this.client?.options?.intents && !new IntentsBitField(this.client?.options?.intents).has(IntentsBitField.Flags.GuildVoiceStates)) {
|
||||||
throw new PlayerError('client is missing "GUILD_VOICE_STATES" intent');
|
throw new PlayerError('client is missing "GuildVoiceStates" intent');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -75,70 +75,81 @@ class Player extends EventEmitter<PlayerEvents> {
|
||||||
*/
|
*/
|
||||||
private _handleVoiceState(oldState: VoiceState, newState: VoiceState): void {
|
private _handleVoiceState(oldState: VoiceState, newState: VoiceState): void {
|
||||||
const queue = this.getQueue(oldState.guild.id);
|
const queue = this.getQueue(oldState.guild.id);
|
||||||
if (!queue) return;
|
if (!queue || !queue.connection) return;
|
||||||
|
|
||||||
if (oldState.channelId && newState.channelId && oldState.channelId !== newState.channelId) {
|
if (oldState.channelId && !newState.channelId && newState.member.id === newState.guild.members.me.id) {
|
||||||
if (queue?.connection && newState.member.id === newState.guild.me.id) queue.connection.channel = newState.channel;
|
try {
|
||||||
if (newState.member.id === newState.guild.me.id || (newState.member.id !== newState.guild.me.id && oldState.channelId === queue.connection.channel.id)) {
|
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;
|
if (!Util.isVoiceEmpty(queue.connection.channel)) return;
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
if (!Util.isVoiceEmpty(queue.connection.channel)) return;
|
if (!Util.isVoiceEmpty(queue.connection.channel)) return;
|
||||||
if (!this.queues.has(queue.guild.id)) return;
|
if (!this.queues.has(queue.guild.id)) return;
|
||||||
if (queue.options.leaveOnEmpty) queue.destroy();
|
if (queue.options.leaveOnEmpty) queue.destroy(true);
|
||||||
this.emit("channelEmpty", queue);
|
this.emit("channelEmpty", queue);
|
||||||
}, queue.options.leaveOnEmptyCooldown || 0).unref();
|
}, queue.options.leaveOnEmptyCooldown || 0).unref();
|
||||||
queue._cooldownsTimeout.set(`empty_${oldState.guild.id}`, timeout);
|
queue._cooldownsTimeout.set(`empty_${oldState.guild.id}`, timeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!oldState.channelId && newState.channelId && newState.member.id === newState.guild.me.id) {
|
if (queue.connection && newState.channelId && newState.channelId === queue.connection.channel.id) {
|
||||||
if (newState.serverMute || !newState.serverMute) {
|
|
||||||
queue.setPaused(newState.serverMute);
|
|
||||||
} else if (newState.suppress || !newState.suppress) {
|
|
||||||
if (newState.suppress) newState.guild.me.voice.setRequestToSpeak(true).catch(Util.noop);
|
|
||||||
queue.setPaused(newState.suppress);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (oldState.channelId === newState.channelId && oldState.member.id === newState.guild.me.id) {
|
|
||||||
if (oldState.serverMute !== newState.serverMute) {
|
|
||||||
queue.setPaused(newState.serverMute);
|
|
||||||
} else if (oldState.suppress !== newState.suppress) {
|
|
||||||
if (newState.suppress) newState.guild.me.voice.setRequestToSpeak(true).catch(Util.noop);
|
|
||||||
queue.setPaused(newState.suppress);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (oldState.member.id === this.client.user.id && !newState.channelId) {
|
|
||||||
queue.destroy();
|
|
||||||
return void this.emit("botDisconnect", queue);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!queue.connection || !queue.connection.channel) return;
|
|
||||||
|
|
||||||
if (!oldState.channelId || newState.channelId) {
|
|
||||||
const emptyTimeout = queue._cooldownsTimeout.get(`empty_${oldState.guild.id}`);
|
const emptyTimeout = queue._cooldownsTimeout.get(`empty_${oldState.guild.id}`);
|
||||||
const channelEmpty = Util.isVoiceEmpty(queue.connection.channel);
|
const channelEmpty = Util.isVoiceEmpty(queue.connection.channel);
|
||||||
|
|
||||||
if (newState.channelId === queue.connection.channel.id) {
|
|
||||||
if (!channelEmpty && emptyTimeout) {
|
if (!channelEmpty && emptyTimeout) {
|
||||||
clearTimeout(emptyTimeout);
|
clearTimeout(emptyTimeout);
|
||||||
queue._cooldownsTimeout.delete(`empty_${oldState.guild.id}`);
|
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 {
|
} else {
|
||||||
if (oldState.channelId === queue.connection.channel.id) {
|
|
||||||
if (!Util.isVoiceEmpty(queue.connection.channel)) return;
|
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
if (!Util.isVoiceEmpty(queue.connection.channel)) return;
|
if (queue.connection && !Util.isVoiceEmpty(queue.connection.channel)) return;
|
||||||
if (!this.queues.has(queue.guild.id)) return;
|
if (!this.queues.has(queue.guild.id)) return;
|
||||||
if (queue.options.leaveOnEmpty) queue.destroy();
|
if (queue.options.leaveOnEmpty) queue.destroy(true);
|
||||||
this.emit("channelEmpty", queue);
|
this.emit("channelEmpty", queue);
|
||||||
}, queue.options.leaveOnEmptyCooldown || 0).unref();
|
}, queue.options.leaveOnEmptyCooldown || 0).unref();
|
||||||
queue._cooldownsTimeout.set(`empty_${oldState.guild.id}`, timeout);
|
queue._cooldownsTimeout.set(`empty_${oldState.guild.id}`, timeout);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a queue for a guild if not available, else returns existing queue
|
* Creates a queue for a guild if not available, else returns existing queue
|
||||||
|
@ -338,7 +349,9 @@ class Player extends EventEmitter<PlayerEvents> {
|
||||||
return { playlist: null, tracks: res };
|
return { playlist: null, tracks: res };
|
||||||
}
|
}
|
||||||
case QueryType.SPOTIFY_SONG: {
|
case QueryType.SPOTIFY_SONG: {
|
||||||
const spotifyData = await Spotify.getData(query).catch(Util.noop);
|
const spotifyData = await Spotify(await Util.getFetch())
|
||||||
|
.getData(query)
|
||||||
|
.catch(Util.noop);
|
||||||
if (!spotifyData) return { playlist: null, tracks: [] };
|
if (!spotifyData) return { playlist: null, tracks: [] };
|
||||||
const spotifyTrack = new Track(this, {
|
const spotifyTrack = new Track(this, {
|
||||||
title: spotifyData.name,
|
title: spotifyData.name,
|
||||||
|
@ -359,7 +372,9 @@ class Player extends EventEmitter<PlayerEvents> {
|
||||||
}
|
}
|
||||||
case QueryType.SPOTIFY_PLAYLIST:
|
case QueryType.SPOTIFY_PLAYLIST:
|
||||||
case QueryType.SPOTIFY_ALBUM: {
|
case QueryType.SPOTIFY_ALBUM: {
|
||||||
const spotifyPlaylist = await Spotify.getData(query).catch(Util.noop);
|
const spotifyPlaylist = await Spotify(await Util.getFetch())
|
||||||
|
.getData(query)
|
||||||
|
.catch(Util.noop);
|
||||||
if (!spotifyPlaylist) return { playlist: null, tracks: [] };
|
if (!spotifyPlaylist) return { playlist: null, tracks: [] };
|
||||||
|
|
||||||
const playlist = new Playlist(this, {
|
const playlist = new Playlist(this, {
|
||||||
|
@ -408,9 +423,9 @@ class Player extends EventEmitter<PlayerEvents> {
|
||||||
const data = new Track(this, {
|
const data = new Track(this, {
|
||||||
title: m.track.name ?? "",
|
title: m.track.name ?? "",
|
||||||
description: m.track.description ?? "",
|
description: m.track.description ?? "",
|
||||||
author: m.track.artists[0]?.name ?? "Unknown Artist",
|
author: m.track.artists?.[0]?.name ?? "Unknown Artist",
|
||||||
url: m.track.external_urls?.spotify ?? query,
|
url: m.track.external_urls?.spotify ?? query,
|
||||||
thumbnail: m.track.album?.images[0]?.url ?? "https://www.scdn.co/i/_global/twitter_card-default.jpg",
|
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)),
|
duration: Util.buildTimeCode(Util.parseMS(m.track.duration_ms)),
|
||||||
views: 0,
|
views: 0,
|
||||||
requestedBy: options.requestedBy as User,
|
requestedBy: options.requestedBy as User,
|
||||||
|
@ -585,6 +600,14 @@ class Player extends EventEmitter<PlayerEvents> {
|
||||||
*[Symbol.iterator]() {
|
*[Symbol.iterator]() {
|
||||||
yield* Array.from(this.queues.values());
|
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 };
|
export { Player };
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { Collection, Guild, StageChannel, VoiceChannel, Snowflake, SnowflakeUtil, GuildChannelResolvable } from "discord.js";
|
import { Collection, Guild, StageChannel, VoiceChannel, SnowflakeUtil, GuildChannelResolvable, ChannelType } from "discord.js";
|
||||||
import { Player } from "../Player";
|
import { Player } from "../Player";
|
||||||
import { StreamDispatcher } from "../VoiceInterface/StreamDispatcher";
|
import { StreamDispatcher } from "../VoiceInterface/StreamDispatcher";
|
||||||
import Track from "./Track";
|
import Track from "./Track";
|
||||||
import { PlayerOptions, PlayerProgressbarOptions, PlayOptions, QueueFilters, QueueRepeatMode, TrackSource } from "../types/types";
|
import { PlayerOptions, PlayerProgressbarOptions, PlayOptions, QueueFilters, QueueRepeatMode, TrackSource } from "../types/types";
|
||||||
import ytdl from "discord-ytdl-core";
|
import ytdl from "ytdl-core";
|
||||||
import { AudioResource, StreamType } from "@discordjs/voice";
|
import { AudioResource, StreamType } from "@discordjs/voice";
|
||||||
import { Util } from "../utils/Util";
|
import { Util } from "../utils/Util";
|
||||||
import YouTube from "youtube-sr";
|
import YouTube from "youtube-sr";
|
||||||
|
@ -11,6 +11,7 @@ import AudioFilters from "../utils/AudioFilters";
|
||||||
import { PlayerError, ErrorStatusCode } from "./PlayerError";
|
import { PlayerError, ErrorStatusCode } from "./PlayerError";
|
||||||
import type { Readable } from "stream";
|
import type { Readable } from "stream";
|
||||||
import { VolumeTransformer } from "../VoiceInterface/VolumeTransformer";
|
import { VolumeTransformer } from "../VoiceInterface/VolumeTransformer";
|
||||||
|
import { createFFmpegStream } from "../utils/FFmpegStream";
|
||||||
|
|
||||||
class Queue<T = unknown> {
|
class Queue<T = unknown> {
|
||||||
public readonly guild: Guild;
|
public readonly guild: Guild;
|
||||||
|
@ -22,7 +23,7 @@ class Queue<T = unknown> {
|
||||||
public playing = false;
|
public playing = false;
|
||||||
public metadata?: T = null;
|
public metadata?: T = null;
|
||||||
public repeatMode: QueueRepeatMode = 0;
|
public repeatMode: QueueRepeatMode = 0;
|
||||||
public readonly id: Snowflake = SnowflakeUtil.generate();
|
public readonly id = SnowflakeUtil.generate().toString();
|
||||||
private _streamTime = 0;
|
private _streamTime = 0;
|
||||||
public _cooldownsTimeout = new Collection<string, NodeJS.Timeout>();
|
public _cooldownsTimeout = new Collection<string, NodeJS.Timeout>();
|
||||||
private _activeFilters: any[] = []; // eslint-disable-line @typescript-eslint/no-explicit-any
|
private _activeFilters: any[] = []; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
|
@ -35,7 +36,7 @@ class Queue<T = unknown> {
|
||||||
* Queue constructor
|
* Queue constructor
|
||||||
* @param {Player} player The player that instantiated this queue
|
* @param {Player} player The player that instantiated this queue
|
||||||
* @param {Guild} guild The guild that instantiated this queue
|
* @param {Guild} guild The guild that instantiated this queue
|
||||||
* @param {PlayerOptions} [options={}] Player options for the queue
|
* @param {PlayerOptions} [options] Player options for the queue
|
||||||
*/
|
*/
|
||||||
constructor(player: Player, guild: Guild, options: PlayerOptions = {}) {
|
constructor(player: Player, guild: Guild, options: PlayerOptions = {}) {
|
||||||
/**
|
/**
|
||||||
|
@ -152,16 +153,16 @@ class Queue<T = unknown> {
|
||||||
async connect(channel: GuildChannelResolvable) {
|
async connect(channel: GuildChannelResolvable) {
|
||||||
if (this.#watchDestroyed()) return;
|
if (this.#watchDestroyed()) return;
|
||||||
const _channel = this.guild.channels.resolve(channel) as StageChannel | VoiceChannel;
|
const _channel = this.guild.channels.resolve(channel) as StageChannel | VoiceChannel;
|
||||||
if (!["GUILD_STAGE_VOICE", "GUILD_VOICE"].includes(_channel?.type))
|
if (![ChannelType.GuildStageVoice, ChannelType.GuildVoice].includes(_channel?.type))
|
||||||
throw new PlayerError(`Channel type must be GUILD_VOICE or GUILD_STAGE_VOICE, got ${_channel?.type}!`, ErrorStatusCode.INVALID_ARG_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, {
|
const connection = await this.player.voiceUtils.connect(_channel, {
|
||||||
deaf: this.options.autoSelfDeaf
|
deaf: this.options.autoSelfDeaf
|
||||||
});
|
});
|
||||||
this.connection = connection;
|
this.connection = connection;
|
||||||
|
|
||||||
if (_channel.type === "GUILD_STAGE_VOICE") {
|
if (_channel.type === ChannelType.GuildStageVoice) {
|
||||||
await _channel.guild.me.voice.setSuppressed(false).catch(async () => {
|
await _channel.guild.members.me.voice.setSuppressed(false).catch(async () => {
|
||||||
return await _channel.guild.me.voice.setRequestToSpeak(true).catch(Util.noop);
|
return await _channel.guild.members.me.voice.setRequestToSpeak(true).catch(Util.noop);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -179,7 +180,7 @@ class Queue<T = unknown> {
|
||||||
this.connection.on("start", (resource) => {
|
this.connection.on("start", (resource) => {
|
||||||
if (this.#watchDestroyed(false)) return;
|
if (this.#watchDestroyed(false)) return;
|
||||||
this.playing = true;
|
this.playing = true;
|
||||||
if (!this._filtersUpdate && resource?.metadata) this.player.emit("trackStart", this, resource?.metadata ?? this.current);
|
if (!this._filtersUpdate) this.player.emit("trackStart", this, resource?.metadata ?? this.current);
|
||||||
this._filtersUpdate = false;
|
this._filtersUpdate = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -188,7 +189,7 @@ class Queue<T = unknown> {
|
||||||
this.playing = false;
|
this.playing = false;
|
||||||
if (this._filtersUpdate) return;
|
if (this._filtersUpdate) return;
|
||||||
this._streamTime = 0;
|
this._streamTime = 0;
|
||||||
if (resource && resource.metadata) this.previousTracks.push(resource.metadata);
|
if (resource?.metadata) this.previousTracks.push(resource.metadata);
|
||||||
|
|
||||||
this.player.emit("trackEnd", this, resource.metadata);
|
this.player.emit("trackEnd", this, resource.metadata);
|
||||||
|
|
||||||
|
@ -206,10 +207,6 @@ class Queue<T = unknown> {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.player.voiceUtils.enterReady(this.connection.voiceConnection, {
|
|
||||||
maxTime: this.player.options.connectionTimeout || 30_000
|
|
||||||
});
|
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -473,10 +470,10 @@ class Queue<T = unknown> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes a track from the queue
|
* Removes a track from the queue
|
||||||
* @param {Track|Snowflake|number} track The track to remove
|
* @param {Track|string|number} track The track to remove
|
||||||
* @returns {Track}
|
* @returns {Track}
|
||||||
*/
|
*/
|
||||||
remove(track: Track | Snowflake | number) {
|
remove(track: Track | string | number) {
|
||||||
if (this.#watchDestroyed()) return;
|
if (this.#watchDestroyed()) return;
|
||||||
let trackFound: Track = null;
|
let trackFound: Track = null;
|
||||||
if (typeof track === "number") {
|
if (typeof track === "number") {
|
||||||
|
@ -496,10 +493,10 @@ class Queue<T = unknown> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the index of the specified track. If found, returns the track index else returns -1.
|
* Returns the index of the specified track. If found, returns the track index else returns -1.
|
||||||
* @param {number|Track|Snowflake} track The track
|
* @param {number|Track|string} track The track
|
||||||
* @returns {number}
|
* @returns {number}
|
||||||
*/
|
*/
|
||||||
getTrackPosition(track: number | Track | Snowflake) {
|
getTrackPosition(track: number | Track | string) {
|
||||||
if (this.#watchDestroyed()) return;
|
if (this.#watchDestroyed()) return;
|
||||||
if (typeof track === "number") return this.tracks[track] != null ? track : -1;
|
if (typeof track === "number") return this.tracks[track] != null ? track : -1;
|
||||||
return this.tracks.findIndex((pred) => pred.id === (track instanceof Track ? track.id : track));
|
return this.tracks.findIndex((pred) => pred.id === (track instanceof Track ? track.id : track));
|
||||||
|
@ -621,7 +618,7 @@ class Queue<T = unknown> {
|
||||||
/**
|
/**
|
||||||
* Play stream in a voice/stage channel
|
* Play stream in a voice/stage channel
|
||||||
* @param {Track} [src] The track to play (if empty, uses first track from the queue)
|
* @param {Track} [src] The track to play (if empty, uses first track from the queue)
|
||||||
* @param {PlayOptions} [options={}] The options
|
* @param {PlayOptions} [options] The options
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async play(src?: Track, options: PlayOptions = {}): Promise<void> {
|
async play(src?: Track, options: PlayOptions = {}): Promise<void> {
|
||||||
|
@ -639,66 +636,46 @@ class Queue<T = unknown> {
|
||||||
}
|
}
|
||||||
|
|
||||||
let stream = null;
|
let stream = null;
|
||||||
const customDownloader = typeof this.onBeforeCreateStream === "function";
|
const hasCustomDownloader = typeof this.onBeforeCreateStream === "function";
|
||||||
|
|
||||||
if (["youtube", "spotify"].includes(track.raw.source)) {
|
if (["youtube", "spotify"].includes(track.raw.source)) {
|
||||||
let spotifyResolved = false;
|
let spotifyResolved = false;
|
||||||
if (this.options.spotifyBridge && track.raw.source === "spotify" && !track.raw.engine) {
|
if (this.options.spotifyBridge && track.raw.source === "spotify" && !track.raw.engine) {
|
||||||
track.raw.engine = await YouTube.search(`${track.author} ${track.title}`, { type: "video" })
|
track.raw.engine = await YouTube.search(`${track.author} ${track.title}`, { type: "video" })
|
||||||
.then((x) => x[0].url)
|
.then((res) => res[0].url)
|
||||||
.catch(() => null);
|
.catch(() => null);
|
||||||
spotifyResolved = true;
|
spotifyResolved = true;
|
||||||
}
|
}
|
||||||
const link = track.raw.source === "spotify" ? track.raw.engine : track.url;
|
|
||||||
if (!link) return void this.play(this.tracks.shift(), { immediate: true });
|
|
||||||
|
|
||||||
if (customDownloader) {
|
const url = track.raw.source === "spotify" ? track.raw.engine : track.url;
|
||||||
stream = (await this.onBeforeCreateStream(track, spotifyResolved ? "youtube" : track.raw.source, this)) ?? null;
|
if (!url) return void this.play(this.tracks.shift(), { immediate: true });
|
||||||
if (stream)
|
|
||||||
stream = ytdl
|
if (hasCustomDownloader) {
|
||||||
.arbitraryStream(stream, {
|
stream = (await this.onBeforeCreateStream(track, spotifyResolved ? "youtube" : track.raw.source, this)) || null;
|
||||||
opusEncoded: false,
|
}
|
||||||
fmt: "s16le",
|
|
||||||
encoderArgs: options.encoderArgs ?? this._activeFilters.length ? ["-af", AudioFilters.create(this._activeFilters)] : [],
|
if (!stream) {
|
||||||
seek: options.seek ? options.seek / 1000 : 0
|
stream = ytdl(url, this.options.ytdlOptions);
|
||||||
})
|
|
||||||
.on("error", (err) => {
|
|
||||||
return err.message.toLowerCase().includes("premature close") ? null : this.player.emit("error", this, err);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
stream = ytdl(link, {
|
|
||||||
...this.options.ytdlOptions,
|
|
||||||
// discord-ytdl-core
|
|
||||||
opusEncoded: false,
|
|
||||||
fmt: "s16le",
|
|
||||||
encoderArgs: options.encoderArgs ?? this._activeFilters.length ? ["-af", AudioFilters.create(this._activeFilters)] : [],
|
|
||||||
seek: options.seek ? options.seek / 1000 : 0
|
|
||||||
}).on("error", (err) => {
|
|
||||||
return err.message.toLowerCase().includes("premature close") ? null : this.player.emit("error", this, err);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const tryArb = (customDownloader && (await this.onBeforeCreateStream(track, track.raw.source || track.raw.engine, this))) || null;
|
const arbitraryStream = (hasCustomDownloader && (await this.onBeforeCreateStream(track, track.raw.source || track.raw.engine, this))) || null;
|
||||||
const arbitrarySource = tryArb
|
stream =
|
||||||
? tryArb
|
arbitraryStream || (track.raw.source === "soundcloud" && typeof track.raw.engine?.downloadProgressive === "function")
|
||||||
: track.raw.source === "soundcloud"
|
|
||||||
? await track.raw.engine.downloadProgressive()
|
? await track.raw.engine.downloadProgressive()
|
||||||
: typeof track.raw.engine === "function"
|
: typeof track.raw.engine === "function"
|
||||||
? await track.raw.engine()
|
? await track.raw.engine()
|
||||||
: track.raw.engine;
|
: track.raw.engine;
|
||||||
stream = ytdl
|
|
||||||
.arbitraryStream(arbitrarySource, {
|
|
||||||
opusEncoded: false,
|
|
||||||
fmt: "s16le",
|
|
||||||
encoderArgs: options.encoderArgs ?? this._activeFilters.length ? ["-af", AudioFilters.create(this._activeFilters)] : [],
|
|
||||||
seek: options.seek ? options.seek / 1000 : 0
|
|
||||||
})
|
|
||||||
.on("error", (err) => {
|
|
||||||
return err.message.toLowerCase().includes("premature close") ? null : this.player.emit("error", this, err);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const resource: AudioResource<Track> = this.connection.createStream(stream, {
|
const ffmpegStream = createFFmpegStream(stream, {
|
||||||
|
encoderArgs: options.encoderArgs || this._activeFilters.length ? ["-af", AudioFilters.create(this._activeFilters)] : [],
|
||||||
|
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,
|
type: StreamType.Raw,
|
||||||
data: track,
|
data: track,
|
||||||
disableVolume: Boolean(this.options.disableVolume)
|
disableVolume: Boolean(this.options.disableVolume)
|
||||||
|
@ -708,12 +685,11 @@ class Queue<T = unknown> {
|
||||||
this._filtersUpdate = options.filtersUpdate;
|
this._filtersUpdate = options.filtersUpdate;
|
||||||
|
|
||||||
const volumeTransformer = resource.volume as VolumeTransformer;
|
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 (volumeTransformer?.hasSmoothness && typeof this.options.volumeSmoothness === "number") {
|
||||||
if (typeof volumeTransformer.setSmoothness === "function") volumeTransformer.setSmoothness(this.options.volumeSmoothness || 0);
|
if (typeof volumeTransformer.setSmoothness === "function") volumeTransformer.setSmoothness(this.options.volumeSmoothness || 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setVolume(this.options.initialVolume);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.connection.playStream(resource);
|
this.connection.playStream(resource);
|
||||||
}, this.#getBufferingTimeout()).unref();
|
}, this.#getBufferingTimeout()).unref();
|
||||||
|
@ -731,9 +707,14 @@ class Queue<T = unknown> {
|
||||||
if (this.options.leaveOnEnd) this.destroy();
|
if (this.options.leaveOnEnd) this.destroy();
|
||||||
return void this.player.emit("queueEnd", this);
|
return void this.player.emit("queueEnd", this);
|
||||||
}
|
}
|
||||||
const info = await YouTube.getVideo(track.url)
|
let info = await YouTube.getVideo(track.url)
|
||||||
.then((x) => x.videos[0])
|
.then((x) => x.videos[0])
|
||||||
.catch(Util.noop);
|
.catch(Util.noop);
|
||||||
|
// fallback
|
||||||
|
if (!info)
|
||||||
|
info = await YouTube.search(track.author)
|
||||||
|
.then((x) => x[0])
|
||||||
|
.catch(Util.noop);
|
||||||
if (!info) {
|
if (!info) {
|
||||||
if (this.options.leaveOnEnd) this.destroy();
|
if (this.options.leaveOnEnd) this.destroy();
|
||||||
return void this.player.emit("queueEnd", this);
|
return void this.player.emit("queueEnd", this);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { User, Util, SnowflakeUtil, Snowflake } from "discord.js";
|
import { User, escapeMarkdown, SnowflakeUtil } from "discord.js";
|
||||||
import { Player } from "../Player";
|
import { Player } from "../Player";
|
||||||
import { RawTrackData, TrackJSON } from "../types/types";
|
import { RawTrackData, TrackJSON } from "../types/types";
|
||||||
import { Playlist } from "./Playlist";
|
import { Playlist } from "./Playlist";
|
||||||
|
@ -16,7 +16,7 @@ class Track {
|
||||||
public requestedBy!: User;
|
public requestedBy!: User;
|
||||||
public playlist?: Playlist;
|
public playlist?: Playlist;
|
||||||
public readonly raw: RawTrackData = {} as RawTrackData;
|
public readonly raw: RawTrackData = {} as RawTrackData;
|
||||||
public readonly id: Snowflake = SnowflakeUtil.generate();
|
public readonly id = SnowflakeUtil.generate().toString();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Track constructor
|
* Track constructor
|
||||||
|
@ -109,7 +109,7 @@ class Track {
|
||||||
}
|
}
|
||||||
|
|
||||||
private _patch(data: RawTrackData) {
|
private _patch(data: RawTrackData) {
|
||||||
this.title = Util.escapeMarkdown(data.title ?? "");
|
this.title = escapeMarkdown(data.title ?? "");
|
||||||
this.author = data.author ?? "";
|
this.author = data.author ?? "";
|
||||||
this.url = data.url ?? "";
|
this.url = data.url ?? "";
|
||||||
this.thumbnail = data.thumbnail ?? "";
|
this.thumbnail = data.thumbnail ?? "";
|
||||||
|
|
|
@ -129,7 +129,7 @@ class StreamDispatcher extends EventEmitter<VoiceEvents> {
|
||||||
/**
|
/**
|
||||||
* Creates stream
|
* Creates stream
|
||||||
* @param {Readable|Duplex|string} src The stream source
|
* @param {Readable|Duplex|string} src The stream source
|
||||||
* @param {object} [ops={}] Options
|
* @param {object} [ops] Options
|
||||||
* @returns {AudioResource}
|
* @returns {AudioResource}
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { VoiceChannel, StageChannel, Collection, Snowflake } from "discord.js";
|
import { VoiceChannel, StageChannel, Collection, Snowflake } from "discord.js";
|
||||||
import { DiscordGatewayAdapterCreator, entersState, joinVoiceChannel, VoiceConnection, VoiceConnectionStatus } from "@discordjs/voice";
|
import { DiscordGatewayAdapterCreator, joinVoiceChannel, VoiceConnection } from "@discordjs/voice";
|
||||||
import { StreamDispatcher } from "./StreamDispatcher";
|
import { StreamDispatcher } from "./StreamDispatcher";
|
||||||
|
|
||||||
class VoiceUtils {
|
class VoiceUtils {
|
||||||
|
@ -20,7 +20,7 @@ class VoiceUtils {
|
||||||
/**
|
/**
|
||||||
* Joins a voice channel, creating basic stream dispatch manager
|
* Joins a voice channel, creating basic stream dispatch manager
|
||||||
* @param {StageChannel|VoiceChannel} channel The voice channel
|
* @param {StageChannel|VoiceChannel} channel The voice channel
|
||||||
* @param {object} [options={}] Join options
|
* @param {object} [options] Join options
|
||||||
* @returns {Promise<StreamDispatcher>}
|
* @returns {Promise<StreamDispatcher>}
|
||||||
*/
|
*/
|
||||||
public async connect(
|
public async connect(
|
||||||
|
@ -39,7 +39,7 @@ class VoiceUtils {
|
||||||
/**
|
/**
|
||||||
* Joins a voice channel
|
* Joins a voice channel
|
||||||
* @param {StageChannel|VoiceChannel} [channel] The voice/stage channel to join
|
* @param {StageChannel|VoiceChannel} [channel] The voice/stage channel to join
|
||||||
* @param {object} [options={}] Join options
|
* @param {object} [options] Join options
|
||||||
* @returns {VoiceConnection}
|
* @returns {VoiceConnection}
|
||||||
*/
|
*/
|
||||||
public async join(
|
public async join(
|
||||||
|
@ -59,16 +59,6 @@ class VoiceUtils {
|
||||||
return conn;
|
return conn;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async enterReady(conn: VoiceConnection, options: { maxTime?: number } = {}) {
|
|
||||||
try {
|
|
||||||
conn = await entersState(conn, VoiceConnectionStatus.Ready, options?.maxTime ?? 20000);
|
|
||||||
return conn;
|
|
||||||
} catch (err) {
|
|
||||||
conn.destroy();
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disconnects voice connection
|
* Disconnects voice connection
|
||||||
* @param {VoiceConnection} connection The voice connection
|
* @param {VoiceConnection} connection The voice connection
|
||||||
|
|
|
@ -13,6 +13,7 @@ export { VoiceUtils } from "./VoiceInterface/VoiceUtils";
|
||||||
export { VoiceEvents, StreamDispatcher } from "./VoiceInterface/StreamDispatcher";
|
export { VoiceEvents, StreamDispatcher } from "./VoiceInterface/StreamDispatcher";
|
||||||
export { Util } from "./utils/Util";
|
export { Util } from "./utils/Util";
|
||||||
export * from "./types/types";
|
export * from "./types/types";
|
||||||
|
export * from "./utils/FFmpegStream";
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
export const version: string = require(`${__dirname}/../package.json`).version;
|
export const version: string = require(`${__dirname}/../package.json`).version;
|
||||||
|
|
|
@ -121,6 +121,7 @@ export interface PlayerProgressbarOptions {
|
||||||
length?: number;
|
length?: number;
|
||||||
line?: string;
|
line?: string;
|
||||||
indicator?: string;
|
indicator?: string;
|
||||||
|
queue?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -130,12 +131,12 @@ export interface PlayerProgressbarOptions {
|
||||||
* @property {boolean} [leaveOnEmpty=true] If it should leave on empty
|
* @property {boolean} [leaveOnEmpty=true] If it should leave on empty
|
||||||
* @property {number} [leaveOnEmptyCooldown=1000] The cooldown in ms
|
* @property {number} [leaveOnEmptyCooldown=1000] The cooldown in ms
|
||||||
* @property {boolean} [autoSelfDeaf=true] If it should set the bot in deaf mode
|
* @property {boolean} [autoSelfDeaf=true] If it should set the bot in deaf mode
|
||||||
* @property {YTDLDownloadOptions} [ytdlOptions={}] The youtube download options
|
* @property {YTDLDownloadOptions} [ytdlOptions] The youtube download options
|
||||||
* @property {number} [initialVolume=100] The initial player volume
|
* @property {number} [initialVolume=100] The initial player volume
|
||||||
* @property {number} [bufferingTimeout=3000] Buffering timeout for the stream
|
* @property {number} [bufferingTimeout=3000] Buffering timeout for the stream
|
||||||
* @property {boolean} [spotifyBridge=true] If player should bridge spotify source to youtube
|
* @property {boolean} [spotifyBridge=true] If player should bridge spotify source to youtube
|
||||||
* @property {boolean} [disableVolume=false] If player should disable inline volume
|
* @property {boolean} [disableVolume=false] If player should disable inline volume
|
||||||
* @property {boolean} [volumeSmoothness=0] The volume transition smoothness between volume changes (lower the value to get better result)
|
* @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`
|
* Setting this or leaving this empty will disable this effect. Example: `volumeSmoothness: 0.1`
|
||||||
* @property {Function} [onBeforeCreateStream] Runs before creating stream
|
* @property {Function} [onBeforeCreateStream] Runs before creating stream
|
||||||
*/
|
*/
|
||||||
|
@ -476,7 +477,7 @@ export interface PlaylistJSON {
|
||||||
/**
|
/**
|
||||||
* @typedef {object} PlayerInitOptions
|
* @typedef {object} PlayerInitOptions
|
||||||
* @property {boolean} [autoRegisterExtractor=true] If it should automatically register `@discord-player/extractor`
|
* @property {boolean} [autoRegisterExtractor=true] If it should automatically register `@discord-player/extractor`
|
||||||
* @property {YTDLDownloadOptions} [ytdlOptions={}] The options passed to `ytdl-core`
|
* @property {YTDLDownloadOptions} [ytdlOptions] The options passed to `ytdl-core`
|
||||||
* @property {number} [connectionTimeout=20000] The voice connection timeout
|
* @property {number} [connectionTimeout=20000] The voice connection timeout
|
||||||
*/
|
*/
|
||||||
export interface PlayerInitOptions {
|
export interface PlayerInitOptions {
|
||||||
|
|
|
@ -2,45 +2,13 @@ import { FiltersName } from "../types/types";
|
||||||
|
|
||||||
const bass = (g: number) => `bass=g=${g}:f=110:w=0.3`;
|
const bass = (g: number) => `bass=g=${g}:f=110:w=0.3`;
|
||||||
|
|
||||||
/**
|
class AudioFilters {
|
||||||
* The available audio filters
|
public constructor() {
|
||||||
* @typedef {object} AudioFilters
|
return AudioFilters;
|
||||||
* @property {string} bassboost_low The bassboost filter (+15dB)
|
}
|
||||||
* @property {string} bassboost The bassboost filter (+20dB)
|
|
||||||
* @property {string} bassboost_high The bassboost filter (+30dB)
|
|
||||||
* @property {string} 8D The 8D filter
|
|
||||||
* @property {string} vaporwave The vaporwave filter
|
|
||||||
* @property {string} nightcore The nightcore filter
|
|
||||||
* @property {string} phaser The phaser filter
|
|
||||||
* @property {string} tremolo The tremolo filter
|
|
||||||
* @property {string} vibrato The vibrato filter
|
|
||||||
* @property {string} reverse The reverse filter
|
|
||||||
* @property {string} treble The treble filter
|
|
||||||
* @property {string} normalizer The normalizer filter (dynamic audio normalizer based)
|
|
||||||
* @property {string} normalizer2 The normalizer filter (audio compressor based)
|
|
||||||
* @property {string} surrounding The surrounding filter
|
|
||||||
* @property {string} pulsator The pulsator filter
|
|
||||||
* @property {string} subboost The subboost filter
|
|
||||||
* @property {string} karaoke The kakaoke filter
|
|
||||||
* @property {string} flanger The flanger filter
|
|
||||||
* @property {string} gate The gate filter
|
|
||||||
* @property {string} haas The haas filter
|
|
||||||
* @property {string} mcompand The mcompand filter
|
|
||||||
* @property {string} mono The mono filter
|
|
||||||
* @property {string} mstlr The mstlr filter
|
|
||||||
* @property {string} mstrr The mstrr filter
|
|
||||||
* @property {string} compressor The compressor filter
|
|
||||||
* @property {string} expander The expander filter
|
|
||||||
* @property {string} softlimiter The softlimiter filter
|
|
||||||
* @property {string} chorus The chorus filter
|
|
||||||
* @property {string} chorus2d The chorus2d filter
|
|
||||||
* @property {string} chorus3d The chorus3d filter
|
|
||||||
* @property {string} fadein The fadein filter
|
|
||||||
* @property {string} dim The dim filter
|
|
||||||
* @property {string} earrape The earrape filter
|
|
||||||
*/
|
|
||||||
|
|
||||||
const FilterList = {
|
public static get filters(): Record<FiltersName, string> {
|
||||||
|
return {
|
||||||
bassboost_low: bass(15),
|
bassboost_low: bass(15),
|
||||||
bassboost: bass(20),
|
bassboost: bass(20),
|
||||||
bassboost_high: bass(30),
|
bassboost_high: bass(30),
|
||||||
|
@ -52,8 +20,8 @@ const FilterList = {
|
||||||
vibrato: "vibrato=f=6.5",
|
vibrato: "vibrato=f=6.5",
|
||||||
reverse: "areverse",
|
reverse: "areverse",
|
||||||
treble: "treble=g=5",
|
treble: "treble=g=5",
|
||||||
normalizer: "dynaudnorm=g=101",
|
normalizer2: "dynaudnorm=g=101",
|
||||||
normalizer2: "acompressor",
|
normalizer: "acompressor",
|
||||||
surrounding: "surround",
|
surrounding: "surround",
|
||||||
pulsator: "apulsator=hz=1",
|
pulsator: "apulsator=hz=1",
|
||||||
subboost: "asubboost",
|
subboost: "asubboost",
|
||||||
|
@ -73,44 +41,67 @@ const FilterList = {
|
||||||
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",
|
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",
|
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))'"`,
|
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",
|
earrape: "channelsplit,sidechaingate=level_in=64"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
*[Symbol.iterator](): IterableIterator<{ name: FiltersName; value: string }> {
|
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)) {
|
for (const [k, v] of Object.entries(this)) {
|
||||||
if (typeof this[k as FiltersName] === "string") yield { name: k as FiltersName, value: v as string };
|
if (typeof this.filters[k as FiltersName] === "string") yield { name: k as FiltersName, value: v as string };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
|
||||||
get names() {
|
public static get names() {
|
||||||
return Object.keys(this).filter((p) => !["names", "length"].includes(p) && typeof this[p as FiltersName] !== "function") as FiltersName[];
|
return Object.keys(this).filter((p) => !["names", "length"].includes(p) && typeof this.filters[p as FiltersName] !== "function") as FiltersName[];
|
||||||
},
|
}
|
||||||
|
|
||||||
get length() {
|
// @ts-expect-error AudioFilters.length
|
||||||
return Object.keys(this).filter((p) => !["names", "length"].includes(p) && typeof this[p as FiltersName] !== "function").length;
|
public static get length() {
|
||||||
},
|
return Object.keys(this).filter((p) => !["names", "length"].includes(p) && typeof this.filters[p as FiltersName] !== "function").length;
|
||||||
|
}
|
||||||
|
|
||||||
toString() {
|
public static toString() {
|
||||||
return this.names.map((m) => (this as any)[m]).join(","); // eslint-disable-line @typescript-eslint/no-explicit-any
|
return this.names.map((m) => (this as any)[m]).join(","); // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
},
|
|
||||||
|
|
||||||
create(filter?: FiltersName[]): string {
|
|
||||||
if (!filter || !Array.isArray(filter)) return this.toString();
|
|
||||||
return filter
|
|
||||||
.filter((predicate) => typeof predicate === "string")
|
|
||||||
.map((m) => this[m])
|
|
||||||
.join(",");
|
|
||||||
},
|
|
||||||
|
|
||||||
define(filterName: string, value: string): void {
|
|
||||||
if (typeof this[filterName as FiltersName] && typeof this[filterName as FiltersName] === "function") return;
|
|
||||||
|
|
||||||
this[filterName as FiltersName] = value;
|
|
||||||
},
|
|
||||||
|
|
||||||
defineBulk(filterArray: { name: string; value: string }[]): void {
|
|
||||||
filterArray.forEach((arr) => this.define(arr.name, arr.value));
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
export default FilterList;
|
/**
|
||||||
export { FilterList as AudioFilters };
|
* Create ffmpeg args from the specified filters name
|
||||||
|
* @param filter The filter name
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
public static create(filters?: FiltersName[]) {
|
||||||
|
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 };
|
||||||
|
|
59
src/utils/FFmpegStream.ts
Normal file
59
src/utils/FFmpegStream.ts
Normal 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;
|
||||||
|
}
|
|
@ -95,6 +95,23 @@ class Util {
|
||||||
}
|
}
|
||||||
|
|
||||||
static noop() {} // eslint-disable-line @typescript-eslint/no-empty-function
|
static noop() {} // eslint-disable-line @typescript-eslint/no-empty-function
|
||||||
|
|
||||||
|
static async getFetch() {
|
||||||
|
if ("fetch" in globalThis) return globalThis.fetch;
|
||||||
|
for (const lib of ["undici", "node-fetch"]) {
|
||||||
|
try {
|
||||||
|
return await import(lib).then((res) => res.fetch || res.default?.fetch || res.default);
|
||||||
|
} catch {
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
const res = require(lib);
|
||||||
|
if (res) return res.fetch || res.default?.fetch || res.default;
|
||||||
|
} catch {
|
||||||
|
// no?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Util };
|
export { Util };
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
{
|
|
||||||
"extends": "./tsconfig.json",
|
|
||||||
"include": [
|
|
||||||
"**/*.ts",
|
|
||||||
"**/*.js"
|
|
||||||
],
|
|
||||||
"exclude": []
|
|
||||||
}
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES6",
|
"target": "ES2020",
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
|
|
Loading…
Reference in a new issue