Merge pull request #806 from Androz2091/develop
feat: better stream management
This commit is contained in:
commit
63bfb17e4b
15 changed files with 1711 additions and 1914 deletions
41
README.md
41
README.md
|
@ -32,11 +32,12 @@ $ npm install --save @discordjs/opus
|
||||||
- Simple & easy to use 🤘
|
- Simple & easy to use 🤘
|
||||||
- Beginner friendly 😱
|
- Beginner friendly 😱
|
||||||
- Audio filters 🎸
|
- Audio filters 🎸
|
||||||
- Lightweight 🛬
|
- Lightweight ☁️
|
||||||
- Custom extractors support 🌌
|
- Custom extractors support 🌌
|
||||||
- Lyrics 📃
|
|
||||||
- Multiple sources support ✌
|
- Multiple sources support ✌
|
||||||
- Play in multiple servers at the same time 🚗
|
- Play in multiple servers at the same time 🚗
|
||||||
|
- Does not inject anything to discord.js or your discord.js client 💉
|
||||||
|
- Allows you to have full control over what is going to be streamed 👑
|
||||||
|
|
||||||
## [Documentation](https://discord-player.js.org)
|
## [Documentation](https://discord-player.js.org)
|
||||||
|
|
||||||
|
@ -99,7 +100,7 @@ client.once("ready", () => {
|
||||||
client.on("interactionCreate", async (interaction) => {
|
client.on("interactionCreate", async (interaction) => {
|
||||||
if (!interaction.isCommand()) return;
|
if (!interaction.isCommand()) return;
|
||||||
|
|
||||||
// /play 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 });
|
||||||
|
@ -156,9 +157,12 @@ You just need to install it using `npm i --save @discord-player/extractor` (disc
|
||||||
These bots are made by the community, they can help you build your own!
|
These bots are made by the community, they can help you build your own!
|
||||||
|
|
||||||
* **[Discord Music Bot](https://github.com/Androz2091/discord-music-bot)** by [Androz2091](https://github.com/Androz2091)
|
* **[Discord Music Bot](https://github.com/Androz2091/discord-music-bot)** by [Androz2091](https://github.com/Androz2091)
|
||||||
|
* [Dodong](https://github.com/nizeic/Dodong) by [nizeic](https://github.com/nizeic)
|
||||||
|
* [Musico](https://github.com/Whirl21/Musico) by [Whirl21](https://github.com/Whirl21)
|
||||||
|
* [Eyesense-Music-Bot](https://github.com/naseif/Eyesense-Music-Bot) by [naseif](https://github.com/naseif)
|
||||||
|
* [Music-bot](https://github.com/ZerioDev/Music-bot) by [ZerioDev](https://github.com/ZerioDev)
|
||||||
* [AtlantaBot](https://github.com/Androz2091/AtlantaBot) by [Androz2091](https://github.com/Androz2091) (**outdated**)
|
* [AtlantaBot](https://github.com/Androz2091/AtlantaBot) by [Androz2091](https://github.com/Androz2091) (**outdated**)
|
||||||
* [Discord-Music](https://github.com/inhydrox/discord-music) by [inhydrox](https://github.com/inhydrox) (**outdated**)
|
* [Discord-Music](https://github.com/inhydrox/discord-music) by [inhydrox](https://github.com/inhydrox) (**outdated**)
|
||||||
* [Music-bot](https://github.com/ZerioDev/Music-bot) by [ZerioDev](https://github.com/ZerioDev) (**outdated**)
|
|
||||||
|
|
||||||
## Advanced
|
## Advanced
|
||||||
|
|
||||||
|
@ -191,3 +195,32 @@ const player = new Player(client, {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> You may also create a simple proxy server and forward requests through it.
|
||||||
|
> See **[https://github.com/http-party/node-http-proxy](https://github.com/http-party/node-http-proxy)** for more info.
|
||||||
|
|
||||||
|
### Custom stream Engine
|
||||||
|
|
||||||
|
Discord Player by default uses **[node-ytdl-core](https://github.com/fent/node-ytdl-core)** for youtube and some other extractors for other sources.
|
||||||
|
If you need to modify this behavior without touching extractors, you need to use `createStream` functionality of discord player.
|
||||||
|
Here's an example on how you can use **[play-dl](https://npmjs.com/package/play-dl)** to download youtube streams instead of using ytdl-core.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const playdl = require("play-dl");
|
||||||
|
|
||||||
|
// other code
|
||||||
|
const queue = player.createQueue(..., {
|
||||||
|
...,
|
||||||
|
async onBeforeCreateStream(track, source, _queue) {
|
||||||
|
// only trap youtube source
|
||||||
|
if (source === "youtube") {
|
||||||
|
// track here would be youtube track
|
||||||
|
return (await playdl.stream(track.url)).stream;
|
||||||
|
// we must return readable stream or void (returning void means telling discord-player to look for default extractor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
`<Queue>.onBeforeCreateStream` is called before actually downloading the stream. It is a different concept from extractors, where you are **just** downloading
|
||||||
|
streams. `source` here will be a video source. Streams from `onBeforeCreateStream` are then piped to `FFmpeg` and finally sent to Discord voice servers.
|
||||||
|
|
51
docs/extractors/create_stream.md
Normal file
51
docs/extractors/create_stream.md
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
# Create Stream
|
||||||
|
|
||||||
|
This is a checkpoint where discord-player calls `createStream` before downloading stream.
|
||||||
|
|
||||||
|
### Custom stream Engine
|
||||||
|
|
||||||
|
Discord Player by default uses **[node-ytdl-core](https://github.com/fent/node-ytdl-core)** for youtube and some other extractors for other sources.
|
||||||
|
If you need to modify this behavior without touching extractors, you need to use `createStream` functionality of discord player.
|
||||||
|
Here's an example on how you can use **[play-dl](https://npmjs.com/package/play-dl)** to download youtube streams instead of using ytdl-core.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const playdl = require("play-dl");
|
||||||
|
|
||||||
|
// other code
|
||||||
|
const queue = player.createQueue(..., {
|
||||||
|
...,
|
||||||
|
async onBeforeCreateStream(track, source, _queue) {
|
||||||
|
// only trap youtube source
|
||||||
|
if (source === "youtube") {
|
||||||
|
// track here would be youtube track
|
||||||
|
return (await playdl.stream(track.url)).stream;
|
||||||
|
// we must return readable stream or void (returning void means telling discord-player to look for default extractor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
`<Queue>.onBeforeCreateStream` is called before actually downloading the stream. It is a different concept from extractors, where you are **just** downloading
|
||||||
|
streams. `source` here will be a video source. Streams from `onBeforeCreateStream` are then piped to `FFmpeg` and finally sent to Discord voice servers.
|
||||||
|
|
||||||
|
# FAQ
|
||||||
|
## How can I remove this?
|
||||||
|
|
||||||
|
> If you already made this change and want to switch to default mode in runtime,
|
||||||
|
> you can set `queue.onBeforeCreateStream` to `null` which will make `discord-player` use default config.
|
||||||
|
|
||||||
|
## Which stream format should I return?
|
||||||
|
|
||||||
|
> It's not necessary to return opus format or whatever, since every streams have to be converted to s16le, due to inline volume.
|
||||||
|
|
||||||
|
## Can I use ytdl-core-discord?
|
||||||
|
|
||||||
|
> Yes, you can.
|
||||||
|
|
||||||
|
## Can I use this for other sources, like soundcloud?
|
||||||
|
|
||||||
|
> Absolutely.
|
||||||
|
|
||||||
|
## This is not working properly
|
||||||
|
|
||||||
|
> `onBeforeCreateStream` may not work properly if you have `spotifyBridge` enabled (enabled by default).
|
26
docs/faq/how_does_it_work.md
Normal file
26
docs/faq/how_does_it_work.md
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
# How does Discord Player actually work?
|
||||||
|
|
||||||
|
- Discord Player can be used by first initializing `Player` class with your discord.js client. Discord Player uses `Queue` to assign queue manager to individual guild.
|
||||||
|
Which means each guild will have its own queue object. Every player action has to go through the `Queue` object for example, `play`, `pause`, `volume` etc.
|
||||||
|
|
||||||
|
- When `Player` is initialized, it creates a cache to store external extractors or queues information. Queue is created by calling `createQueue` method of `Player` instance.
|
||||||
|
A client should have only one `Player` instance, otherwise it will be complicated to track queues and other metadata.
|
||||||
|
|
||||||
|
- Searching tracks can be done via `search` method of `Player` instance. Discord Player offers `search engine` option to target specific searches. Discord Player first
|
||||||
|
calls all the registered extractors first with the search query. If all external extractors failed to validate the query, player then passes the query to built-in extractors.
|
||||||
|
Invalid or unknown queries may return `arbitrary` result.
|
||||||
|
|
||||||
|
- The track result obtained from `search` can be loaded into `Queue` by calling `queue.addTrack`/`queue.addTracks`/`queue.play`.
|
||||||
|
|
||||||
|
- Player calls `onBeforeCreateStream` if user has enabled the function while creating queue. This method runs each time before stream is downloaded. Users may use it
|
||||||
|
to modify how and which stream will be played.
|
||||||
|
|
||||||
|
- Queue is based on FIFO method (First In First Out)
|
||||||
|
|
||||||
|
- Final stream is a pcm format, required for volume controls which is created by Discord Player itself.
|
||||||
|
|
||||||
|
- Since inline volume is enabled by default for volume controls, you may face more resource usage.
|
||||||
|
|
||||||
|
- You can disable inline volume for better performance but setting volume won't work and current volume will always be 100.
|
||||||
|
|
||||||
|
- All the audio filters are handled by FFmpeg and stream has to reload in order to update filters.
|
|
@ -11,12 +11,16 @@
|
||||||
files:
|
files:
|
||||||
- name: Extractors API
|
- name: Extractors API
|
||||||
path: extractor.md
|
path: extractor.md
|
||||||
|
- name: Creating Stream
|
||||||
|
path: create_stream.md
|
||||||
- name: FAQ
|
- name: FAQ
|
||||||
files:
|
files:
|
||||||
- name: Custom Filters
|
- name: Custom Filters
|
||||||
path: custom_filters.md
|
path: custom_filters.md
|
||||||
- name: Slash Commands
|
- name: Slash Commands
|
||||||
path: slash_commands.md
|
path: slash_commands.md
|
||||||
|
- name: How Does It Work
|
||||||
|
path: how_does_it_work.md
|
||||||
- name: YouTube
|
- name: YouTube
|
||||||
files:
|
files:
|
||||||
- name: Using Cookies
|
- name: Using Cookies
|
||||||
|
|
|
@ -14,3 +14,6 @@ const player = new Player(client, {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> You may also create a simple proxy server and forward requests through it.
|
||||||
|
> See **[https://github.com/http-party/node-http-proxy](https://github.com/http-party/node-http-proxy)** for more info.
|
39
package.json
39
package.json
|
@ -22,6 +22,7 @@
|
||||||
"docs": "docgen --jsdoc jsdoc.json --source src/*.ts src/**/*.ts --custom docs/index.yml --output docs/docs.json",
|
"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",
|
"docs:test": "docgen --jsdoc jsdoc.json --source src/*.ts src/**/*.ts --custom docs/index.yml",
|
||||||
"lint": "eslint src --ext .ts",
|
"lint": "eslint src --ext .ts",
|
||||||
|
"prepare": "husky install",
|
||||||
"lint:fix": "eslint src --ext .ts --fix"
|
"lint:fix": "eslint src --ext .ts --fix"
|
||||||
},
|
},
|
||||||
"funding": "https://github.com/Androz2091/discord-player?sponsor=1",
|
"funding": "https://github.com/Androz2091/discord-player?sponsor=1",
|
||||||
|
@ -59,37 +60,37 @@
|
||||||
},
|
},
|
||||||
"homepage": "https://discord-player.js.org",
|
"homepage": "https://discord-player.js.org",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@discordjs/voice": "^0.6.0",
|
"@discordjs/voice": "^0.7.5",
|
||||||
"discord-ytdl-core": "^5.0.4",
|
"discord-ytdl-core": "^5.0.4",
|
||||||
"libsodium-wrappers": "^0.7.9",
|
"libsodium-wrappers": "^0.7.9",
|
||||||
"soundcloud-scraper": "^5.0.2",
|
"soundcloud-scraper": "^5.0.2",
|
||||||
"spotify-url-info": "^2.2.3",
|
"spotify-url-info": "^2.2.3",
|
||||||
"tiny-typed-emitter": "^2.1.0",
|
"tiny-typed-emitter": "^2.1.0",
|
||||||
"youtube-sr": "^4.1.7",
|
"youtube-sr": "^4.1.9",
|
||||||
"ytdl-core": "^4.9.1"
|
"ytdl-core": "^4.9.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/cli": "^7.14.8",
|
"@babel/cli": "^7.16.0",
|
||||||
"@babel/core": "^7.15.0",
|
"@babel/core": "^7.16.0",
|
||||||
"@babel/preset-env": "^7.15.0",
|
"@babel/preset-env": "^7.16.4",
|
||||||
"@babel/preset-typescript": "^7.15.0",
|
"@babel/preset-typescript": "^7.16.0",
|
||||||
"@devsnowflake/docgen": "devsnowflake/docgen#ts-patch",
|
"@devsnowflake/docgen": "devsnowflake/docgen#ts-patch",
|
||||||
"@discord-player/extractor": "^3.0.2",
|
"@discord-player/extractor": "^3.0.2",
|
||||||
"@discordjs/opus": "github:discordjs/opus",
|
"@discordjs/opus": "github:discordjs/opus",
|
||||||
"@favware/rollup-type-bundler": "^1.0.3",
|
"@favware/rollup-type-bundler": "^1.0.6",
|
||||||
"@types/node": "^16.6.2",
|
"@types/node": "^16.11.10",
|
||||||
"@types/ws": "^7.4.7",
|
"@types/ws": "^8.2.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.29.2",
|
"@typescript-eslint/eslint-plugin": "^5.4.0",
|
||||||
"@typescript-eslint/parser": "^4.29.2",
|
"@typescript-eslint/parser": "^5.4.0",
|
||||||
"discord-api-types": "^0.22.0",
|
"discord-api-types": "^0.24.0",
|
||||||
"discord.js": "^13.1.0",
|
"discord.js": "^13.3.1",
|
||||||
"eslint": "^7.32.0",
|
"eslint": "^8.3.0",
|
||||||
"gen-esm-wrapper": "^1.1.2",
|
"gen-esm-wrapper": "^1.1.3",
|
||||||
"husky": "^7.0.1",
|
"husky": "^7.0.4",
|
||||||
"jsdoc-babel": "^0.5.0",
|
"jsdoc-babel": "^0.5.0",
|
||||||
"prettier": "^2.3.2",
|
"prettier": "^2.5.0",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"ts-node": "^10.2.1",
|
"ts-node": "^10.4.0",
|
||||||
"typescript": "^4.3.5"
|
"typescript": "^4.5.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
162
src/Player.ts
162
src/Player.ts
|
@ -2,7 +2,7 @@ import { Client, Collection, GuildResolvable, Snowflake, User, VoiceState, Inten
|
||||||
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 } from "./types/types";
|
import { PlayerEvents, PlayerOptions, QueryType, SearchOptions, PlayerInitOptions, PlayerSearchResult } 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";
|
||||||
|
@ -29,6 +29,7 @@ class Player extends EventEmitter<PlayerEvents> {
|
||||||
public readonly queues = new Collection<Snowflake, Queue>();
|
public readonly queues = new Collection<Snowflake, Queue>();
|
||||||
public readonly voiceUtils = new VoiceUtils();
|
public readonly voiceUtils = new VoiceUtils();
|
||||||
public readonly extractors = new Collection<string, ExtractorModel>();
|
public readonly extractors = new Collection<string, ExtractorModel>();
|
||||||
|
public requiredEvents = ["error", "connectionError"] as string[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates new Discord Player
|
* Creates new Discord Player
|
||||||
|
@ -77,51 +78,65 @@ class Player extends EventEmitter<PlayerEvents> {
|
||||||
if (!queue) return;
|
if (!queue) return;
|
||||||
|
|
||||||
if (oldState.channelId && newState.channelId && oldState.channelId !== newState.channelId) {
|
if (oldState.channelId && newState.channelId && oldState.channelId !== newState.channelId) {
|
||||||
if (queue?.connection) queue.connection.channel = newState.channel;
|
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.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 (!channelEmpty && emptyTimeout) {
|
|
||||||
clearTimeout(emptyTimeout);
|
|
||||||
queue._cooldownsTimeout.delete(`empty_${oldState.guild.id}`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (!Util.isVoiceEmpty(queue.connection.channel)) return;
|
|
||||||
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;
|
const timeout = setTimeout(() => {
|
||||||
if (queue.options.leaveOnEmpty) queue.destroy();
|
if (!Util.isVoiceEmpty(queue.connection.channel)) return;
|
||||||
this.emit("channelEmpty", queue);
|
if (!this.queues.has(queue.guild.id)) return;
|
||||||
}, queue.options.leaveOnEmptyCooldown || 0).unref();
|
if (queue.options.leaveOnEmpty) queue.destroy();
|
||||||
queue._cooldownsTimeout.set(`empty_${oldState.guild.id}`, timeout);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -176,7 +191,7 @@ class Player extends EventEmitter<PlayerEvents> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {object} SearchResult
|
* @typedef {object} PlayerSearchResult
|
||||||
* @property {Playlist} [playlist] The playlist (if any)
|
* @property {Playlist} [playlist] The playlist (if any)
|
||||||
* @property {Track[]} tracks The tracks
|
* @property {Track[]} tracks The tracks
|
||||||
*/
|
*/
|
||||||
|
@ -184,13 +199,40 @@ class Player extends EventEmitter<PlayerEvents> {
|
||||||
* Search tracks
|
* Search tracks
|
||||||
* @param {string|Track} query The search query
|
* @param {string|Track} query The search query
|
||||||
* @param {SearchOptions} options The search options
|
* @param {SearchOptions} options The search options
|
||||||
* @returns {Promise<SearchResult>}
|
* @returns {Promise<PlayerSearchResult>}
|
||||||
*/
|
*/
|
||||||
async search(query: string | Track, options: SearchOptions) {
|
async search(query: string | Track, options: SearchOptions): Promise<PlayerSearchResult> {
|
||||||
if (query instanceof Track) return { playlist: null, tracks: [query] };
|
if (query instanceof Track) return { playlist: query.playlist || null, tracks: [query] };
|
||||||
if (!options) throw new PlayerError("DiscordPlayer#search needs search options!", ErrorStatusCode.INVALID_ARG_TYPE);
|
if (!options) throw new PlayerError("DiscordPlayer#search needs search options!", ErrorStatusCode.INVALID_ARG_TYPE);
|
||||||
options.requestedBy = this.client.users.resolve(options.requestedBy);
|
options.requestedBy = this.client.users.resolve(options.requestedBy);
|
||||||
if (!("searchEngine" in options)) options.searchEngine = QueryType.AUTO;
|
if (!("searchEngine" in options)) options.searchEngine = QueryType.AUTO;
|
||||||
|
if (typeof options.searchEngine === "string" && this.extractors.has(options.searchEngine)) {
|
||||||
|
const extractor = this.extractors.get(options.searchEngine);
|
||||||
|
if (!extractor.validate(query)) return { playlist: null, tracks: [] };
|
||||||
|
const data = await extractor.handle(query);
|
||||||
|
if (data && data.data.length) {
|
||||||
|
const playlist = !data.playlist
|
||||||
|
? null
|
||||||
|
: new Playlist(this, {
|
||||||
|
...data.playlist,
|
||||||
|
tracks: []
|
||||||
|
});
|
||||||
|
|
||||||
|
const tracks = data.data.map(
|
||||||
|
(m) =>
|
||||||
|
new Track(this, {
|
||||||
|
...m,
|
||||||
|
requestedBy: options.requestedBy as User,
|
||||||
|
duration: Util.buildTimeCode(Util.parseMS(m.duration)),
|
||||||
|
playlist: playlist
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
if (playlist) playlist.tracks = tracks;
|
||||||
|
|
||||||
|
return { playlist: playlist, tracks: tracks };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
for (const [_, extractor] of this.extractors) {
|
for (const [_, extractor] of this.extractors) {
|
||||||
|
@ -224,7 +266,7 @@ class Player extends EventEmitter<PlayerEvents> {
|
||||||
const qt = options.searchEngine === QueryType.AUTO ? QueryResolver.resolve(query) : options.searchEngine;
|
const qt = options.searchEngine === QueryType.AUTO ? QueryResolver.resolve(query) : options.searchEngine;
|
||||||
switch (qt) {
|
switch (qt) {
|
||||||
case QueryType.YOUTUBE_VIDEO: {
|
case QueryType.YOUTUBE_VIDEO: {
|
||||||
const info = await ytdlGetInfo(query).catch(Util.noop);
|
const info = await ytdlGetInfo(query, this.options.ytdlOptions).catch(Util.noop);
|
||||||
if (!info) return { playlist: null, tracks: [] };
|
if (!info) return { playlist: null, tracks: [] };
|
||||||
|
|
||||||
const track = new Track(this, {
|
const track = new Track(this, {
|
||||||
|
@ -509,7 +551,25 @@ class Player extends EventEmitter<PlayerEvents> {
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
scanDeps() {
|
scanDeps() {
|
||||||
return generateDependencyReport();
|
const line = "-".repeat(50);
|
||||||
|
const depsReport = generateDependencyReport();
|
||||||
|
const extractorReport = this.extractors
|
||||||
|
.map((m) => {
|
||||||
|
return `${m.name} :: ${m.version || "0.1.0"}`;
|
||||||
|
})
|
||||||
|
.join("\n");
|
||||||
|
return `${depsReport}\n${line}\nLoaded Extractors:\n${extractorReport || "None"}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit<U extends keyof PlayerEvents>(eventName: U, ...args: Parameters<PlayerEvents[U]>): boolean {
|
||||||
|
if (this.requiredEvents.includes(eventName) && !super.eventNames().includes(eventName)) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(...args);
|
||||||
|
process.emitWarning(`[DiscordPlayerWarning] Unhandled "${eventName}" event! Events ${this.requiredEvents.map((m) => `"${m}"`).join(", ")} must have event listeners!`);
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return super.emit(eventName, ...args);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -39,7 +39,12 @@ export class PlayerError extends Error {
|
||||||
}
|
}
|
||||||
|
|
||||||
toJSON() {
|
toJSON() {
|
||||||
return { stack: this.stack, code: this.statusCode, created: this.createdTimestamp };
|
return {
|
||||||
|
stack: this.stack,
|
||||||
|
code: this.statusCode,
|
||||||
|
message: this.message,
|
||||||
|
created: this.createdTimestamp
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
toString() {
|
toString() {
|
||||||
|
|
|
@ -2,13 +2,14 @@ import { Collection, Guild, StageChannel, VoiceChannel, Snowflake, SnowflakeUtil
|
||||||
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 } from "../types/types";
|
import { PlayerOptions, PlayerProgressbarOptions, PlayOptions, QueueFilters, QueueRepeatMode, TrackSource } from "../types/types";
|
||||||
import ytdl from "discord-ytdl-core";
|
import ytdl from "discord-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";
|
||||||
import AudioFilters from "../utils/AudioFilters";
|
import AudioFilters from "../utils/AudioFilters";
|
||||||
import { PlayerError, ErrorStatusCode } from "./PlayerError";
|
import { PlayerError, ErrorStatusCode } from "./PlayerError";
|
||||||
|
import type { Readable } from "stream";
|
||||||
|
|
||||||
class Queue<T = unknown> {
|
class Queue<T = unknown> {
|
||||||
public readonly guild: Guild;
|
public readonly guild: Guild;
|
||||||
|
@ -27,6 +28,7 @@ class Queue<T = unknown> {
|
||||||
private _filtersUpdate = false;
|
private _filtersUpdate = false;
|
||||||
#lastVolume = 0;
|
#lastVolume = 0;
|
||||||
#destroyed = false;
|
#destroyed = false;
|
||||||
|
public onBeforeCreateStream: (track: Track, source: TrackSource, queue: Queue) => Promise<Readable | undefined> = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Queue constructor
|
* Queue constructor
|
||||||
|
@ -103,11 +105,15 @@ class Queue<T = unknown> {
|
||||||
highWaterMark: 1 << 25
|
highWaterMark: 1 << 25
|
||||||
},
|
},
|
||||||
initialVolume: 100,
|
initialVolume: 100,
|
||||||
bufferingTimeout: 3000
|
bufferingTimeout: 3000,
|
||||||
|
spotifyBridge: true,
|
||||||
|
disableVolume: false
|
||||||
} as PlayerOptions,
|
} as PlayerOptions,
|
||||||
options
|
options
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if ("onBeforeCreateStream" in this.options) this.onBeforeCreateStream = this.options.onBeforeCreateStream;
|
||||||
|
|
||||||
this.player.emit("debug", this, `Queue initialized:\n\n${this.player.scanDeps()}`);
|
this.player.emit("debug", this, `Queue initialized:\n\n${this.player.scanDeps()}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -189,16 +195,14 @@ class Queue<T = unknown> {
|
||||||
if (!this.tracks.length && this.repeatMode === QueueRepeatMode.OFF) {
|
if (!this.tracks.length && this.repeatMode === QueueRepeatMode.OFF) {
|
||||||
if (this.options.leaveOnEnd) this.destroy();
|
if (this.options.leaveOnEnd) this.destroy();
|
||||||
this.player.emit("queueEnd", this);
|
this.player.emit("queueEnd", this);
|
||||||
|
} else if (!this.tracks.length && this.repeatMode === QueueRepeatMode.AUTOPLAY) {
|
||||||
|
this._handleAutoplay(Util.last(this.previousTracks));
|
||||||
} else {
|
} else {
|
||||||
if (this.repeatMode !== QueueRepeatMode.AUTOPLAY) {
|
if (this.repeatMode === QueueRepeatMode.TRACK) return void this.play(Util.last(this.previousTracks), { immediate: true });
|
||||||
if (this.repeatMode === QueueRepeatMode.TRACK) return void this.play(Util.last(this.previousTracks), { immediate: true });
|
if (this.repeatMode === QueueRepeatMode.QUEUE) this.tracks.push(Util.last(this.previousTracks));
|
||||||
if (this.repeatMode === QueueRepeatMode.QUEUE) this.tracks.push(Util.last(this.previousTracks));
|
const nextTrack = this.tracks.shift();
|
||||||
const nextTrack = this.tracks.shift();
|
this.play(nextTrack, { immediate: true });
|
||||||
this.play(nextTrack, { immediate: true });
|
return;
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
this._handleAutoplay(Util.last(this.previousTracks));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -486,6 +490,17 @@ class Queue<T = unknown> {
|
||||||
return trackFound;
|
return trackFound;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the index of the specified track. If found, returns the track index else returns -1.
|
||||||
|
* @param {number|Track|Snowflake} track The track
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
getTrackPosition(track: number | Track | Snowflake) {
|
||||||
|
if (this.#watchDestroyed()) return;
|
||||||
|
if (typeof track === "number") return this.tracks[track] != null ? track : -1;
|
||||||
|
return this.tracks.findIndex((pred) => pred.id === (track instanceof Track ? track.id : track));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Jumps to particular track
|
* Jumps to particular track
|
||||||
* @param {Track|number} track The track
|
* @param {Track|number} track The track
|
||||||
|
@ -493,14 +508,26 @@ class Queue<T = unknown> {
|
||||||
*/
|
*/
|
||||||
jump(track: Track | number): void {
|
jump(track: Track | number): void {
|
||||||
if (this.#watchDestroyed()) return;
|
if (this.#watchDestroyed()) return;
|
||||||
// remove the track if exists
|
|
||||||
const foundTrack = this.remove(track);
|
const foundTrack = this.remove(track);
|
||||||
if (!foundTrack) throw new PlayerError("Track not found", ErrorStatusCode.TRACK_NOT_FOUND);
|
if (!foundTrack) throw new PlayerError("Track not found", ErrorStatusCode.TRACK_NOT_FOUND);
|
||||||
// since we removed the existing track from the queue,
|
|
||||||
// we now have to place that to position 1
|
this.tracks.splice(0, 0, foundTrack);
|
||||||
// because we want to jump to that track
|
|
||||||
// this will skip current track and play the next one which will be the foundTrack
|
return void this.skip();
|
||||||
this.tracks.splice(1, 0, foundTrack);
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Jumps to particular track, removing other tracks on the way
|
||||||
|
* @param {Track|number} track The track
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
skipTo(track: Track | number): void {
|
||||||
|
if (this.#watchDestroyed()) return;
|
||||||
|
const trackIndex = this.getTrackPosition(track);
|
||||||
|
const removedTrack = this.remove(track);
|
||||||
|
if (!removedTrack) throw new PlayerError("Track not found", ErrorStatusCode.TRACK_NOT_FOUND);
|
||||||
|
|
||||||
|
this.tracks.splice(0, trackIndex, removedTrack);
|
||||||
|
|
||||||
return void this.skip();
|
return void this.skip();
|
||||||
}
|
}
|
||||||
|
@ -511,6 +538,7 @@ class Queue<T = unknown> {
|
||||||
* @param {number} [index=0] The index where this track should be
|
* @param {number} [index=0] The index where this track should be
|
||||||
*/
|
*/
|
||||||
insert(track: Track, index = 0) {
|
insert(track: Track, index = 0) {
|
||||||
|
if (this.#watchDestroyed()) return;
|
||||||
if (!track || !(track instanceof Track)) throw new PlayerError("track must be the instance of Track", ErrorStatusCode.INVALID_TRACK);
|
if (!track || !(track instanceof Track)) throw new PlayerError("track must be the instance of Track", ErrorStatusCode.INVALID_TRACK);
|
||||||
if (typeof index !== "number" || index < 0 || !Number.isFinite(index)) throw new PlayerError(`Invalid index "${index}"`, ErrorStatusCode.INVALID_ARG_TYPE);
|
if (typeof index !== "number" || index < 0 || !Number.isFinite(index)) throw new PlayerError(`Invalid index "${index}"`, ErrorStatusCode.INVALID_ARG_TYPE);
|
||||||
|
|
||||||
|
@ -606,38 +634,61 @@ class Queue<T = unknown> {
|
||||||
this.previousTracks.push(track);
|
this.previousTracks.push(track);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: remove discord-ytdl-core
|
let stream = null;
|
||||||
let stream;
|
const customDownloader = typeof this.onBeforeCreateStream === "function";
|
||||||
|
|
||||||
if (["youtube", "spotify"].includes(track.raw.source)) {
|
if (["youtube", "spotify"].includes(track.raw.source)) {
|
||||||
if (track.raw.source === "spotify" && !track.raw.engine) {
|
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" })
|
track.raw.engine = await YouTube.search(`${track.author} ${track.title}`, { type: "video" })
|
||||||
.then((x) => x[0].url)
|
.then((x) => x[0].url)
|
||||||
.catch(() => null);
|
.catch(() => null);
|
||||||
|
spotifyResolved = true;
|
||||||
}
|
}
|
||||||
const link = track.raw.source === "spotify" ? track.raw.engine : track.url;
|
const link = track.raw.source === "spotify" ? track.raw.engine : track.url;
|
||||||
if (!link) return void this.play(this.tracks.shift(), { immediate: true });
|
if (!link) return void this.play(this.tracks.shift(), { immediate: true });
|
||||||
|
|
||||||
stream = ytdl(link, {
|
if (customDownloader) {
|
||||||
...this.options.ytdlOptions,
|
stream = (await this.onBeforeCreateStream(track, spotifyResolved ? "youtube" : track.raw.source, this)) ?? null;
|
||||||
// discord-ytdl-core
|
if (stream)
|
||||||
opusEncoded: false,
|
stream = ytdl
|
||||||
fmt: "s16le",
|
.arbitraryStream(stream, {
|
||||||
encoderArgs: options.encoderArgs ?? this._activeFilters.length ? ["-af", AudioFilters.create(this._activeFilters)] : [],
|
opusEncoded: false,
|
||||||
seek: options.seek ? options.seek / 1000 : 0
|
fmt: "s16le",
|
||||||
}).on("error", (err) => {
|
encoderArgs: options.encoderArgs ?? this._activeFilters.length ? ["-af", AudioFilters.create(this._activeFilters)] : [],
|
||||||
return err.message.toLowerCase().includes("premature close") ? null : this.player.emit("error", this, err);
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
} else {
|
} 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
|
stream = ytdl
|
||||||
.arbitraryStream(
|
.arbitraryStream(arbitrarySource, {
|
||||||
track.raw.source === "soundcloud" ? await track.raw.engine.downloadProgressive() : typeof track.raw.engine === "function" ? await track.raw.engine() : track.raw.engine,
|
opusEncoded: false,
|
||||||
{
|
fmt: "s16le",
|
||||||
opusEncoded: false,
|
encoderArgs: options.encoderArgs ?? this._activeFilters.length ? ["-af", AudioFilters.create(this._activeFilters)] : [],
|
||||||
fmt: "s16le",
|
seek: options.seek ? options.seek / 1000 : 0
|
||||||
encoderArgs: options.encoderArgs ?? this._activeFilters.length ? ["-af", AudioFilters.create(this._activeFilters)] : [],
|
})
|
||||||
seek: options.seek ? options.seek / 1000 : 0
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.on("error", (err) => {
|
.on("error", (err) => {
|
||||||
return err.message.toLowerCase().includes("premature close") ? null : this.player.emit("error", this, err);
|
return err.message.toLowerCase().includes("premature close") ? null : this.player.emit("error", this, err);
|
||||||
});
|
});
|
||||||
|
@ -645,7 +696,8 @@ class Queue<T = unknown> {
|
||||||
|
|
||||||
const resource: AudioResource<Track> = this.connection.createStream(stream, {
|
const resource: AudioResource<Track> = this.connection.createStream(stream, {
|
||||||
type: StreamType.Raw,
|
type: StreamType.Raw,
|
||||||
data: track
|
data: track,
|
||||||
|
disableVolume: Boolean(this.options.disableVolume)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (options.seek) this._streamTime = options.seek;
|
if (options.seek) this._streamTime = options.seek;
|
||||||
|
|
|
@ -121,7 +121,7 @@ class Track {
|
||||||
* @type {Queue}
|
* @type {Queue}
|
||||||
*/
|
*/
|
||||||
get queue(): Queue {
|
get queue(): Queue {
|
||||||
return this.player.queues.find((q) => q.tracks.includes(this));
|
return this.player.queues.find((q) => q.tracks.some((ab) => ab.id === this.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -173,7 +173,7 @@ class Track {
|
||||||
duration: this.duration,
|
duration: this.duration,
|
||||||
durationMS: this.durationMS,
|
durationMS: this.durationMS,
|
||||||
views: this.views,
|
views: this.views,
|
||||||
requestedBy: this.requestedBy.id,
|
requestedBy: this.requestedBy?.id,
|
||||||
playlist: hidePlaylist ? null : this.playlist?.toJSON() ?? null
|
playlist: hidePlaylist ? null : this.playlist?.toJSON() ?? null
|
||||||
} as TrackJSON;
|
} as TrackJSON;
|
||||||
}
|
}
|
||||||
|
|
|
@ -74,13 +74,21 @@ class StreamDispatcher extends EventEmitter<VoiceEvents> {
|
||||||
try {
|
try {
|
||||||
await entersState(this.voiceConnection, VoiceConnectionStatus.Connecting, this.connectionTimeout);
|
await entersState(this.voiceConnection, VoiceConnectionStatus.Connecting, this.connectionTimeout);
|
||||||
} catch {
|
} catch {
|
||||||
this.voiceConnection.destroy();
|
try {
|
||||||
|
this.voiceConnection.destroy();
|
||||||
|
} catch (err) {
|
||||||
|
this.emit("error", err as AudioPlayerError);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (this.voiceConnection.rejoinAttempts < 5) {
|
} else if (this.voiceConnection.rejoinAttempts < 5) {
|
||||||
await Util.wait((this.voiceConnection.rejoinAttempts + 1) * 5000);
|
await Util.wait((this.voiceConnection.rejoinAttempts + 1) * 5000);
|
||||||
this.voiceConnection.rejoin();
|
this.voiceConnection.rejoin();
|
||||||
} else {
|
} else {
|
||||||
this.voiceConnection.destroy();
|
try {
|
||||||
|
this.voiceConnection.destroy();
|
||||||
|
} catch (err) {
|
||||||
|
this.emit("error", err as AudioPlayerError);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (newState.status === VoiceConnectionStatus.Destroyed) {
|
} else if (newState.status === VoiceConnectionStatus.Destroyed) {
|
||||||
this.end();
|
this.end();
|
||||||
|
@ -89,7 +97,13 @@ class StreamDispatcher extends EventEmitter<VoiceEvents> {
|
||||||
try {
|
try {
|
||||||
await entersState(this.voiceConnection, VoiceConnectionStatus.Ready, this.connectionTimeout);
|
await entersState(this.voiceConnection, VoiceConnectionStatus.Ready, this.connectionTimeout);
|
||||||
} catch {
|
} catch {
|
||||||
if (this.voiceConnection.state.status !== VoiceConnectionStatus.Destroyed) this.voiceConnection.destroy();
|
if (this.voiceConnection.state.status !== VoiceConnectionStatus.Destroyed) {
|
||||||
|
try {
|
||||||
|
this.voiceConnection.destroy();
|
||||||
|
} catch (err) {
|
||||||
|
this.emit("error", err as AudioPlayerError);
|
||||||
|
}
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
this.readyLock = false;
|
this.readyLock = false;
|
||||||
}
|
}
|
||||||
|
@ -119,11 +133,12 @@ class StreamDispatcher extends EventEmitter<VoiceEvents> {
|
||||||
* @returns {AudioResource}
|
* @returns {AudioResource}
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
createStream(src: Readable | Duplex | string, ops?: { type?: StreamType; data?: any }) {
|
createStream(src: Readable | Duplex | string, ops?: { type?: StreamType; data?: any; disableVolume?: boolean }) {
|
||||||
this.audioResource = createAudioResource(src, {
|
this.audioResource = createAudioResource(src, {
|
||||||
inputType: ops?.type ?? StreamType.Arbitrary,
|
inputType: ops?.type ?? StreamType.Arbitrary,
|
||||||
metadata: ops?.data,
|
metadata: ops?.data,
|
||||||
inlineVolume: true // we definitely need volume controls, right?
|
// eslint-disable-next-line no-extra-boolean-cast
|
||||||
|
inlineVolume: !Boolean(ops?.disableVolume)
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.audioResource;
|
return this.audioResource;
|
||||||
|
@ -209,9 +224,8 @@ class StreamDispatcher extends EventEmitter<VoiceEvents> {
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
setVolume(value: number) {
|
setVolume(value: number) {
|
||||||
if (!this.audioResource || isNaN(value) || value < 0 || value > Infinity) return false;
|
if (!this.audioResource?.volume || isNaN(value) || value < 0 || value > Infinity) return false;
|
||||||
|
|
||||||
// ye boi logarithmic ✌
|
|
||||||
this.audioResource.volume.setVolumeLogarithmic(value / 100);
|
this.audioResource.volume.setVolumeLogarithmic(value / 100);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -221,7 +235,7 @@ class StreamDispatcher extends EventEmitter<VoiceEvents> {
|
||||||
* @type {number}
|
* @type {number}
|
||||||
*/
|
*/
|
||||||
get volume() {
|
get volume() {
|
||||||
if (!this.audioResource || !this.audioResource.volume) return 100;
|
if (!this.audioResource?.volume) return 100;
|
||||||
const currentVol = this.audioResource.volume.volume;
|
const currentVol = this.audioResource.volume.volume;
|
||||||
return Math.round(Math.pow(currentVol, 1 / 1.660964) * 100);
|
return Math.round(Math.pow(currentVol, 1 / 1.660964) * 100);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,11 @@ import { downloadOptions } from "ytdl-core";
|
||||||
|
|
||||||
export type FiltersName = keyof QueueFilters;
|
export type FiltersName = keyof QueueFilters;
|
||||||
|
|
||||||
|
export interface PlayerSearchResult {
|
||||||
|
playlist: Playlist | null;
|
||||||
|
tracks: Track[];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {AudioFilters} QueueFilters
|
* @typedef {AudioFilters} QueueFilters
|
||||||
*/
|
*/
|
||||||
|
@ -128,6 +133,9 @@ export interface PlayerProgressbarOptions {
|
||||||
* @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} [disableVolume=false] If player should disable inline volume
|
||||||
|
* @property {Function} [onBeforeCreateStream] Runs before creating stream
|
||||||
*/
|
*/
|
||||||
export interface PlayerOptions {
|
export interface PlayerOptions {
|
||||||
leaveOnEnd?: boolean;
|
leaveOnEnd?: boolean;
|
||||||
|
@ -138,6 +146,9 @@ export interface PlayerOptions {
|
||||||
ytdlOptions?: downloadOptions;
|
ytdlOptions?: downloadOptions;
|
||||||
initialVolume?: number;
|
initialVolume?: number;
|
||||||
bufferingTimeout?: number;
|
bufferingTimeout?: number;
|
||||||
|
spotifyBridge?: boolean;
|
||||||
|
disableVolume?: boolean;
|
||||||
|
onBeforeCreateStream?: (track: Track, source: TrackSource, queue: Queue) => Promise<Readable>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -218,25 +229,25 @@ export interface ExtractorModelData {
|
||||||
* - YOUTUBE_SEARCH
|
* - YOUTUBE_SEARCH
|
||||||
* - YOUTUBE_VIDEO
|
* - YOUTUBE_VIDEO
|
||||||
* - SOUNDCLOUD_SEARCH
|
* - SOUNDCLOUD_SEARCH
|
||||||
* @typedef {string} QueryType
|
* @typedef {number} QueryType
|
||||||
*/
|
*/
|
||||||
export enum QueryType {
|
export enum QueryType {
|
||||||
AUTO = "auto",
|
AUTO,
|
||||||
YOUTUBE = "youtube",
|
YOUTUBE,
|
||||||
YOUTUBE_PLAYLIST = "youtube_playlist",
|
YOUTUBE_PLAYLIST,
|
||||||
SOUNDCLOUD_TRACK = "soundcloud_track",
|
SOUNDCLOUD_TRACK,
|
||||||
SOUNDCLOUD_PLAYLIST = "soundcloud_playlist",
|
SOUNDCLOUD_PLAYLIST,
|
||||||
SOUNDCLOUD = "soundcloud",
|
SOUNDCLOUD,
|
||||||
SPOTIFY_SONG = "spotify_song",
|
SPOTIFY_SONG,
|
||||||
SPOTIFY_ALBUM = "spotify_album",
|
SPOTIFY_ALBUM,
|
||||||
SPOTIFY_PLAYLIST = "spotify_playlist",
|
SPOTIFY_PLAYLIST,
|
||||||
FACEBOOK = "facebook",
|
FACEBOOK,
|
||||||
VIMEO = "vimeo",
|
VIMEO,
|
||||||
ARBITRARY = "arbitrary",
|
ARBITRARY,
|
||||||
REVERBNATION = "reverbnation",
|
REVERBNATION,
|
||||||
YOUTUBE_SEARCH = "youtube_search",
|
YOUTUBE_SEARCH,
|
||||||
YOUTUBE_VIDEO = "youtube_video",
|
YOUTUBE_VIDEO,
|
||||||
SOUNDCLOUD_SEARCH = "soundcloud_search"
|
SOUNDCLOUD_SEARCH
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -348,12 +359,12 @@ export interface PlayOptions {
|
||||||
/**
|
/**
|
||||||
* @typedef {object} SearchOptions
|
* @typedef {object} SearchOptions
|
||||||
* @property {UserResolvable} requestedBy The user who requested this search
|
* @property {UserResolvable} requestedBy The user who requested this search
|
||||||
* @property {QueryType} [searchEngine=QueryType.AUTO] The query search engine
|
* @property {QueryType|string} [searchEngine=QueryType.AUTO] The query search engine, can be extractor name to target specific one (custom)
|
||||||
* @property {boolean} [blockExtractor=false] If it should block custom extractors
|
* @property {boolean} [blockExtractor=false] If it should block custom extractors
|
||||||
*/
|
*/
|
||||||
export interface SearchOptions {
|
export interface SearchOptions {
|
||||||
requestedBy: UserResolvable;
|
requestedBy: UserResolvable;
|
||||||
searchEngine?: QueryType;
|
searchEngine?: QueryType | string;
|
||||||
blockExtractor?: boolean;
|
blockExtractor?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ const bass = (g: number) => `bass=g=${g}:f=110:w=0.3`;
|
||||||
* @property {string} surrounding The surrounding filter
|
* @property {string} surrounding The surrounding filter
|
||||||
* @property {string} pulsator The pulsator filter
|
* @property {string} pulsator The pulsator filter
|
||||||
* @property {string} subboost The subboost filter
|
* @property {string} subboost The subboost filter
|
||||||
* @property {string} kakaoke The kakaoke filter
|
* @property {string} karaoke The kakaoke filter
|
||||||
* @property {string} flanger The flanger filter
|
* @property {string} flanger The flanger filter
|
||||||
* @property {string} gate The gate filter
|
* @property {string} gate The gate filter
|
||||||
* @property {string} haas The haas filter
|
* @property {string} haas The haas filter
|
||||||
|
|
|
@ -8,7 +8,9 @@
|
||||||
"strictNullChecks": false,
|
"strictNullChecks": false,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"pretty": true,
|
"pretty": true,
|
||||||
"skipLibCheck": true
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"importHelpers": true
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src/**/*"
|
"src/**/*"
|
||||||
|
|
Loading…
Reference in a new issue