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/
|
||||
dist/
|
||||
.github/
|
||||
|
|
3
.gitattributes
vendored
3
.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
|
||||
uses: actions/checkout@master
|
||||
|
||||
- name: Install Node v14
|
||||
- name: Install Node v18
|
||||
uses: actions/setup-node@master
|
||||
with:
|
||||
node-version: 14
|
||||
node-version: 18
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm install
|
||||
|
|
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -9,8 +9,4 @@ dist
|
|||
# Yarn logs
|
||||
yarn*.log
|
||||
|
||||
# example
|
||||
example/test
|
||||
example/music-bot/node_modules
|
||||
example/music-bot/package-lock.json
|
||||
example/music-bot/.env
|
||||
dev/
|
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
|
||||
const { REST } = require("@discordjs/rest");
|
||||
const { Routes } = require("discord-api-types/v9");
|
||||
const { Routes, ApplicationCommandOptionType } = require("discord.js");
|
||||
|
||||
const commands = [{
|
||||
const commands = [
|
||||
{
|
||||
name: "play",
|
||||
description: "Plays a song!",
|
||||
options: [
|
||||
{
|
||||
name: "query",
|
||||
type: "STRING",
|
||||
type: ApplicationCommandOptionType.String,
|
||||
description: "The song you want to play",
|
||||
required: true
|
||||
}
|
||||
]
|
||||
}];
|
||||
}
|
||||
];
|
||||
|
||||
const rest = new REST({ version: "9" }).setToken(process.env.DISCORD_TOKEN);
|
||||
const rest = new REST({ version: "10" }).setToken("BOT_TOKEN");
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
console.log("Started refreshing application [/] commands.");
|
||||
|
||||
await rest.put(
|
||||
Routes.applicationGuildCommands(CLIENT_ID, GUILD_ID),
|
||||
{ body: commands },
|
||||
);
|
||||
await rest.put(Routes.applicationGuildCommands(CLIENT_ID, GUILD_ID), { body: commands });
|
||||
|
||||
console.log("Successfully reloaded application [/] commands.");
|
||||
} catch (error) {
|
||||
} catch(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:
|
||||
|
||||
```js
|
||||
const { Client, Intents } = require("discord.js");
|
||||
const client = new Discord.Client({ intents: [Intents.FLAGS.GUILDS, Intents.FLAGS.GUILD_MESSAGES, Intents.FLAGS.GUILD_VOICE_STATES] });
|
||||
const { Client } = require("discord.js");
|
||||
const client = new Discord.Client({
|
||||
intents: [
|
||||
"Guilds",
|
||||
"GuildVoiceStates"
|
||||
]
|
||||
});
|
||||
const { Player } = require("discord-player");
|
||||
|
||||
// Create a new Player (you don't need any API Key)
|
||||
|
@ -98,14 +102,14 @@ client.once("ready", () => {
|
|||
});
|
||||
|
||||
client.on("interactionCreate", async (interaction) => {
|
||||
if (!interaction.isCommand()) return;
|
||||
if (!interaction.isChatInputCommand()) return;
|
||||
|
||||
// /play track:Despacito
|
||||
// will play "Despacito" in the voice channel
|
||||
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.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 });
|
||||
const query = interaction.options.get("query").value;
|
||||
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.getString("query");
|
||||
const queue = player.createQueue(interaction.guild, {
|
||||
metadata: {
|
||||
channel: interaction.channel
|
||||
|
@ -132,7 +136,7 @@ client.on("interactionCreate", async (interaction) => {
|
|||
}
|
||||
});
|
||||
|
||||
client.login(process.env.DISCORD_TOKEN);
|
||||
client.login("BOT_TOKEN");
|
||||
```
|
||||
|
||||
## 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/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`.
|
||||
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
|
||||
|
||||
|
|
|
@ -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",
|
||||
"version": "5.2.2",
|
||||
"version": "5.3.0-dev",
|
||||
"description": "Complete framework to facilitate music commands using discord.js",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
|
@ -23,9 +23,9 @@
|
|||
"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\" \"example/**/*.ts\"",
|
||||
"docs": "docgen --jsdoc jsdoc.json --source src/*.ts src/**/*.ts --custom docs/index.yml --output docs/docs.json",
|
||||
"docs:test": "docgen --jsdoc jsdoc.json --source src/*.ts src/**/*.ts --custom docs/index.yml",
|
||||
"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"
|
||||
|
@ -65,37 +65,32 @@
|
|||
},
|
||||
"homepage": "https://discord-player.js.org",
|
||||
"dependencies": {
|
||||
"@discordjs/voice": "^0.8.0",
|
||||
"discord-ytdl-core": "^5.0.4",
|
||||
"libsodium-wrappers": "^0.7.9",
|
||||
"soundcloud-scraper": "^5.0.2",
|
||||
"spotify-url-info": "^2.2.5",
|
||||
"@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",
|
||||
"youtube-sr": "^4.1.13",
|
||||
"ytdl-core": "^4.10.0"
|
||||
"tslib": "^2.4.0",
|
||||
"youtube-sr": "^4.3.0",
|
||||
"ytdl-core": "^4.11.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.16.0",
|
||||
"@babel/core": "^7.16.0",
|
||||
"@babel/preset-env": "^7.16.4",
|
||||
"@babel/preset-typescript": "^7.16.0",
|
||||
"@devsnowflake/docgen": "devsnowflake/docgen#ts-patch",
|
||||
"@discord-player/extractor": "^3.0.2",
|
||||
"@discordjs/opus": "github:discordjs/opus",
|
||||
"@favware/rollup-type-bundler": "^1.0.6",
|
||||
"@types/node": "^16.11.10",
|
||||
"@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",
|
||||
"@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": "^7.0.4",
|
||||
"jsdoc-babel": "^0.5.0",
|
||||
"prettier": "^2.5.0",
|
||||
"husky": "^8.0.1",
|
||||
"opusscript": "^0.0.8",
|
||||
"prettier": "^2.7.1",
|
||||
"rimraf": "^3.0.2",
|
||||
"ts-node": "^10.4.0",
|
||||
"typescript": "^4.5.2"
|
||||
"ts-node": "^10.9.1",
|
||||
"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"
|
||||
});
|
149
src/Player.ts
149
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 { Queue } from "./Structures/Queue";
|
||||
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 { QueryResolver } from "./utils/QueryResolver";
|
||||
import YouTube from "youtube-sr";
|
||||
|
@ -34,7 +34,7 @@ class Player extends EventEmitter<PlayerEvents> {
|
|||
/**
|
||||
* Creates new Discord Player
|
||||
* @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 = {}) {
|
||||
super();
|
||||
|
@ -45,8 +45,8 @@ class Player extends EventEmitter<PlayerEvents> {
|
|||
*/
|
||||
this.client = client;
|
||||
|
||||
if (this.client?.options?.intents && !new Intents(this.client?.options?.intents).has(Intents.FLAGS.GUILD_VOICE_STATES)) {
|
||||
throw new PlayerError('client is missing "GUILD_VOICE_STATES" intent');
|
||||
if (this.client?.options?.intents && !new IntentsBitField(this.client?.options?.intents).has(IntentsBitField.Flags.GuildVoiceStates)) {
|
||||
throw new PlayerError('client is missing "GuildVoiceStates" intent');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -75,68 +75,79 @@ class Player extends EventEmitter<PlayerEvents> {
|
|||
*/
|
||||
private _handleVoiceState(oldState: VoiceState, newState: VoiceState): void {
|
||||
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 (queue?.connection && newState.member.id === newState.guild.me.id) queue.connection.channel = newState.channel;
|
||||
if (newState.member.id === newState.guild.me.id || (newState.member.id !== newState.guild.me.id && oldState.channelId === queue.connection.channel.id)) {
|
||||
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 (!Util.isVoiceEmpty(queue.connection.channel)) return;
|
||||
if (queue.connection && !Util.isVoiceEmpty(queue.connection.channel)) 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);
|
||||
}, queue.options.leaveOnEmptyCooldown || 0).unref();
|
||||
queue._cooldownsTimeout.set(`empty_${oldState.guild.id}`, timeout);
|
||||
}
|
||||
|
||||
if (!oldState.channelId && newState.channelId && newState.member.id === newState.guild.me.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 channelEmpty = Util.isVoiceEmpty(queue.connection.channel);
|
||||
|
||||
if (newState.channelId === queue.connection.channel.id) {
|
||||
if (!channelEmpty && emptyTimeout) {
|
||||
clearTimeout(emptyTimeout);
|
||||
queue._cooldownsTimeout.delete(`empty_${oldState.guild.id}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (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();
|
||||
this.emit("channelEmpty", queue);
|
||||
}, queue.options.leaveOnEmptyCooldown || 0).unref();
|
||||
queue._cooldownsTimeout.set(`empty_${oldState.guild.id}`, timeout);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -338,7 +349,9 @@ class Player extends EventEmitter<PlayerEvents> {
|
|||
return { playlist: null, tracks: res };
|
||||
}
|
||||
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: [] };
|
||||
const spotifyTrack = new Track(this, {
|
||||
title: spotifyData.name,
|
||||
|
@ -359,7 +372,9 @@ class Player extends EventEmitter<PlayerEvents> {
|
|||
}
|
||||
case QueryType.SPOTIFY_PLAYLIST:
|
||||
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: [] };
|
||||
|
||||
const playlist = new Playlist(this, {
|
||||
|
@ -408,9 +423,9 @@ class Player extends EventEmitter<PlayerEvents> {
|
|||
const data = new Track(this, {
|
||||
title: m.track.name ?? "",
|
||||
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,
|
||||
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)),
|
||||
views: 0,
|
||||
requestedBy: options.requestedBy as User,
|
||||
|
@ -585,6 +600,14 @@ class Player extends EventEmitter<PlayerEvents> {
|
|||
*[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 };
|
||||
|
|
|
@ -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 { StreamDispatcher } from "../VoiceInterface/StreamDispatcher";
|
||||
import Track from "./Track";
|
||||
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 { Util } from "../utils/Util";
|
||||
import YouTube from "youtube-sr";
|
||||
|
@ -11,6 +11,7 @@ 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;
|
||||
|
@ -22,7 +23,7 @@ class Queue<T = unknown> {
|
|||
public playing = false;
|
||||
public metadata?: T = null;
|
||||
public repeatMode: QueueRepeatMode = 0;
|
||||
public readonly id: Snowflake = SnowflakeUtil.generate();
|
||||
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
|
||||
|
@ -35,7 +36,7 @@ class Queue<T = unknown> {
|
|||
* 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
|
||||
* @param {PlayerOptions} [options] Player options for the queue
|
||||
*/
|
||||
constructor(player: Player, guild: Guild, options: PlayerOptions = {}) {
|
||||
/**
|
||||
|
@ -152,16 +153,16 @@ class Queue<T = unknown> {
|
|||
async connect(channel: GuildChannelResolvable) {
|
||||
if (this.#watchDestroyed()) return;
|
||||
const _channel = this.guild.channels.resolve(channel) as StageChannel | VoiceChannel;
|
||||
if (!["GUILD_STAGE_VOICE", "GUILD_VOICE"].includes(_channel?.type))
|
||||
throw new PlayerError(`Channel type must be GUILD_VOICE or GUILD_STAGE_VOICE, got ${_channel?.type}!`, ErrorStatusCode.INVALID_ARG_TYPE);
|
||||
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 === "GUILD_STAGE_VOICE") {
|
||||
await _channel.guild.me.voice.setSuppressed(false).catch(async () => {
|
||||
return await _channel.guild.me.voice.setRequestToSpeak(true).catch(Util.noop);
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -179,7 +180,7 @@ class Queue<T = unknown> {
|
|||
this.connection.on("start", (resource) => {
|
||||
if (this.#watchDestroyed(false)) return;
|
||||
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;
|
||||
});
|
||||
|
||||
|
@ -188,7 +189,7 @@ class Queue<T = unknown> {
|
|||
this.playing = false;
|
||||
if (this._filtersUpdate) return;
|
||||
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);
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -473,10 +470,10 @@ class Queue<T = unknown> {
|
|||
|
||||
/**
|
||||
* 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}
|
||||
*/
|
||||
remove(track: Track | Snowflake | number) {
|
||||
remove(track: Track | string | number) {
|
||||
if (this.#watchDestroyed()) return;
|
||||
let trackFound: Track = null;
|
||||
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.
|
||||
* @param {number|Track|Snowflake} track The track
|
||||
* @param {number|Track|string} track The track
|
||||
* @returns {number}
|
||||
*/
|
||||
getTrackPosition(track: number | Track | Snowflake) {
|
||||
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));
|
||||
|
@ -621,7 +618,7 @@ class Queue<T = unknown> {
|
|||
/**
|
||||
* 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
|
||||
* @param {PlayOptions} [options] The options
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async play(src?: Track, options: PlayOptions = {}): Promise<void> {
|
||||
|
@ -639,66 +636,46 @@ class Queue<T = unknown> {
|
|||
}
|
||||
|
||||
let stream = null;
|
||||
const customDownloader = typeof this.onBeforeCreateStream === "function";
|
||||
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 YouTube.search(`${track.author} ${track.title}`, { type: "video" })
|
||||
.then((x) => x[0].url)
|
||||
.then((res) => res[0].url)
|
||||
.catch(() => null);
|
||||
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) {
|
||||
stream = (await this.onBeforeCreateStream(track, spotifyResolved ? "youtube" : track.raw.source, this)) ?? null;
|
||||
if (stream)
|
||||
stream = ytdl
|
||||
.arbitraryStream(stream, {
|
||||
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 {
|
||||
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);
|
||||
});
|
||||
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 = ytdl(url, this.options.ytdlOptions);
|
||||
}
|
||||
} else {
|
||||
const tryArb = (customDownloader && (await this.onBeforeCreateStream(track, track.raw.source || track.raw.engine, this))) || null;
|
||||
const arbitrarySource = tryArb
|
||||
? tryArb
|
||||
: track.raw.source === "soundcloud"
|
||||
? await track.raw.engine.downloadProgressive()
|
||||
: typeof track.raw.engine === "function"
|
||||
? await 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 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 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,
|
||||
data: track,
|
||||
disableVolume: Boolean(this.options.disableVolume)
|
||||
|
@ -708,12 +685,11 @@ class Queue<T = unknown> {
|
|||
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);
|
||||
}
|
||||
|
||||
this.setVolume(this.options.initialVolume);
|
||||
|
||||
setTimeout(() => {
|
||||
this.connection.playStream(resource);
|
||||
}, this.#getBufferingTimeout()).unref();
|
||||
|
@ -731,9 +707,14 @@ class Queue<T = unknown> {
|
|||
if (this.options.leaveOnEnd) this.destroy();
|
||||
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])
|
||||
.catch(Util.noop);
|
||||
// fallback
|
||||
if (!info)
|
||||
info = await YouTube.search(track.author)
|
||||
.then((x) => x[0])
|
||||
.catch(Util.noop);
|
||||
if (!info) {
|
||||
if (this.options.leaveOnEnd) this.destroy();
|
||||
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 { RawTrackData, TrackJSON } from "../types/types";
|
||||
import { Playlist } from "./Playlist";
|
||||
|
@ -16,7 +16,7 @@ class Track {
|
|||
public requestedBy!: User;
|
||||
public playlist?: Playlist;
|
||||
public readonly raw: RawTrackData = {} as RawTrackData;
|
||||
public readonly id: Snowflake = SnowflakeUtil.generate();
|
||||
public readonly id = SnowflakeUtil.generate().toString();
|
||||
|
||||
/**
|
||||
* Track constructor
|
||||
|
@ -109,7 +109,7 @@ class Track {
|
|||
}
|
||||
|
||||
private _patch(data: RawTrackData) {
|
||||
this.title = Util.escapeMarkdown(data.title ?? "");
|
||||
this.title = escapeMarkdown(data.title ?? "");
|
||||
this.author = data.author ?? "";
|
||||
this.url = data.url ?? "";
|
||||
this.thumbnail = data.thumbnail ?? "";
|
||||
|
|
|
@ -129,7 +129,7 @@ class StreamDispatcher extends EventEmitter<VoiceEvents> {
|
|||
/**
|
||||
* Creates stream
|
||||
* @param {Readable|Duplex|string} src The stream source
|
||||
* @param {object} [ops={}] Options
|
||||
* @param {object} [ops] Options
|
||||
* @returns {AudioResource}
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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";
|
||||
|
||||
class VoiceUtils {
|
||||
|
@ -20,7 +20,7 @@ class VoiceUtils {
|
|||
/**
|
||||
* Joins a voice channel, creating basic stream dispatch manager
|
||||
* @param {StageChannel|VoiceChannel} channel The voice channel
|
||||
* @param {object} [options={}] Join options
|
||||
* @param {object} [options] Join options
|
||||
* @returns {Promise<StreamDispatcher>}
|
||||
*/
|
||||
public async connect(
|
||||
|
@ -39,7 +39,7 @@ class VoiceUtils {
|
|||
/**
|
||||
* Joins a voice channel
|
||||
* @param {StageChannel|VoiceChannel} [channel] The voice/stage channel to join
|
||||
* @param {object} [options={}] Join options
|
||||
* @param {object} [options] Join options
|
||||
* @returns {VoiceConnection}
|
||||
*/
|
||||
public async join(
|
||||
|
@ -59,16 +59,6 @@ class VoiceUtils {
|
|||
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
|
||||
* @param {VoiceConnection} connection The voice connection
|
||||
|
|
|
@ -13,6 +13,7 @@ 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;
|
||||
|
|
|
@ -121,6 +121,7 @@ export interface PlayerProgressbarOptions {
|
|||
length?: number;
|
||||
line?: string;
|
||||
indicator?: string;
|
||||
queue?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -130,12 +131,12 @@ export interface PlayerProgressbarOptions {
|
|||
* @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 {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 {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`
|
||||
* @property {Function} [onBeforeCreateStream] Runs before creating stream
|
||||
*/
|
||||
|
@ -476,7 +477,7 @@ export interface PlaylistJSON {
|
|||
/**
|
||||
* @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 {YTDLDownloadOptions} [ytdlOptions] The options passed to `ytdl-core`
|
||||
* @property {number} [connectionTimeout=20000] The voice connection timeout
|
||||
*/
|
||||
export interface PlayerInitOptions {
|
||||
|
|
|
@ -2,115 +2,106 @@ import { FiltersName } from "../types/types";
|
|||
|
||||
const bass = (g: number) => `bass=g=${g}:f=110:w=0.3`;
|
||||
|
||||
/**
|
||||
* The available audio filters
|
||||
* @typedef {object} 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 = {
|
||||
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",
|
||||
normalizer: "dynaudnorm=g=101",
|
||||
normalizer2: "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",
|
||||
|
||||
*[Symbol.iterator](): IterableIterator<{ name: FiltersName; value: string }> {
|
||||
for (const [k, v] of Object.entries(this)) {
|
||||
if (typeof this[k as FiltersName] === "string") yield { name: k as FiltersName, value: v as string };
|
||||
}
|
||||
},
|
||||
|
||||
get names() {
|
||||
return Object.keys(this).filter((p) => !["names", "length"].includes(p) && typeof this[p as FiltersName] !== "function") as FiltersName[];
|
||||
},
|
||||
|
||||
get length() {
|
||||
return Object.keys(this).filter((p) => !["names", "length"].includes(p) && typeof this[p as FiltersName] !== "function").length;
|
||||
},
|
||||
|
||||
toString() {
|
||||
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));
|
||||
class AudioFilters {
|
||||
public constructor() {
|
||||
return AudioFilters;
|
||||
}
|
||||
};
|
||||
|
||||
export default FilterList;
|
||||
export { FilterList as 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)) {
|
||||
if (typeof this.filters[k as FiltersName] === "string") yield { name: k as FiltersName, value: v as string };
|
||||
}
|
||||
}
|
||||
|
||||
public static get names() {
|
||||
return Object.keys(this).filter((p) => !["names", "length"].includes(p) && typeof this.filters[p as FiltersName] !== "function") as FiltersName[];
|
||||
}
|
||||
|
||||
// @ts-expect-error AudioFilters.length
|
||||
public static get length() {
|
||||
return Object.keys(this).filter((p) => !["names", "length"].includes(p) && typeof this.filters[p as FiltersName] !== "function").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(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 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 };
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.js"
|
||||
],
|
||||
"exclude": []
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES6",
|
||||
"target": "ES2020",
|
||||
"module": "commonjs",
|
||||
"declaration": true,
|
||||
"outDir": "./dist",
|
||||
|
|
Loading…
Reference in a new issue