ft: master
This commit is contained in:
commit
211abefe77
49 changed files with 7830 additions and 2465 deletions
22
.eslintrc.js
22
.eslintrc.js
|
@ -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
2
.gitattributes
vendored
|
@ -1,2 +0,0 @@
|
|||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
32
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
32
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal 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
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal 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
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal 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
11
.github/ISSUE_TEMPLATE/question.md
vendored
Normal 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
8
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal 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/dependabot.yml
vendored
Normal file
27
.github/dependabot.yml
vendored
Normal file
|
@ -0,0 +1,27 @@
|
|||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: npm
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
time: "04:00"
|
||||
open-pull-requests-limit: 10
|
||||
target-branch: develop
|
||||
ignore:
|
||||
- dependency-name: "@types/node"
|
||||
versions:
|
||||
- 14.14.32
|
||||
- 14.14.33
|
||||
- 14.14.34
|
||||
- 14.14.35
|
||||
- 14.14.37
|
||||
- 14.14.39
|
||||
- 14.14.41
|
||||
- 15.0.0
|
||||
- 15.0.1
|
||||
- dependency-name: parse-ms
|
||||
versions:
|
||||
- 3.0.0
|
||||
- dependency-name: "@discordjs/opus"
|
||||
versions:
|
||||
- 0.5.0
|
27
.github/workflows/docs-deploy.yml
vendored
Normal file
27
.github/workflows/docs-deploy.yml
vendored
Normal 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 }}
|
28
.github/workflows/jsdoc-deploy.yml
vendored
28
.github/workflows/jsdoc-deploy.yml
vendored
|
@ -1,28 +0,0 @@
|
|||
name: GitHub pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Build
|
||||
uses: andstor/jsdoc-action@v1
|
||||
with:
|
||||
output_dir: ./docs
|
||||
config_file: .jsdoc.json
|
||||
template: Androz2091/jsdoc-skyceil
|
||||
front_page: README.md
|
||||
|
||||
- name: Deploy
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
personal_token: ${{ secrets.ACTIONS_DEPLOY_KEY }}
|
||||
publish_dir: ./docs
|
||||
cname: discord-player.js.org
|
4
.github/workflows/npm-publish.yml
vendored
4
.github/workflows/npm-publish.yml
vendored
|
@ -14,6 +14,8 @@ jobs:
|
|||
with:
|
||||
node-version: 12
|
||||
registry-url: https://registry.npmjs.org/
|
||||
- run: npm publish
|
||||
- run: npm install
|
||||
- run: npm run build
|
||||
- run: npm publish --access public
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{secrets.npm_token}}
|
18
.gitignore
vendored
18
.gitignore
vendored
|
@ -1,7 +1,15 @@
|
|||
# Node
|
||||
node_modules
|
||||
poc.js
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
docs
|
||||
.vscode
|
||||
test
|
||||
|
||||
# Tests
|
||||
test
|
||||
|
||||
# Compiled files
|
||||
lib
|
||||
|
||||
# Yarn logs
|
||||
yarn*.log
|
||||
|
||||
# Demo
|
||||
demo
|
||||
|
|
31
.jsdoc.json
31
.jsdoc.json
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -1,3 +1,6 @@
|
|||
docs
|
||||
node_modules/
|
||||
poc.js
|
||||
src/
|
||||
tslint.json
|
||||
tsconfig.json
|
||||
.prettierrc
|
||||
test/
|
||||
demo/
|
6
.prettierrc
Normal file
6
.prettierrc
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"printWidth": 120,
|
||||
"trailingComma": "none",
|
||||
"singleQuote": true,
|
||||
"tabWidth": 4
|
||||
}
|
128
CODE_OF_CONDUCT.md
Normal file
128
CODE_OF_CONDUCT.md
Normal 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
22
CONTRIBUTING.md
Normal 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`
|
2
LICENSE
2
LICENSE
|
@ -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
|
||||
|
|
199
README.md
199
README.md
|
@ -1,32 +1,42 @@
|
|||
# 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**.
|
||||
[![discordBadge](https://img.shields.io/discord/558328638911545423?style=for-the-badge&color=7289da)](https://androz2091.fr/discord)
|
||||
|
||||
## 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 +44,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 +71,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 +82,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)
|
||||
|
||||
## Advanced
|
||||
|
||||
### Use cookies
|
||||
|
||||
```js
|
||||
const player = new Player(client, {
|
||||
ytdlDownloadOptions: {
|
||||
requestOptions: {
|
||||
headers: {
|
||||
cookie: "YOUR_YOUTUBE_COOKIE"
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 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 }
|
||||
}
|
||||
});
|
||||
```
|
||||
|
|
89
docs/extractors/extractor.md
Normal file
89
docs/extractors/extractor.md
Normal 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
139
docs/general/welcome.md
Normal 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
14
docs/index.yml
Normal 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
18
docs/youtube/cookies.md
Normal 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
16
docs/youtube/proxy.md
Normal 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 }
|
||||
}
|
||||
});
|
||||
```
|
10
index.js
10
index.js
|
@ -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
17
jsdoc.json
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
72
package.json
72
package.json
|
@ -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.5",
|
||||
"description": "Complete framework to facilitate music commands using discord.js",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/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.1",
|
||||
"jsdom": "^16.4.0",
|
||||
"merge-options": "^3.0.4",
|
||||
"node-fetch": "^2.6.0",
|
||||
"parse-ms": "^2.1.0",
|
||||
"reverbnation-scraper": "^2.0.0",
|
||||
"discord-ytdl-core": "^5.0.2",
|
||||
"soundcloud-scraper": "^4.0.3",
|
||||
"spotify-url-info": "^2.2.0",
|
||||
"youtube-sr": "^4.0.2",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -1,6 +0,0 @@
|
|||
module.exports = {
|
||||
DiscordExtractor: require('./Discord'),
|
||||
FacebookExtractor: require('./Facebook'),
|
||||
ReverbnationExtractor: require('reverbnation-scraper'),
|
||||
VimeoExtractor: require('./Vimeo')
|
||||
}
|
|
@ -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
|
|
@ -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
|
1339
src/Player.js
1339
src/Player.js
File diff suppressed because it is too large
Load diff
1466
src/Player.ts
Normal file
1466
src/Player.ts
Normal file
File diff suppressed because it is too large
Load diff
128
src/Queue.js
128
src/Queue.js
|
@ -1,128 +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
|
||||
}
|
||||
|
||||
/**
|
||||
* The current playing track
|
||||
* @type {Track}
|
||||
*/
|
||||
get playing () {
|
||||
return this.tracks[0]
|
||||
}
|
||||
|
||||
/**
|
||||
* The calculated volume of the queue
|
||||
* @type {number}
|
||||
*/
|
||||
get calculatedVolume () {
|
||||
return this.filters.bassboost ? this.volume + 50 : this.volume
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total time of the queue in milliseconds
|
||||
* @type {number}
|
||||
*/
|
||||
get totalTime () {
|
||||
return this.tracks.length > 0 ? this.tracks.map((t) => t.durationMS).reduce((p, c) => p + c) : 0
|
||||
}
|
||||
|
||||
/**
|
||||
* The current stream time
|
||||
* @type {number}
|
||||
*/
|
||||
get currentStreamTime () {
|
||||
return this.voiceConnection.dispatcher
|
||||
? this.voiceConnection.dispatcher.streamTime + this.additionalStreamTime
|
||||
: 0
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Queue
|
70
src/Structures/ExtractorModel.ts
Normal file
70
src/Structures/ExtractorModel.ts
Normal 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
204
src/Structures/Queue.ts
Normal 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
146
src/Structures/Track.ts
Normal 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;
|
94
src/Track.js
94
src/Track.js
|
@ -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
|
91
src/Util.js
91
src/Util.js
|
@ -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
|
||||
}
|
||||
}
|
9
src/index.ts
Normal file
9
src/index.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
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';
|
156
src/types/types.ts
Normal file
156
src/types/types.ts
Normal 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
104
src/utils/AudioFilters.ts
Normal 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
40
src/utils/Constants.ts
Normal 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
10
src/utils/PlayerError.ts
Normal 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
231
src/utils/Util.ts
Normal 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;
|
|
@ -1,21 +1,14 @@
|
|||
{
|
||||
"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,
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
]
|
||||
}
|
||||
|
|
15
tslint.json
Normal file
15
tslint.json
Normal 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": []
|
||||
}
|
274
typings/index.d.ts
vendored
274
typings/index.d.ts
vendored
|
@ -1,274 +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' | 'MusicStarting';
|
||||
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;
|
||||
public totalTime: 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;
|
||||
}
|
Loading…
Reference in a new issue