TypeScript rewrite (#356)

Co-authored-by: Androz2091 <androz2091@gmail.com>
This commit is contained in:
Androz 2021-04-25 11:14:23 +02:00 committed by GitHub
commit 2f01dc96c2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
47 changed files with 7802 additions and 2419 deletions

View file

@ -1,22 +0,0 @@
module.exports = {
env: {
commonjs: true,
es6: true,
node: true
},
extends: [
'standard'
],
globals: {
Atomics: 'readonly',
SharedArrayBuffer: 'readonly'
},
parserOptions: {
ecmaVersion: 11
},
rules: {
indent: ['error', 4],
'no-async-promise-executor': 'off',
'no-unused-vars': 'off'
}
}

2
.gitattributes vendored
View file

@ -1,2 +0,0 @@
# Auto detect text files and perform LF normalization
* text=auto

32
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View file

@ -0,0 +1,32 @@
---
name: Bug report
about: Create a bug report to help us improve
title: "[BUG] "
labels: bug
assignees: ''
---
**Describe the bug**
<!-- A clear and concise description of what the bug is. -->
**To Reproduce**
Steps to reproduce the behavior:
<!-- 1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error -->
**Expected behavior**
<!-- A clear and concise description of what you expected to happen. -->
**Screenshots**
<!-- If applicable, add screenshots to help explain your problem. -->
**Please complete the following information:**
- Node Version: [x.x.x]
- Library Version: [x.x.x]
- Discord.js Version: [x.x.x]
**Additional context**
<!-- Add any other context about the problem here. -->

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View file

@ -0,0 +1,5 @@
blank_issues_enabled: true
contact_links:
- name: Discord Community Support
url: https://discord.gg/J4kK8ygxmW
about: Join our Discord server for further support

View file

@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: "[Feature Request] "
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
**Describe the solution you'd like**
<!-- A clear and concise description of what you want to happen. -->
**Describe alternatives you've considered**
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
**Additional context**
<!-- Add any other context or screenshots about the feature request here. -->

11
.github/ISSUE_TEMPLATE/question.md vendored Normal file
View file

@ -0,0 +1,11 @@
---
name: Question
about: Some questions related to this lib
title: "[QUESTION] "
labels: question
assignees: ''
---
**Question**
<!-- I have a question about ... -->

8
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View file

@ -0,0 +1,8 @@
## Changes
<!-- describe what changes this PR includes and explain why are they needed -->
## Status
- [ ] These changes have been tested and formatted properly.
- [ ] This PR includes only documentation changes, no code change.
- [ ] This PR introduces some Breaking changes.

27
.github/workflows/deploy.yml vendored Normal file
View file

@ -0,0 +1,27 @@
name: Deployment
on:
push:
branches:
- '*'
- '!docs'
- '!gh-pages'
jobs:
docs:
name: Documentation
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@master
- name: Install Node v14
uses: actions/setup-node@master
with:
node-version: 14
- name: Install dependencies
run: npm install
- name: Build and deploy documentation
uses: discordjs/action-docs@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View file

@ -14,6 +14,8 @@ jobs:
with:
node-version: 12
registry-url: https://registry.npmjs.org/
- run: npm run install
- run: npm run build
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{secrets.npm_token}}

21
.gitignore vendored
View file

@ -1,7 +1,14 @@
node_modules
poc.js
package-lock.json
yarn.lock
docs
.vscode
test
# Modules
node_modules/
# Test dir
test/
# Compiled
lib/
# error logs
yarn-error.log
# demo
demo/

View file

@ -1,31 +0,0 @@
{
"tags": {
"allowUnknownTags": true,
"dictionaries": ["jsdoc"]
},
"source": {
"include": ["index.js", "package.json", "README.md", "src"],
"includePattern": ".js$",
"excludePattern": "(node_modules/|docs)"
},
"plugins": [
"plugins/markdown"
],
"templates": {
"cleverLinks": false,
"monospaceLinks": true,
"useLongnameInNav": false,
"showInheritedInNav": true,
"default": {
"outputSourceFiles": false,
"includeDate": false
}
},
"opts": {
"destination": "./docs/",
"encoding": "utf8",
"private": true,
"recurse": true,
"template": "./node_modules/minami"
}
}

View file

@ -1,3 +1,6 @@
docs
node_modules/
poc.js
src/
tslint.json
tsconfig.json
.prettierrc
test/
demo/

6
.prettierrc Normal file
View file

@ -0,0 +1,6 @@
{
"printWidth": 120,
"trailingComma": "none",
"singleQuote": true,
"tabWidth": 4
}

128
CODE_OF_CONDUCT.md Normal file
View file

@ -0,0 +1,128 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
androz2091@gmail.com.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.

22
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,22 @@
# Hello
This document is for people who want to contribute to this project!
# Code Style
## Formatting
We are using **[Prettier](https://prettier.io)** to format the code.
## File names
- Always use `PascalCase` for the files containing classes (example: `Queue`, `Track`, `Player` etc.)
## Some Rules
- Use `camelCase` for `Function names`, `Variables`, etc. and `PascalCase` for `Class name`
- Do not make unused variables/imports
- Don't forget to write `JSDOC` for each properties and methods
- Use English language
# Pull Requests
- Use English language
- Explain what your update does
- Run `npm run docs:test` command to make sure documentation is working
- Format the code properly with `npm run format`

View file

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2020 Androz
Copyright (c) 2020-present Androz
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

198
README.md
View file

@ -1,32 +1,41 @@
# Discord Player
Complete framework to facilitate music commands using **[discord.js](https://discord.js.org)**.
[![downloadsBadge](https://img.shields.io/npm/dt/discord-player?style=for-the-badge)](https://npmjs.com/discord-player)
[![versionBadge](https://img.shields.io/npm/v/discord-player?style=for-the-badge)](https://npmjs.com/discord-player)
**Note**: this module uses recent discordjs features and requires discord.js version 12 and Node.js 14.
Discord Player is a powerful [Node.js](https://nodejs.org) module that allows you to easily implement music commands. **Everything** is customizable, and everything is done to simplify your work **without limiting you**! It doesn't require any api key, as it uses **scraping**.
## Installation
```sh
npm install --save discord-player
```
Install **@discordjs/opus**:
### Install **[discord-player](https://npmjs.com/package/discord-player)**
```sh
npm install --save @discordjs/opus
$ npm install --save discord-player
```
Install [FFMPEG](https://www.ffmpeg.org/download.html) and you're done!
### Install **[@discordjs/opus](https://npmjs.com/package/@discordjs/opus)**
## Features
```sh
$ npm install --save @discordjs/opus
```
🤘 Easy to use!
🎸 You can apply some cool filters (bassboost, reverse, 8D, etc...)
🎼 Manage your server queues with simple functions (add songs, skip the current song, pause the music, resume it, etc...)!
🌐 Multi-servers support
### Install FFmpeg or Avconv
- Official FFMPEG Website: **[https://www.ffmpeg.org/download.html](https://www.ffmpeg.org/download.html)**
- Node Module (FFMPEG): **[https://npmjs.com/package/ffmpeg-static](https://npmjs.com/package/ffmpeg-static)**
- Avconv: **[https://libav.org/download](https://libav.org/download)**
# Features
- Simple & easy to use 🤘
- Beginner friendly 😱
- Audio filters 🎸
- Lightweight 🛬
- Custom extractors support 🌌
- Lyrics 📃
- Multiple sources support ✌
- Play in multiple servers at the same time 🚗
## [Documentation](https://discord-player.js.org)
## Getting Started
@ -34,21 +43,24 @@ Here is the code you will need to get started with discord-player. Then, you wil
```js
const Discord = require("discord.js"),
client = new Discord.Client(),
client = new Discord.Client,
settings = {
prefix: "!",
token: "Your Discord Token"
};
const { Player } = require("discord-player");
// Create a new Player (you don't need any API Key)
const player = new Player(client);
// To easily access the player
client.player = player;
// add the trackStart event so when a song will be played this message will be sent
client.player.on('trackStart', (message, track) => message.channel.send(`Now playing ${track.title}...`))
client.on("ready", () => {
// add the trackStart event so when a song will be played this message will be sent
client.player.on("trackStart", (message, track) => message.channel.send(`Now playing ${track.title}...`))
client.once("ready", () => {
console.log("I'm ready !");
});
@ -58,8 +70,7 @@ client.on("message", async (message) => {
const command = args.shift().toLowerCase();
// !play Despacito
// will play "Despacito" in the member voice channel
// will play "Despacito" in the voice channel
if(command === "play"){
client.player.play(message, args[0]);
// as we registered the event above, no need to send a success message here
@ -70,116 +81,59 @@ client.on("message", async (message) => {
client.login(settings.token);
```
## [Documentation](https://discord-player.js.org)
## Supported websites
You will find many examples in the documentation to understand how the package works!
By default, discord-player supports **YouTube**, **Spotify** and **SoundCloud** streams only.
### Methods overview
### Optional dependencies
You need to **init the guild queue using the play() function**, then you are able to manage the queue and the music using the following functions. Click on a function name to get an example code and explanations.
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.
#### Play a track
#### [@discord-player/extractor](https://github.com/Snowflake107/discord-player-extractors) (optional)
* [play(message, query)](https://discord-player.js.org/Player.html#play) - play a track in a server
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).
#### Check if a track is being played
#### [@discord-player/downloader](https://github.com/DevSnowflake/discord-player-downloader) (optional)
* [isPlaying(message)](https://discord-player.js.org/Player.html#isPlaying) - check if there is a queue for a specific server
`@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).
#### Manage the queue
* [getQueue(message)](https://discord-player.js.org/Player.html#getQueue) - get the server queue
* [clearQueue(message)](https://discord-player.js.org/Player.html#clearQueue) - clear the server queue
* [remove(message, track)](https://discord-player.js.org/Player.html#remove) - remove a track from the server queue
* [shuffle(message)](https://discord-player.js.org/Player.html#shuffle) - shuffle the server queue
* [nowPlaying(message)](https://discord-player.js.org/Player.html#nowPlaying) - get the current track
#### Manage music stream
* [skip(message)](https://discord-player.js.org/Player.html#skip) - skip the current track
* [back(message)](https://discord-player.js.org/Player.html#back) - play the previous track
* [pause(message)](https://discord-player.js.org/Player.html#pause) - pause the current track
* [resume(message)](https://discord-player.js.org/Player.html#resume) - resume the current track
* [stop(message)](https://discord-player.js.org/Player.html#stop) - stop the current track
* [setFilters(message, newFilters)](https://discord-player.js.org/Player.html#setFilters) - update filters (bassboost for example)
* [setRepeatMode(message, boolean)](https://discord-player.js.org/Player.html#setRepeatMode) - enable or disable repeat mode for the server (play the song again and again)
* [setLoopMode(message, boolean)](https://discord-player.js.org/Player.html#setLoopMode) - enable or disable loop mode for the server (play the queue again and again)
* [seek(message, time)](https://discord-player.js.org/Player.html#seek) - seek to a specific position
* [moveTo(message, channel)](https://discord-player.js.org/Player.html#moveTo) - move the bot to another channel
### Utils
* [createProgressBar(message, options)](https://discord-player.js.org/Player.html#createProgressBar) - generate a progress bar for the current song/queue
### Event messages
```js
// Then add some messages that will be sent when the events will be triggered
client.player
// Send a message when a track starts
.on('trackStart', (message, track) => message.channel.send(`Now playing ${track.title}...`))
// Send a message when something is added to the queue
.on('trackAdd', (message, queue, track) => message.channel.send(`${track.title} has been added to the queue!`))
.on('playlistAdd', (message, queue, playlist) => message.channel.send(`${playlist.title} has been added to the queue (${playlist.tracks.length} songs)!`))
// Send messages to format search results
.on('searchResults', (message, query, tracks) => {
const embed = new Discord.MessageEmbed()
.setAuthor(`Here are your search results for ${query}!`)
.setDescription(tracks.map((t, i) => `${i}. ${t.title}`))
.setFooter('Send the number of the song you want to play!')
message.channel.send(embed);
})
.on('searchInvalidResponse', (message, query, tracks, content, collector) => {
if (content === 'cancel') {
collector.stop()
return message.channel.send('Search cancelled!')
}
message.channel.send(`You must send a valid number between 1 and ${tracks.length}!`)
})
.on('searchCancel', (message, query, tracks) => message.channel.send('You did not provide a valid response... Please send the command again!'))
.on('noResults', (message, query) => message.channel.send(`No results found on YouTube for ${query}!`))
// Send a message when the music is stopped
.on('queueEnd', (message, queue) => message.channel.send('Music stopped as there is no more music in the queue!'))
.on('channelEmpty', (message, queue) => message.channel.send('Music stopped as there is no more member in the voice channel!'))
.on('botDisconnect', (message) => message.channel.send('Music stopped as I have been disconnected from the channel!'))
// Error handling
.on('error', (error, message) => {
switch(error){
case 'NotPlaying':
message.channel.send('There is no music being played on this server!')
break;
case 'NotConnected':
message.channel.send('You are not connected in any voice channel!')
break;
case 'UnableToJoin':
message.channel.send('I am not able to join your voice channel, please check my permissions!')
break;
case 'LiveVideo':
message.channel.send('YouTube lives are not supported!')
break;
case 'VideoUnavailable':
message.channel.send('This YouTube video is not available!');
break;
default:
message.channel.send(`Something went wrong... Error: ${error}`)
}
})
```
## Examples of bots made with discord-player
## Examples of bots made with Discord Player
These bots are made by the community, they can help you build your own!
* [AtlantaBot](https://github.com/Androz2091/AtlantaBot) by [me](https://github.com/Androz2091)
* [AtlantaBot](https://github.com/Androz2091/AtlantaBot) by [Androz2091](https://github.com/Androz2091)
* [Discord-Music](https://github.com/inhydrox/discord-music) by [inhydrox](https://github.com/inhydrox)
* [Music-bot](https://github.com/ZerioDev/Music-bot) by [ZerioDev](https://github.com/ZerioDev)
## FAQ
### How to use cookies
```js
const player = new Player(client, {
ytdlDownloadOptions: {
requestOptions: {
headers: {
cookie: "YOUR_YOUTUBE_COOKIE"
}
}
}
});
```
### How to use custom proxies
```js
const HttpsProxyAgent = require("https-proxy-agent");
// Remove "user:pass@" if you don't need to authenticate to your proxy.
const proxy = "http://user:pass@111.111.111.111:8080";
const agent = HttpsProxyAgent(proxy);
const player = new Player(client, {
ytdlDownloadOptions: {
requestOptions: { agent }
}
});
```

View file

@ -0,0 +1,89 @@
# Discord Player Extractor API
The Extractor API allows you to build your own stream extractor for **Discord Player**.
# Example Extractor
Your extractor should have 2 methods (required):
- `validate(query): boolean`
This method is called by Discord Player while validating the query provided via `Player.play()`. (Note that only `string` queries are passed to your extractor)
- `getInfo(query): object`
This method is used by Discord Player to create `Track` object. You can return your data here that gets passed to `Track`.
Your info must be similar to this:
```js
{
// the title
title: "Extracted by custom extractor",
// the duration in ms
duration: 20000,
// the thumbnail
thumbnail: "some thumbnail link",
// engine, can be Readable streams or link to raw stream that gets played
engine: "someStreamLink",
// number of views
views: 0,
// author of this stream
author: "Some Artist",
// description
description: "",
// link of this stream
url: "Some Link"
}
```
- `important: boolean`
You can mark your Extractor as `important` by adding `important: true` to your extractor object. Doing this will disable rest of the extractors that comes after your extractor and use your extractor to get data. By default, it is set to `false`.
- `version: string`
This should be the version of your extractor. It is not really important and is set to `0.0.0` by default.
# Loading Extractors
Discord Player Extractors can be loaded using `Player.use(ExtractorName, Extractor)` method.
## Register Extractor
```js
const myExtractor = {
version: "1.0.0",
important: false,
validate: (query) => true,
getInfo: async (query) => {
return {
title: "Extracted by custom extractor",
duration: 20000,
thumbnail: "some thumbnail link",
engine: "someStreamLink",
views: 0,
author: "Some Artist",
description: "",
url: "Some Link"
};
}
};
player.use("GiveItSomeName", myExtractor);
```
## Remove Extractor
```js
player.unuse("GiveItSomeName");
```
# Readymade Extractors
## **[@discord-player/extractor](https://github.com/Snowflake107/discord-player-extractors)**
This extractor enables optional sources such as `Discord Attachments`, `Vimeo`, `Facebook` and `Reverbnation`. It also enables the `Lyrics` feature!
## **[@discord-player/downloader](https://github.com/DevSnowflake/discord-player-downloader)**
This extractor is based on **[YouTube DL](https://youtube-dl.org)**. This extractor enables `700+ websites` support. However, this extractor can get buggy and is not updated frequently. So, it is suggested to make your own extractor if you want to use it!
```js
const downloader = require("@discord-player/downloader").Downloader;
player.use("YOUTUBE_DL", downloader);
```
> Discord Player auto-detects and uses `@discord-player/extractor` if it is installed!

139
docs/general/welcome.md Normal file
View file

@ -0,0 +1,139 @@
# Discord Player
Complete framework to facilitate music commands using **[discord.js](https://discord.js.org)**.
[![downloadsBadge](https://img.shields.io/npm/dt/discord-player?style=for-the-badge)](https://npmjs.com/discord-player)
[![versionBadge](https://img.shields.io/npm/v/discord-player?style=for-the-badge)](https://npmjs.com/discord-player)
## Installation
### Install **[discord-player](https://npmjs.com/package/discord-player)**
```sh
$ npm install --save discord-player
```
### Install **[@discordjs/opus](https://npmjs.com/package/@discordjs/opus)**
```sh
$ npm install --save @discordjs/opus
```
### Install FFmpeg or Avconv
- Official FFMPEG Website: **[https://www.ffmpeg.org/download.html](https://www.ffmpeg.org/download.html)**
- Node Module (FFMPEG): **[https://npmjs.com/package/ffmpeg-static](https://npmjs.com/package/ffmpeg-static)**
- Avconv: **[https://libav.org/download](https://libav.org/download)**
# Features
- Simple & easy to use 🤘
- Beginner friendly 😱
- Audio filters 🎸
- Lightweight 🛬
- Custom extractors support 🌌
- Lyrics 📃
- Multiple sources support ✌
- Play in multiple servers at the same time 🚗
## [Documentation](https://discord-player.js.org)
## Getting Started
Here is the code you will need to get started with discord-player. Then, you will be able to use `client.player` everywhere in your code!
```js
const Discord = require("discord.js"),
client = new Discord.Client,
settings = {
prefix: "!",
token: "Your Discord Token"
};
const { Player } = require("discord-player");
// Create a new Player (you don't need any API Key)
const player = new Player(client);
// To easily access the player
client.player = player;
// add the trackStart event so when a song will be played this message will be sent
client.player.on("trackStart", (message, track) => message.channel.send(`Now playing ${track.title}...`))
client.once("ready", () => {
console.log("I'm ready !");
});
client.on("message", async (message) => {
const args = message.content.slice(settings.prefix.length).trim().split(/ +/g);
const command = args.shift().toLowerCase();
// !play Despacito
// will play the song "Despacito" in the voice channel
if(command === "play"){
client.player.play(message, args[0]);
// as we registered the event above, no need to send a success message here
}
});
client.login(settings.token);
```
## Supported websites
By default, discord-player supports **YouTube**, **Spotify** and **SoundCloud** streams only.
### Optional dependencies
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)
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` is an optional package that brings support for +700 websites. The documentation is available [here](https://github.com/DevSnowflake/discord-player-downloader).
## Examples of bots made with Discord Player
These bots are made by the community, they can help you build your own!
* [AtlantaBot](https://github.com/Androz2091/AtlantaBot) by [Androz2091](https://github.com/Androz2091)
* [Discord-Music](https://github.com/inhydrox/discord-music) by [inhydrox](https://github.com/inhydrox)
* [Music-bot](https://github.com/ZerioDev/Music-bot) by [ZerioDev](https://github.com/ZerioDev)
## FAQ
### How to use cookies
```js
const player = new Player(client, {
ytdlDownloadOptions: {
requestOptions: {
headers: {
cookie: "YOUR_YOUTUBE_COOKIE"
}
}
}
});
```
### How to use custom proxies
```js
const HttpsProxyAgent = require("https-proxy-agent");
// Remove "user:pass@" if you don't need to authenticate to your proxy.
const proxy = "http://user:pass@111.111.111.111:8080";
const agent = HttpsProxyAgent(proxy);
const player = new Player(client, {
ytdlDownloadOptions: {
requestOptions: { agent }
}
});
```

14
docs/index.yml Normal file
View file

@ -0,0 +1,14 @@
- name: General
files:
- name: Welcome
path: welcome.md
- name: Extractors
files:
- name: Extractors API
path: extractor.md
- name: YouTube
files:
- name: Using Cookies
path: cookies.md
- name: Using Proxy
path: proxy.md

18
docs/youtube/cookies.md Normal file
View file

@ -0,0 +1,18 @@
# Using Cookies to avoid 429
```js
const { Player } = require("discord-player");
const player = new Player(client, {
ytdlDownloadOptions: {
requestOptions: {
headers: {
cookie: "YOUR_YOUTUBE_COOKIE"
}
}
}
});
```
> Keep in mind that using `cookies` after getting `429` **does not fix the problem**.
> You should use `cookies` before getting `429` which helps to **_reduce_** `Error: Status Code 429`

16
docs/youtube/proxy.md Normal file
View file

@ -0,0 +1,16 @@
# Using Proxy to avoid 429
```js
const { Player } = require("discord-player");
const HttpsProxyAgent = require("https-proxy-agent");
// Remove "user:pass@" if you don't need to authenticate to your proxy.
const proxy = "http://user:pass@111.111.111.111:8080";
const agent = HttpsProxyAgent(proxy);
const player = new Player(client, {
ytdlDownloadOptions: {
requestOptions: { agent }
}
});
```

View file

@ -1,10 +0,0 @@
process.env.YTDL_NO_UPDATE = true
module.exports = {
Extractors: require('./src/Extractors/Extractor'),
Player: require('./src/Player'),
Queue: require('./src/Queue'),
Track: require('./src/Track'),
Util: require('./src/Util'),
version: require('./package.json').version
}

17
jsdoc.json Normal file
View file

@ -0,0 +1,17 @@
{
"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"
]
}
}

View file

@ -1,14 +1,24 @@
{
"name": "discord-player",
"version": "3.4.0",
"description": "Complete framework to facilitate music commands using discord.js v12",
"main": "index.js",
"types": "typings/index.d.ts",
"funding": "https://github.com/Androz2091/discord-player?sponsor=1",
"version": "4.0.0",
"description": "Complete framework to facilitate music commands using discord.js",
"main": "lib/src/index.js",
"types": "lib/src/index.d.ts",
"files": [
"lib/"
],
"scripts": {
"test": "cd test && node index.js",
"generate-docs": "node_modules/.bin/jsdoc --configure .jsdoc.json --verbose"
"test": "yarn build && cd test && node index.js",
"build": "tsc",
"format": "prettier --write \"src/**/*.ts\"",
"lint": "tslint -p tsconfig.json",
"docs": "docgen --jsdoc jsdoc.json --verbose --source src/*.ts src/**/*.ts --custom docs/index.yml --output docs/docs.json",
"docs:test": "docgen --jsdoc jsdoc.json --verbose --source src/*.ts src/**/*.ts --custom docs/index.yml"
},
"funding": "https://github.com/Androz2091/discord-player?sponsor=1",
"contributors": [
"Snowflake107"
],
"repository": {
"type": "git",
"url": "git+https://github.com/Androz2091/discord-player.git"
@ -21,7 +31,17 @@
"discord",
"volume",
"queue",
"youtube"
"youtube",
"discord.js",
"musicbot",
"discord-music-player",
"discord-music",
"music-player",
"youtube-dl",
"ytdl-core",
"ytdl",
"lavalink",
"api"
],
"author": "Androz2091",
"license": "MIT",
@ -30,29 +50,27 @@
},
"homepage": "https://github.com/Androz2091/discord-player#readme",
"dependencies": {
"chalk": "^4.1.0",
"discord-ytdl-core": "^5.0.2",
"jsdom": "^16.4.0",
"merge-options": "^3.0.4",
"node-fetch": "^2.6.0",
"parse-ms": "^2.1.0",
"reverbnation-scraper": "^2.0.0",
"soundcloud-scraper": "^4.0.3",
"spotify-url-info": "^2.2.0",
"youtube-sr": "^4.0.4",
"ytdl-core": "^4.5.0"
},
"devDependencies": {
"@discordjs/opus": "^0.4.0",
"@types/node": "14.14.31",
"discord.js": "^12.2.0",
"eslint": "^7.20.0",
"eslint-config-standard": "^16.0.2",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.3.1",
"eslint-plugin-standard": "^5.0.0",
"jsdoc": "^3.6.3",
"jsdoc-skyceil": "Androz2091/jsdoc-skyceil"
"@babel/cli": "^7.13.16",
"@babel/core": "^7.13.16",
"@babel/preset-env": "^7.13.15",
"@babel/preset-typescript": "^7.13.0",
"@discord-player/extractor": "^2.0.0",
"@discordjs/opus": "^0.5.0",
"@types/node": "^14.14.41",
"@types/ws": "^7.4.1",
"discord.js": "^12.5.3",
"discord.js-docgen": "discordjs/docgen#ts-patch",
"jsdoc-babel": "^0.5.0",
"prettier": "^2.2.1",
"tslint": "^6.1.3",
"tslint-config-prettier": "^1.18.0",
"typescript": "^4.2.3"
}
}

View file

@ -1,47 +0,0 @@
const https = require('https')
const { Readable } = require('stream')
class Discord {
constructor () {
throw new Error(`The ${this.constructor.name} class may not be instantiated!`)
}
/**
* @typedef {Readable} Readable
*/
/**
* Downloads discord attachment
* @param {string} url Discord attachment url
* @returns {Promise<Readable>}
*/
static async download (url) {
const data = await Discord.getInfo(url)
return data.stream
}
/**
* Returns discord attachment info
* @param {string} url Attachment url
*/
static getInfo (url) {
return new Promise((resolve) => {
https.get(url, res => {
const data = {
title: res.req.path.split('/').pop(),
format: res.headers['content-type'],
size: !isNaN(res.headers['content-length']) ? Math.round((parseInt(res.headers['content-length']) / (1024 * 1024)) * 100) / 100 : 0,
sizeFormat: 'MB'
}
Object.defineProperty(data, 'stream', {
get: () => res
})
resolve(data)
})
})
}
}
module.exports = Discord

View file

@ -1,6 +0,0 @@
module.exports = {
DiscordExtractor: require('./Discord'),
FacebookExtractor: require('./Facebook'),
ReverbnationExtractor: require('reverbnation-scraper'),
VimeoExtractor: require('./Vimeo')
}

View file

@ -1,148 +0,0 @@
const fetch = require('node-fetch').default
const { JSDOM } = require('jsdom')
const { Readable } = require('stream')
class Facebook {
constructor () {
throw new Error(`The ${this.constructor.name} class may not be instantiated!`)
}
/**
* Validates facebook url
* @param {string} url URL to validate
*/
static validateURL (url) {
const REGEX = /(https?:\/\/)(www\.|m\.)?(facebook|fb).com\/.*\/videos\/.*/
if (!url || typeof url !== 'string') return false
return REGEX.test(url)
}
/**
* @typedef {Readable} Readable
*/
/**
* Downloads facebook video
* @param {string} url Video url to download
* @returns {Promise<Readable>}
*/
static download (url) {
return new Promise(async (resolve, reject) => {
if (!Facebook.validateURL(url)) reject(new Error('Invalid url.'))
const info = await Facebook.getInfo(url)
if (!info || !info.streamURL) return reject(new Error('video not found'))
const link = info.streamURL
let req = require('https')
if (link.startsWith('http://')) req = require('http')
req.get(link, res => {
resolve(res)
})
})
}
/**
* Fetches facebook video info
* @param {string} url Facebook video url
*/
static async getInfo (url) {
if (!Facebook.validateURL(url)) throw new Error('Invalid url.')
try {
const html = await Facebook._parseHTML(url)
const document = new JSDOM(html).window.document
const rawdata = document.querySelector('script[type="application/ld+json"]').innerHTML
const json = JSON.parse(rawdata)
const obj = {
name: json.name,
title: document.querySelector('meta[property="og:title"]').attributes.item(1).value,
description: json.description,
rawVideo: json.contentUrl,
thumbnail: json.thumbnailUrl,
uploadedAt: new Date(json.uploadDate),
duration: Facebook.parseTime(json.duration),
interactionCount: json.interactionCount,
streamURL: json.url,
publishedAt: new Date(json.datePublished),
width: json.width,
height: json.height,
live: !!json.publication[0].isLiveBroadcast,
nsfw: !json.isFamilyFriendly,
genre: json.genre,
keywords: json.keywords ? json.keywords.split(', ') : [],
comments: json.commentCount,
size: json.contentSize,
quality: json.videoQuality,
author: {
type: json.author['@type'],
name: json.author.name,
url: json.author.url
},
publisher: {
type: json.publisher['@type'],
name: json.publisher.name,
url: json.publisher.url,
avatar: json.publisher.logo.url
},
url: html.split('",page_uri:"')[1].split('",')[0],
shares: html.split(',share_count:{')[1].split('},')[0].split(':')[1],
views: html.split(',video_view_count:')[1].split(',')[0]
}
return obj
} catch {
return null
}
}
/**
* Parses time in ms
* @param {string} duration Raw duration to parse
* @returns {string}
*/
static parseTime (duration) {
if (typeof duration !== 'string') return duration
let a = duration.match(/\d+/g)
if (duration.indexOf('M') >= 0 && duration.indexOf('H') === -1 && duration.indexOf('S') === -1) {
a = [0, a[0], 0]
}
if (duration.indexOf('H') >= 0 && duration.indexOf('M') === -1) {
a = [a[0], 0, a[1]]
}
if (duration.indexOf('H') >= 0 && duration.indexOf('M') === -1 && duration.indexOf('S') === -1) {
a = [a[0], 0, 0]
}
duration = 0
if (a.length === 3) {
duration = duration + parseInt(a[0]) * 3600
duration = duration + parseInt(a[1]) * 60
duration = duration + parseInt(a[2])
}
if (a.length === 2) {
duration = duration + parseInt(a[0]) * 60
duration = duration + parseInt(a[1])
}
if (a.length === 1) {
duration = duration + parseInt(a[0])
}
return duration
}
/**
* @ignore
* @param {string} url website url to parse html
*/
static async _parseHTML (url) {
const res = await fetch(url.replace('/m.', '/'))
return await res.text()
}
}
module.exports = Facebook

View file

@ -1,69 +0,0 @@
const fetch = require('node-fetch').default
const { Readable } = require('stream')
class Vimeo {
constructor () {
throw new Error(`The ${this.constructor.name} class may not be instantiated!`)
}
/**
* @typedef {Readable} Readable
*/
/**
* Downloads from vimeo
* @param {number} id Vimeo video id
* @returns {Promise<Readable>}
*/
static download (id) {
return new Promise(async (resolve) => {
const info = await Vimeo.getInfo(id)
if (!info) return null
const downloader = info.stream.url.startsWith('https://') ? require('https') : require('http')
downloader.get(info.stream.url, res => {
resolve(res)
})
})
}
/**
* Returns video info
* @param {number} id Video id
*/
static async getInfo (id) {
if (!id) throw new Error('Invalid id')
const url = `https://player.vimeo.com/video/${id}`
try {
const res = await fetch(url)
const data = await res.text()
const json = JSON.parse(data.split('<script> (function(document, player) { var config = ')[1].split(';')[0])
const obj = {
id: json.video.id,
duration: json.video.duration,
title: json.video.title,
url: json.video.url,
thumbnail: json.video.thumbs['1280'] || json.video.thumbs.base,
width: json.video.width,
height: json.video.height,
stream: json.request.files.progressive[0],
author: {
accountType: json.video.owner.account_type,
id: json.video.owner.id,
name: json.video.owner.name,
url: json.video.owner.url,
avatar: json.video.owner.img_2x || json.video.owner.img
}
}
return obj
} catch {
return null
}
}
}
module.exports = Vimeo

File diff suppressed because it is too large Load diff

1466
src/Player.ts Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,112 +0,0 @@
const Discord = require('discord.js')
const { EventEmitter } = require('events')
const Track = require('./Track')
const Player = require('./Player')
const { Stream } = require('stream')
/**
* Represents a guild queue.
*/
class Queue extends EventEmitter {
/**
* @param {Discord.Snowflake} guildID ID of the guild this queue is for.
* @param {Discord.Message} message Message that initialized the queue
* @param {Filters} filters Filters the queue should be initialized with.
*/
constructor (guildID, message, filters) {
super()
/**
* ID of the guild this queue is for.
* @type {Discord.Snowflake}
*/
this.guildID = guildID
/**
* The voice connection of this queue.
* @type {Discord.VoiceConnection}
*/
this.voiceConnection = null
/**
* The ytdl stream.
* @type {any}
*/
this.stream = null
/**
* The tracks of this queue. The first one is currenlty playing and the others are going to be played.
* @type {Track[]}
*/
this.tracks = []
/**
* The previous tracks in this queue.
* @type {Track[]}
*/
this.previousTracks = []
/**
* Whether the stream is currently stopped.
* @type {boolean}
*/
this.stopped = false
/**
* Whether the last track was skipped.
* @type {boolean}
*/
this.lastSkipped = false
/**
* The stream volume of this queue. (0-100)
* @type {number}
*/
this.volume = 100
/**
* Whether the stream is currently paused.
* @type {boolean}
*/
this.paused = this.voiceConnection && this.voiceConnection.dispatcher && this.voiceConnection.dispatcher.paused
/**
* Whether the repeat mode is enabled.
* @type {boolean}
*/
this.repeatMode = false
/**
* Whether the loop mode is enabled.
* @type {boolean}
*/
this.loopMode = false
/**
* Filters status
* @type {Filters}
*/
this.filters = {}
Object.keys(filters).forEach((f) => {
this.filters[f] = false
})
/**
* Additional stream time
* @type {Number}
*/
this.additionalStreamTime = 0
/**
* Message that initialized the queue
* @type {Discord.Message}
*/
this.firstMessage = message
}
get playing () {
return this.tracks[0]
}
get calculatedVolume () {
return this.filters.bassboost ? this.volume + 50 : this.volume
}
get totalTime () {
return this.tracks.length > 0 ? this.tracks.map((t) => t.durationMS).reduce((p, c) => p + c) : 0
}
get currentStreamTime () {
return this.voiceConnection.dispatcher
? this.voiceConnection.dispatcher.streamTime + this.additionalStreamTime
: 0
}
}
module.exports = Queue

View file

@ -0,0 +1,70 @@
import { ExtractorModelData } from '../types/types';
class ExtractorModel {
name: string;
private _raw: any;
/**
* Model for raw Discord Player extractors
* @param {String} extractorName Name of the extractor
* @param {Object} data Extractor object
*/
constructor(extractorName: string, data: any) {
/**
* The extractor name
* @type {String}
*/
this.name = extractorName;
Object.defineProperty(this, '_raw', { value: data, configurable: false, writable: false, enumerable: false });
}
/**
* Method to handle requests from `Player.play()`
* @param {String} query Query to handle
* @returns {Promise<ExtractorModelData>}
*/
async handle(query: string): Promise<ExtractorModelData> {
const data = await this._raw.getInfo(query);
if (!data) return null;
return {
title: data.title,
duration: data.duration,
thumbnail: data.thumbnail,
engine: data.engine,
views: data.views,
author: data.author,
description: data.description,
url: data.url
};
}
/**
* Method used by Discord Player to validate query with this extractor
* @param {String} query The query to validate
* @returns {Boolean}
*/
validate(query: string): boolean {
return Boolean(this._raw.validate(query));
}
/**
* The extractor version
* @type {String}
*/
get version(): string {
return this._raw.version ?? '0.0.0';
}
/**
* If player should mark this extractor as important
* @type {Boolean}
*/
get important(): boolean {
return Boolean(this._raw.important);
}
}
export default ExtractorModel;
export { ExtractorModel };

204
src/Structures/Queue.ts Normal file
View file

@ -0,0 +1,204 @@
import { Message, Snowflake, VoiceConnection } from 'discord.js';
import AudioFilters from '../utils/AudioFilters';
import { Player } from '../Player';
import { EventEmitter } from 'events';
import { Track } from './Track';
import { QueueFilters } from '../types/types';
export class Queue extends EventEmitter {
public player!: Player;
public guildID: Snowflake;
public voiceConnection?: VoiceConnection;
public stream?: any;
public tracks: Track[];
public previousTracks: Track[];
public stopped: boolean;
public lastSkipped: boolean;
public volume: number;
public paused: boolean;
public repeatMode: boolean;
public loopMode: boolean;
public filters: QueueFilters;
public additionalStreamTime: number;
public firstMessage: Message;
/**
* If autoplay is enabled in this queue
* @type {boolean}
*/
public autoPlay = false;
/**
* Queue constructor
* @param {Player} player The player that instantiated this Queue
* @param {DiscordMessage} message The message object
*/
constructor(player: Player, message: Message) {
super();
/**
* The player that instantiated this Queue
* @name Queue#player
* @type {Player}
* @readonly
*/
Object.defineProperty(this, 'player', { value: player, enumerable: false });
/**
* ID of the guild assigned to this queue
* @type {DiscordSnowflake}
*/
this.guildID = message.guild.id;
/**
* The voice connection of this queue
* @type {DiscordVoiceConnection}
*/
this.voiceConnection = null;
/**
* Tracks of this queue
* @type {Track[]}
*/
this.tracks = [];
/**
* Previous tracks of this queue
* @type {Track[]}
*/
this.previousTracks = [];
/**
* If the player of this queue is stopped
* @type {boolean}
*/
this.stopped = false;
/**
* If last track was skipped
* @type {boolean}
*/
this.lastSkipped = false;
/**
* Queue volume
* @type {Number}
*/
this.volume = 100;
/**
* If the player of this queue is paused
* @type {boolean}
*/
this.paused = Boolean(this.voiceConnection?.dispatcher?.paused);
/**
* If repeat mode is enabled in this queue
* @type {boolean}
*/
this.repeatMode = false;
/**
* If loop mode is enabled in this queue
* @type {boolean}
*/
this.loopMode = false;
/**
* The additional calculated stream time
* @type {Number}
*/
this.additionalStreamTime = 0;
/**
* The initial message object
* @type {DiscordMessage}
*/
this.firstMessage = message;
/**
* The audio filters in this queue
* @type {QueueFilters}
*/
this.filters = {};
Object.keys(AudioFilters).forEach((fn) => {
this.filters[fn as keyof QueueFilters] = false;
});
}
/**
* Currently playing track
* @type {Track}
*/
get playing(): Track {
return this.tracks[0];
}
/**
* Calculated volume of this queue
* @type {Number}
*/
get calculatedVolume(): number {
return this.filters.normalizer ? this.volume + 70 : this.volume;
}
/**
* Total duration
* @type {Number}
*/
get totalTime(): number {
return this.tracks.length > 0 ? this.tracks.map((t) => t.durationMS).reduce((p, c) => p + c) : 0;
}
/**
* Current stream time
* @type {Number}
*/
get currentStreamTime(): number {
return this.voiceConnection?.dispatcher?.streamTime + this.additionalStreamTime || 0;
}
/**
* Sets audio filters in this player
* @param {QueueFilters} filters Audio filters to set
* @type {Promise<void>}
*/
setFilters(filters: QueueFilters): Promise<void> {
return this.player.setFilters(this.firstMessage, filters);
}
/**
* Returns array of all enabled filters
* @type {String[]}
*/
getFiltersEnabled(): string[] {
const filters: string[] = [];
for (const filter in this.filters) {
if (this.filters[filter as keyof QueueFilters] !== false) filters.push(filter);
}
return filters;
}
/**
* Returns all disabled filters
* @type {String[]}
*/
getFiltersDisabled(): string[] {
const enabled = this.getFiltersEnabled();
return Object.keys(this.filters).filter((f) => !enabled.includes(f));
}
/**
* String representation of this Queue
* @returns {String}
*/
toString(): string {
return `<Queue ${this.guildID}>`;
}
}
export default Queue;

146
src/Structures/Track.ts Normal file
View file

@ -0,0 +1,146 @@
import { Player } from '../Player';
import { User } from 'discord.js';
import { TrackData } from '../types/types';
import Queue from './Queue';
export class Track {
public player!: Player;
public title!: string;
public description!: string;
public author!: string;
public url!: string;
public thumbnail!: string;
public duration!: string;
public views!: number;
public requestedBy!: User;
public fromPlaylist!: boolean;
public raw!: TrackData;
/**
* Track constructor
* @param {Player} player The player that instantiated this Track
* @param {TrackData} data Track data
*/
constructor(player: Player, data: TrackData) {
/**
* The player that instantiated this Track
* @name Track#player
* @type {Player}
* @readonly
*/
Object.defineProperty(this, 'player', { value: player, enumerable: false });
/**
* Title of this track
* @name Track#title
* @type {String}
*/
/**
* Description of this track
* @name Track#description
* @type {String}
*/
/**
* Author of this track
* @name Track#author
* @type {String}
*/
/**
* URL of this track
* @name Track#url
* @type {String}
*/
/**
* Thumbnail of this track
* @name Track#thumbnail
* @type {String}
*/
/**
* Duration of this track
* @name Track#duration
* @type {String}
*/
/**
* Views count of this track
* @name Track#views
* @type {Number}
*/
/**
* Person who requested this track
* @name Track#requestedBy
* @type {DiscordUser}
*/
/**
* If this track belongs to playlist
* @name Track#fromPlaylist
* @type {Boolean}
*/
/**
* Raw track data
* @name Track#raw
* @type {TrackData}
*/
void this._patch(data);
}
private _patch(data: TrackData) {
this.title = data.title ?? '';
this.description = data.description ?? '';
this.author = data.author ?? '';
this.url = data.url ?? '';
this.thumbnail = data.thumbnail ?? '';
this.duration = data.duration ?? '';
this.views = data.views ?? 0;
this.requestedBy = data.requestedBy;
this.fromPlaylist = Boolean(data.fromPlaylist);
// raw
Object.defineProperty(this, 'raw', { get: () => data, enumerable: false });
}
/**
* The queue in which this track is located
* @type {Queue}
*/
get queue(): Queue {
return this.player.queues.find((q) => q.tracks.includes(this));
}
/**
* The track duration in millisecond
* @type {Number}
*/
get durationMS(): number {
const times = (n: number, t: number) => {
let tn = 1;
for (let i = 0; i < t; i++) tn *= n;
return t <= 0 ? 1000 : tn * 1000;
};
return this.duration
.split(':')
.reverse()
.map((m, i) => parseInt(m) * times(60, i))
.reduce((a, c) => a + c, 0);
}
/**
* String representation of this track
* @returns {String}
*/
toString(): string {
return `${this.title} by ${this.author}`;
}
}
export default Track;

View file

@ -1,94 +0,0 @@
const Discord = require('discord.js')
const Queue = require('./Queue')
const Player = require('./Player')
/**
* Represents a track.
*/
class Track {
/**
* @param {Object} videoData The video data for this track
* @param {Discord.User | null} user The user who requested the track
* @param {Player} player
*/
constructor (videoData, user, player, fromPlaylist = false) {
/**
* The player instantiating the track
* @type {Player}
*/
this.player = player
/**
* The track title
* @type {string}
*/
this.title = videoData.title
/**
* The Youtube URL of the track
* @type {string}
*/
this.url = videoData.url
/**
* The video duration (formatted).
* @type {string}
*/
this.duration = videoData.durationFormatted ||
`${Math.floor(parseInt(videoData.lengthSeconds) / 60)}:${parseInt(videoData.lengthSeconds) % 60}`
/**
* The video description
* @type {string}
*/
this.description = videoData.description
/**
* The video thumbnail
* @type {string}
*/
this.thumbnail = videoData.thumbnail && typeof videoData.thumbnail === 'object'
? videoData.thumbnail.url
: videoData.thumbnail
/**
* The video views
* @type {?number}
*/
this.views = parseInt(videoData.views)
/**
* The video channel
* @type {string}
*/
this.author = videoData.channel
? videoData.channel.name
: videoData.author.name
/**
* The user who requested the track
* @type {Discord.User?}
*/
this.requestedBy = user
/**
* Whether the track was added from a playlist
* @type {boolean}
*/
this.fromPlaylist = fromPlaylist
}
/**
* The queue in which the track is
* @type {Queue}
*/
get queue () {
return this.player.queues.find((queue) => queue.tracks.includes(this))
}
/**
* The track duration
* @type {number}
*/
get durationMS () {
const args = this.duration.split(':')
switch (args.length) {
case 3: return parseInt(args[0]) * 60 * 60 * 1000 + parseInt(args[1]) * 60 * 1000 + parseInt(args[2]) * 1000
case 2: return parseInt(args[0]) * 60 * 1000 + parseInt(args[1]) * 1000
default: return parseInt(args[0]) * 1000
}
}
}
module.exports = Track

View file

@ -1,91 +0,0 @@
const ytsr = require('youtube-sr').default
const soundcloud = require('soundcloud-scraper')
const chalk = require('chalk')
const spotifySongRegex = (/https?:\/\/(?:embed\.|open\.)(?:spotify\.com\/)(?:track\/|\?uri=spotify:track:)((\w|-){22})/)
const spotifyPlaylistRegex = (/https?:\/\/(?:embed\.|open\.)(?:spotify\.com\/)(?:playlist\/|\?uri=spotify:playlist:)((\w|-){22})/)
const spotifyAlbumRegex = (/https?:\/\/(?:embed\.|open\.)(?:spotify\.com\/)(?:album\/|\?uri=spotify:album:)((\w|-){22})/)
const vimeoRegex = (/(http|https)?:\/\/(www\.|player\.)?vimeo\.com\/(?:channels\/(?:\w+\/)?|groups\/([^/]*)\/videos\/|video\/|)(\d+)(?:|\/\?)/)
const facebookRegex = (/(https?:\/\/)(www\.|m\.)?(facebook|fb).com\/.*\/videos\/.*/)
module.exports = class Util {
constructor () {
throw new Error(`The ${this.constructor.name} class may not be instantiated.`)
}
static checkFFMPEG () {
try {
const prism = require('prism-media')
prism.FFmpeg.getInfo()
return true
} catch {
Util.alertFFMPEG()
return false
}
}
static alertFFMPEG () {
console.log(chalk.red('ERROR:'), 'FFMPEG is not installed. Install with "npm install ffmpeg-static" or download it here: https://ffmpeg.org/download.html.')
}
static isVoiceEmpty (channel) {
return channel.members.filter((member) => !member.user.bot).size === 0
}
static isSoundcloudLink (query) {
return soundcloud.validateURL(query)
}
static isSpotifyLink (query) {
return spotifySongRegex.test(query)
}
static isSpotifyPLLink (query) {
return spotifyPlaylistRegex.test(query)
}
static isSpotifyAlbumLink (query) {
return spotifyAlbumRegex.test(query)
}
static isYTPlaylistLink (query) {
return ytsr.validate(query, 'PLAYLIST_ID')
}
static isYTVideoLink (query) {
return ytsr.validate(query, 'VIDEO')
}
static isSoundcloudPlaylist (query) {
return Util.isSoundcloudLink(query) && query.includes('/sets/')
}
static isVimeoLink (query) {
return vimeoRegex.test(query)
}
static getVimeoID (query) {
return Util.isVimeoLink(query) ? query.split('/').filter(x => !!x).pop() : null
}
static isFacebookLink (query) {
return facebookRegex.test(query)
}
static isReverbnationLink (query) {
return /https:\/\/(www.)?reverbnation.com\/(.+)\/song\/(.+)/.test(query)
}
static isDiscordAttachment (query) {
return /https:\/\/cdn.discordapp.com\/attachments\/(\d{17,19})\/(\d{17,19})\/(.+)/.test(query)
}
static buildTimecode (data) {
const items = Object.keys(data)
const required = ['days', 'hours', 'minutes', 'seconds']
const parsed = items.filter(x => required.includes(x)).map(m => data[m] > 0 ? data[m] : '')
const final = parsed.filter(x => !!x).map((x) => x.toString().padStart(2, '0')).join(':')
return final.length <= 3 ? `0:${final.padStart(2, '0') || 0}` : final
}
}

10
src/index.ts Normal file
View file

@ -0,0 +1,10 @@
export { AudioFilters } from './utils/AudioFilters';
export * as Constants from './utils/Constants';
export { ExtractorModel } from './Structures/ExtractorModel';
export { Player } from './Player';
export { Util } from './utils/Util';
export { Track } from './Structures/Track';
export { Queue } from './Structures/Queue';
export * from './types/types';
export { PlayerError } from './utils/PlayerError';
export { version } from '../package.json';

156
src/types/types.ts Normal file
View file

@ -0,0 +1,156 @@
import { downloadOptions } from 'ytdl-core';
import { User } from 'discord.js';
import { Readable, Duplex } from 'stream';
export interface PlayerOptions {
leaveOnEnd?: boolean;
leaveOnEndCooldown?: number;
leaveOnStop?: boolean;
leaveOnEmpty?: boolean;
leaveOnEmptyCooldown?: number;
autoSelfDeaf?: boolean;
enableLive?: boolean;
ytdlDownloadOptions?: downloadOptions;
useSafeSearch?: boolean;
disableAutoRegister?: boolean;
}
export type FiltersName = keyof QueueFilters;
export type TrackSource = 'soundcloud' | 'youtube' | 'arbitrary';
export interface TrackData {
title: string;
description: string;
author: string;
url: string;
thumbnail: string;
duration: string;
views: number;
requestedBy: User;
fromPlaylist: boolean;
source?: TrackSource;
engine?: any;
live?: boolean;
}
export type QueueFilters = {
bassboost?: boolean;
'8D'?: boolean;
vaporwave?: boolean;
nightcore?: boolean;
phaser?: boolean;
tremolo?: boolean;
vibrato?: boolean;
reverse?: boolean;
treble?: boolean;
normalizer?: boolean;
surrounding?: boolean;
pulsator?: boolean;
subboost?: boolean;
karaoke?: boolean;
flanger?: boolean;
gate?: boolean;
haas?: boolean;
mcompand?: boolean;
mono?: boolean;
mstlr?: boolean;
mstrr?: boolean;
compressor?: boolean;
expander?: boolean;
softlimiter?: boolean;
chorus?: boolean;
chorus2d?: boolean;
chorus3d?: boolean;
fadein?: boolean;
};
export type QueryType =
| 'soundcloud_track'
| 'soundcloud_playlist'
| 'spotify_song'
| 'spotify_album'
| 'spotify_playlist'
| 'youtube_video'
| 'youtube_playlist'
| 'vimeo'
| 'facebook'
| 'reverbnation'
| 'attachment'
| 'youtube_search';
export interface ExtractorModelData {
title: string;
duration: number;
thumbnail: string;
engine: string | Readable | Duplex;
views: number;
author: string;
description: string;
url: string;
version?: string;
important?: boolean;
}
export interface PlayerProgressbarOptions {
timecodes?: boolean;
queue?: boolean;
length?: number;
}
export interface LyricsData {
title: string;
id: number;
thumbnail: string;
image: string;
url: string;
artist: {
name: string;
id: number;
url: string;
image: string;
};
lyrics?: string;
}
export interface PlayerStats {
uptime: number;
connections: number;
users: number;
queues: number;
extractors: number;
versions: {
ffmpeg: string;
node: string;
v8: string;
};
system: {
arch: string;
platform:
| 'aix'
| 'android'
| 'darwin'
| 'freebsd'
| 'linux'
| 'openbsd'
| 'sunos'
| 'win32'
| 'cygwin'
| 'netbsd';
cpu: number;
memory: {
total: string;
usage: string;
rss: string;
arrayBuffers: string;
};
uptime: number;
};
}
export interface TimeData {
days: number;
hours: number;
minutes: number;
seconds: number;
}

104
src/utils/AudioFilters.ts Normal file
View file

@ -0,0 +1,104 @@
import { FiltersName } from '../types/types';
/**
* The available audio filters
* @typedef {Object} AudioFilters
* @property {String} bassboost The bassboost filter
* @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
* @property {String} surrounding The surrounding filter
* @property {String} pulsator The pulsator filter
* @property {String} subboost The subboost filter
* @property {String} kakaoke 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
*/
const FilterList = {
bassboost: 'bass=g=20',
'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',
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',
*[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');
},
get length() {
return Object.keys(this).filter((p) => ['names', 'length'].includes(p) && typeof this[p as FiltersName] !== 'function').length;
},
toString() {
return `${Object.values(this).join(',')}`;
},
create(filter?: FiltersName[]): string {
if (!filter || !Array.isArray(filter)) return this.toString();
return filter
.filter((predicate) => typeof predicate === 'string')
.map((m) => this[m])
.join(',');
},
define(filterName: string, value: string): void {
if (typeof this[filterName as FiltersName] && typeof this[filterName as FiltersName] === 'function') return;
this[filterName as FiltersName] = value;
},
defineBulk(filterArray: { name: string; value: string }[]): void {
filterArray.forEach((arr) => this.define(arr.name, arr.value));
}
};
export default FilterList;
export { FilterList as AudioFilters };

40
src/utils/Constants.ts Normal file
View file

@ -0,0 +1,40 @@
import { PlayerOptions as DP_OPTIONS } from '../types/types';
export const PlayerEvents = {
BOT_DISCONNECT: 'botDisconnect',
CHANNEL_EMPTY: 'channelEmpty',
CONNECTION_CREATE: 'connectionCreate',
ERROR: 'error',
MUSIC_STOP: 'musicStop',
NO_RESULTS: 'noResults',
PLAYLIST_ADD: 'playlistAdd',
PLAYLIST_PARSE_END: 'playlistParseEnd',
PLAYLIST_PARSE_START: 'playlistParseStart',
QUEUE_CREATE: 'queueCreate',
QUEUE_END: 'queueEnd',
SEARCH_CANCEL: 'searchCancel',
SEARCH_INVALID_RESPONSE: 'searchInvalidResponse',
SEARCH_RESULTS: 'searchResults',
TRACK_ADD: 'trackAdd',
TRACK_START: 'trackStart'
};
export const PlayerErrorEventCodes = {
LIVE_VIDEO: 'LiveVideo',
NOT_CONNECTED: 'NotConnected',
UNABLE_TO_JOIN: 'UnableToJoin',
NOT_PLAYING: 'NotPlaying',
PARSE_ERROR: 'ParseError',
VIDEO_UNAVAILABLE: 'VideoUnavailable',
MUSIC_STARTING: 'MusicStarting'
};
export const PlayerOptions: DP_OPTIONS = {
leaveOnEnd: true,
leaveOnStop: true,
leaveOnEmpty: true,
leaveOnEmptyCooldown: 0,
autoSelfDeaf: true,
enableLive: false,
ytdlDownloadOptions: {}
};

10
src/utils/PlayerError.ts Normal file
View file

@ -0,0 +1,10 @@
export default class PlayerError extends Error {
constructor(msg: string, name?: string) {
super();
this.name = name ?? 'PlayerError';
this.message = msg;
Error.captureStackTrace(this);
}
}
export { PlayerError };

231
src/utils/Util.ts Normal file
View file

@ -0,0 +1,231 @@
import { QueryType, TimeData } from '../types/types';
import { FFmpeg } from 'prism-media';
import YouTube from 'youtube-sr';
import { Track } from '../Structures/Track';
// @ts-ignore
import { validateURL as SoundcloudValidateURL } from 'soundcloud-scraper';
import { VoiceChannel } from 'discord.js';
const spotifySongRegex = /https?:\/\/(?:embed\.|open\.)(?:spotify\.com\/)(?:track\/|\?uri=spotify:track:)((\w|-){22})/;
const spotifyPlaylistRegex = /https?:\/\/(?:embed\.|open\.)(?:spotify\.com\/)(?:playlist\/|\?uri=spotify:playlist:)((\w|-){22})/;
const spotifyAlbumRegex = /https?:\/\/(?:embed\.|open\.)(?:spotify\.com\/)(?:album\/|\?uri=spotify:album:)((\w|-){22})/;
const vimeoRegex = /(http|https)?:\/\/(www\.|player\.)?vimeo\.com\/(?:channels\/(?:\w+\/)?|groups\/([^/]*)\/videos\/|video\/|)(\d+)(?:|\/\?)/;
const facebookRegex = /(https?:\/\/)(www\.|m\.)?(facebook|fb).com\/.*\/videos\/.*/;
const reverbnationRegex = /https:\/\/(www.)?reverbnation.com\/(.+)\/song\/(.+)/;
const attachmentRegex = /^(?:(?:https?|ftp):\/\/)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))(?::\d{2,5})?(?:\/\S*)?$/;
export class Util {
/**
* Static Player Util class
*/
constructor() {
throw new Error(`The ${this.constructor.name} class is static and cannot be instantiated!`);
}
/**
* Checks FFmpeg Version
* @param {Boolean} [force] If it should forcefully get the version
* @returns {String}
*/
static getFFmpegVersion(force?: boolean): string {
try {
const info = FFmpeg.getInfo(Boolean(force));
return info.version;
} catch {
return null;
}
}
/**
* Checks FFmpeg
* @param {Boolean} [force] If it should forcefully get the version
* @returns {Boolean}
*/
static checkFFmpeg(force?: boolean): boolean {
const version = Util.getFFmpegVersion(force);
return version === null ? false : true;
}
/**
* Alerts if FFmpeg is not available
*/
static alertFFmpeg(): void {
const hasFFmpeg = Util.checkFFmpeg();
if (!hasFFmpeg)
console.warn(
'[Discord Player] FFmpeg/Avconv not found! Install via "npm install ffmpeg-static" or download from https://ffmpeg.org/download.html'
);
}
/**
* Resolves query type
* @param {String} query The query
* @returns {QueryType}
*/
static getQueryType(query: string): QueryType {
if (SoundcloudValidateURL(query) && !query.includes('/sets/')) return 'soundcloud_track';
if (SoundcloudValidateURL(query) && query.includes('/sets/')) return 'soundcloud_playlist';
if (spotifySongRegex.test(query)) return 'spotify_song';
if (spotifyAlbumRegex.test(query)) return 'spotify_album';
if (spotifyPlaylistRegex.test(query)) return 'spotify_playlist';
if (YouTube.validate(query, 'PLAYLIST')) return 'youtube_playlist';
if (YouTube.validate(query, 'VIDEO')) return 'youtube_video';
if (vimeoRegex.test(query)) return 'vimeo';
if (facebookRegex.test(query)) return 'facebook';
if (reverbnationRegex.test(query)) return 'reverbnation';
if (Util.isURL(query)) return 'attachment';
return 'youtube_search';
}
/**
* Checks if the given string is url
* @param {String} str URL to check
* @returns {Boolean}
*/
static isURL(str: string): boolean {
return str.length < 2083 && attachmentRegex.test(str);
}
/**
* Returns Vimeo ID
* @param {String} query Vimeo link
* @returns {String}
*/
static getVimeoID(query: string): string {
return Util.getQueryType(query) === 'vimeo'
? query
.split('/')
.filter((x) => !!x)
.pop()
: null;
}
/**
* Parses ms time
* @param {Number} milliseconds Time to parse
* @returns {TimeData}
*/
static parseMS(milliseconds: number): TimeData {
const roundTowardsZero = milliseconds > 0 ? Math.floor : Math.ceil;
return {
days: roundTowardsZero(milliseconds / 86400000),
hours: roundTowardsZero(milliseconds / 3600000) % 24,
minutes: roundTowardsZero(milliseconds / 60000) % 60,
seconds: roundTowardsZero(milliseconds / 1000) % 60
};
}
/**
* Creates simple duration string
* @param {object} durObj Duration object
* @returns {String}
*/
static durationString(durObj: object): string {
return Object.values(durObj)
.map((m) => (isNaN(m) ? 0 : m))
.join(':');
}
/**
* Makes youtube searches
* @param {String} query The query
* @param {any} options Options
* @returns {Promise<Track[]>}
*/
static ytSearch(query: string, options?: any): Promise<Track[]> {
return new Promise(async (resolve) => {
await YouTube.search(query, {
type: 'video',
safeSearch: Boolean(options?.player.options.useSafeSearch),
limit: options.limit ?? 10
})
.then((results) => {
resolve(
results.map(
(r) =>
new Track(options?.player, {
title: r.title,
description: r.description,
author: r.channel.name,
url: r.url,
thumbnail: r.thumbnail.displayThumbnailURL(),
duration: Util.buildTimeCode(Util.parseMS(r.duration)),
views: r.views,
requestedBy: options?.user,
fromPlaylist: Boolean(options?.pl),
source: 'youtube'
})
)
);
})
.catch(() => resolve([]));
});
}
/**
* Checks if this system is running in replit.com
* @returns {Boolean}
*/
static isRepl(): boolean {
if ('DP_REPL_NOCHECK' in process.env) return false;
const REPL_IT_PROPS = [
'REPL_SLUG',
'REPL_OWNER',
'REPL_IMAGE',
'REPL_PUBKEYS',
'REPL_ID',
'REPL_LANGUAGE',
'REPLIT_DB_URL'
];
for (const prop of REPL_IT_PROPS) if (prop in process.env) return true;
return false;
}
/**
* Checks if the given voice channel is empty
* @param {DiscordVoiceChannel} channel The voice channel
* @returns {Boolean}
*/
static isVoiceEmpty(channel: VoiceChannel): boolean {
return channel.members.filter((member) => !member.user.bot).size === 0;
}
/**
* Builds time code
* @param {object} data The data to build time code from
* @returns {String}
*/
static buildTimeCode(data: any): string {
const items = Object.keys(data);
const required = ['days', 'hours', 'minutes', 'seconds'];
const parsed = items.filter((x) => required.includes(x)).map((m) => (data[m] > 0 ? data[m] : ''));
const final = parsed
.filter((x) => !!x)
.map((x) => x.toString().padStart(2, '0'))
.join(':');
return final.length <= 3 ? `0:${final.padStart(2, '0') || 0}` : final;
}
/**
* Manage CJS require
* @param {String} id id to require
* @returns {any}
*/
static require(id: string): any {
try {
return require(id);
} catch {
return null;
}
}
}
export default Util;

View file

@ -1,21 +1,15 @@
{
"compilerOptions": {
"strict": true,
"moduleResolution": "node",
"declaration": false,
"removeComments": false,
"alwaysStrict": true,
"pretty": false,
"target": "ES6",
"module": "commonjs",
"target": "es2019",
"lib": [
"esnext",
"esnext.array",
"esnext.asynciterable",
"esnext.intl",
"esnext.symbol"
],
"sourceMap": false,
"skipDefaultLibCheck": true
}
"declaration": true,
"outDir": "./lib",
"strict": true,
"strictNullChecks": false,
"resolveJsonModule": true,
"esModuleInterop": true
},
"include": [
"src/**/*"
]
}

15
tslint.json Normal file
View file

@ -0,0 +1,15 @@
{
"defaultSeverity": "error",
"extends": ["tslint:recommended", "tslint-config-prettier"],
"jsRules": {
"no-unused-expression": true
},
"rules": {
"object-literal-sort-keys": false,
"interface-name": false,
"no-empty": false,
"no-console": false,
"radix": false
},
"rulesDirectory": []
}

273
typings/index.d.ts vendored
View file

@ -1,273 +0,0 @@
declare module 'discord-player' {
import { EventEmitter } from 'events';
import { Client, Collection, Message, MessageCollector, Snowflake, User, VoiceChannel, VoiceConnection } from 'discord.js';
import { Playlist as YTSRPlaylist } from 'youtube-sr';
import { Stream, Readable } from 'stream';
export const version: string;
class Util {
static isVoiceEmpty(channel: VoiceChannel): boolean;
static isSoundcloudLink(query: string): boolean;
static isSpotifyLink(query: string): boolean;
static isYTPlaylistLink(query: string): boolean;
static isYTVideoLink(query: string): boolean;
static isSoundcloudPlaylist(query: string): boolean;
static isVimeoLink(query: string): boolean;
static getVimeoID(query: string): string;
static isFacebookLink(query: string): boolean;
static buildTimecode(data: any): string;
}
export class Player extends EventEmitter {
constructor(client: Client, options?: PlayerOptions)
public client: Client;
public util: Util;
public options: PlayerOptions;
public queues: Collection<Snowflake, Queue>;
public filters: PlayerFilters;
public static get AudioFilters(): PlayerFilters;
public isPlaying(message: Message): boolean;
public setFilters(message: Message, newFilters: Partial<FiltersOption>): Promise<void>;
public play(message: Message, query: string | Track, firstResult?: boolean, isAttachment?: boolean): Promise<void>;
public pause(message: Message): void;
public resume(message: Message): void;
public stop(message: Message): void;
public setVolume(message: Message, percent: number): void;
public getQueue(message: Message): Queue;
public clearQueue(message: Message): void;
public skip(message: Message): void;
public back(message: Message): void;
public nowPlaying(message: Message): Track;
public setRepeatMode(message: Message, enabled: boolean): boolean;
public setLoopMode(message: Message, enabled: boolean): boolean
public shuffle(message: Message): Queue;
public remove(message: Message, trackOrPosition: Track | number): Track;
public createProgressBar(message: Message, progressBarOptions?: ProgressBarOptions): string;
public seek(message: Message, time: number): Promise<void>;
public moveTo(message: Message, channel: VoiceChannel): void;
public on<K extends keyof PlayerEvents>(event: K, listener: (...args: PlayerEvents[K]) => void): this;
public once<K extends keyof PlayerEvents>(event: K, listener: (...args: PlayerEvents[K]) => void): this;
public emit<K extends keyof PlayerEvents>(event: K, ...args: PlayerEvents[K]): boolean;
}
type MusicQuality = 'high' | 'low';
interface PlayerOptions {
leaveOnEnd?: boolean;
leaveOnEndCooldown?: number;
leaveOnStop?: boolean;
leaveOnEmpty?: boolean;
leaveOnEmptyCooldown?: number;
autoSelfDeaf?: boolean;
quality?: MusicQuality;
enableLive?: boolean;
ytdlRequestOptions?: any;
}
type Filters =
| 'bassboost'
| '8D'
| 'vaporwave'
| 'nightcore'
| 'phaser'
| 'tremolo'
| 'vibrato'
| 'reverse'
| 'treble'
| 'normalizer'
| 'surrounding'
| 'pulsator'
| 'subboost'
| 'karaoke'
| 'flanger'
| 'gate'
| 'haas'
| 'mcompand'
| 'mono'
| 'mstlr'
| 'mstrr'
| 'compressor'
| 'expander'
| 'softlimiter'
| 'chorus'
| 'chorus2d'
| 'chorus3d'
| 'fadein';
type FiltersStatuses = {
[key in Filters]: boolean;
}
type FiltersOption = {
[key in Filters]: boolean;
}
type PlayerFilters = {
[key in Filters]: string
}
interface ProgressBarOptions {
timecodes?: boolean;
queue?: boolean;
}
interface CustomPlaylist {
tracks: Track[];
duration: number;
thumbnail: string;
requestedBy: User;
}
type Playlist = YTSRPlaylist & CustomPlaylist;
type PlayerError = 'NotConnected' | 'UnableToJoin' | 'NotPlaying' | 'LiveVideo' | 'ParseError' | 'VideoUnavailable';
interface PlayerEvents {
searchResults: [Message, string, Track[]];
searchInvalidResponse: [Message, string, Track[], string, MessageCollector];
searchCancel: [Message, string, Track[]];
noResults: [Message, string];
playlistAdd: [Message, Queue, Playlist];
trackAdd: [Message, Queue, Track];
trackStart: [Message, Track];
botDisconnect: [Message];
channelEmpty: [Message, Queue];
musicStop: [];
queueCreate: [Message, Queue];
queueEnd: [Message, Queue];
error: [PlayerError, Message];
playlistParseStart: [any, Message];
playlistParseEnd: [any, Message];
}
class Queue {
constructor(guildID: string, message: Message, filters: PlayerFilters);
public guildID: string;
public voiceConnection?: VoiceConnection;
public stream: Stream;
public tracks: Track[];
public previousTracks: Track[];
public stopped: boolean;
public lastSkipped: boolean;
public volume: number;
public paused: boolean;
public repeatMode: boolean;
public loopMode: boolean;
public filters: FiltersStatuses;
public firstMessage: Message;
private additionalStreamTime: number;
// these are getters
public playing: Track;
public calculatedVolume: number;
public currentStreamTime: number;
}
class Track {
constructor(videoData: object, user: User, player: Player);
public player: Player;
public title: string;
public description: string;
public author: string;
public url: string;
public thumbnail: string;
public duration: string;
public views: number;
public requestedBy: User;
public fromPlaylist: boolean;
// these are getters
public durationMS: number;
public queue: Queue;
}
export interface RawExtractedData {
title: string;
format: string;
size: number;
sizeFormat: "MB";
stream: Readable;
}
export interface VimeoExtractedData {
id: number;
duration: number;
title: string;
url: string;
thumbnail: string;
width: number;
height: number;
stream: {
cdn: string;
fps: number;
width: number;
height: number;
id: string;
mime: string;
origin: string;
profile: number;
quality: string;
url: string;
};
author: {
accountType: string;
id: number;
name: string;
url: string;
avatar: string;
}
}
interface FacebookExtractedData {
name: string;
title: string;
description: string;
rawVideo: string;
thumbnail: string;
uploadedAt: Date;
duration: string;
interactionCount: number;
streamURL: string;
publishedAt: Date;
width: number;
height: number;
nsfw: boolean;
genre: string;
keywords: string[];
comments: number;
size: string;
quality: string;
author: {
type: string;
name: string;
url: string;
};
publisher: {
type: string;
name: string;
url: string;
avatar: string;
};
url: string;
shares: string;
views: string;
}
class Discord {
static getInfo(url: string): Promise<RawExtractedData>;
static download(url: string): Promise<Readable>;
}
class Facebook {
static validateURL(url: string): boolean;
static download(url: string): Promise<Readable>;
static getInfo(url: string): Promise<FacebookExtractedData>;
}
class Vimeo {
static getInfo(id: number): Promise<VimeoExtractedData>;
static download(id: number): Promise<Readable>;
}
interface Extractors {
DiscordExtractor: Discord;
FacebookExtractor: Facebook;
VimeoExtractor: Vimeo;
}
export const Extractors: Extractors;
}

4645
yarn.lock Normal file

File diff suppressed because it is too large Load diff