diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 43b6d49..0000000 --- a/.eslintrc.js +++ /dev/null @@ -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' - } -} diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index dfe0770..0000000 --- a/.gitattributes +++ /dev/null @@ -1,2 +0,0 @@ -# Auto detect text files and perform LF normalization -* text=auto diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..325fe93 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,32 @@ +--- +name: Bug report +about: Create a bug report to help us improve +title: "[BUG] " +labels: bug +assignees: '' + +--- + +**Describe the bug** + + +**To Reproduce** +Steps to reproduce the behavior: + + +**Expected behavior** + + +**Screenshots** + + +**Please complete the following information:** + - Node Version: [x.x.x] + - Library Version: [x.x.x] + - Discord.js Version: [x.x.x] + +**Additional context** + diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..6635d17 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -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 \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..16ba85a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -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.** + + +**Describe the solution you'd like** + + +**Describe alternatives you've considered** + + +**Additional context** + diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md new file mode 100644 index 0000000..2953cde --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.md @@ -0,0 +1,11 @@ +--- +name: Question +about: Some questions related to this lib +title: "[QUESTION] " +labels: question +assignees: '' + +--- + +**Question** + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..2004733 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,8 @@ +## Changes + + +## Status + +- [ ] These changes have been tested and formatted properly. +- [ ] This PR includes only documentation changes, no code change. +- [ ] This PR introduces some Breaking changes. \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..a9702c2 --- /dev/null +++ b/.github/dependabot.yml @@ -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 diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml new file mode 100644 index 0000000..f75630c --- /dev/null +++ b/.github/workflows/docs-deploy.yml @@ -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 }} \ No newline at end of file diff --git a/.github/workflows/jsdoc-deploy.yml b/.github/workflows/jsdoc-deploy.yml deleted file mode 100644 index ce11890..0000000 --- a/.github/workflows/jsdoc-deploy.yml +++ /dev/null @@ -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 diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index 8b152aa..2147d77 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -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}} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 5013b61..7033b56 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,15 @@ +# Node node_modules -poc.js package-lock.json -yarn.lock -docs -.vscode -test \ No newline at end of file + +# Tests +test + +# Compiled files +lib + +# Yarn logs +yarn*.log + +# Demo +demo diff --git a/.jsdoc.json b/.jsdoc.json deleted file mode 100644 index bb162a6..0000000 --- a/.jsdoc.json +++ /dev/null @@ -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" - } -} \ No newline at end of file diff --git a/.npmignore b/.npmignore index fcb7c83..412f4d3 100644 --- a/.npmignore +++ b/.npmignore @@ -1,3 +1,6 @@ -docs -node_modules/ -poc.js +src/ +tslint.json +tsconfig.json +.prettierrc +test/ +demo/ \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..ab07ced --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "printWidth": 120, + "trailingComma": "none", + "singleQuote": true, + "tabWidth": 4 +} \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..6ac2874 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -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. \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..dd2bd5e --- /dev/null +++ b/CONTRIBUTING.md @@ -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` \ No newline at end of file diff --git a/LICENSE b/LICENSE index 4f58a0f..e1e6f2c 100644 --- a/LICENSE +++ b/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 diff --git a/README.md b/README.md index be2a168..7357eb1 100644 --- a/README.md +++ b/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 } + } +}); +``` diff --git a/docs/extractors/extractor.md b/docs/extractors/extractor.md new file mode 100644 index 0000000..9510969 --- /dev/null +++ b/docs/extractors/extractor.md @@ -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! \ No newline at end of file diff --git a/docs/general/welcome.md b/docs/general/welcome.md new file mode 100644 index 0000000..2075526 --- /dev/null +++ b/docs/general/welcome.md @@ -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 } + } +}); +``` diff --git a/docs/index.yml b/docs/index.yml new file mode 100644 index 0000000..f367636 --- /dev/null +++ b/docs/index.yml @@ -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 \ No newline at end of file diff --git a/docs/youtube/cookies.md b/docs/youtube/cookies.md new file mode 100644 index 0000000..2405dcf --- /dev/null +++ b/docs/youtube/cookies.md @@ -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` \ No newline at end of file diff --git a/docs/youtube/proxy.md b/docs/youtube/proxy.md new file mode 100644 index 0000000..3dc8ba5 --- /dev/null +++ b/docs/youtube/proxy.md @@ -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 } + } +}); +``` \ No newline at end of file diff --git a/index.js b/index.js deleted file mode 100644 index 05f8ee9..0000000 --- a/index.js +++ /dev/null @@ -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 -} diff --git a/jsdoc.json b/jsdoc.json new file mode 100644 index 0000000..6aeb5e9 --- /dev/null +++ b/jsdoc.json @@ -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" + ] + } +} \ No newline at end of file diff --git a/package.json b/package.json index 70cdee1..7e94075 100644 --- a/package.json +++ b/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" } } diff --git a/src/Extractors/Discord.js b/src/Extractors/Discord.js deleted file mode 100644 index 59c84f1..0000000 --- a/src/Extractors/Discord.js +++ /dev/null @@ -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} - */ - 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 diff --git a/src/Extractors/Extractor.js b/src/Extractors/Extractor.js deleted file mode 100644 index da3e026..0000000 --- a/src/Extractors/Extractor.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - DiscordExtractor: require('./Discord'), - FacebookExtractor: require('./Facebook'), - ReverbnationExtractor: require('reverbnation-scraper'), - VimeoExtractor: require('./Vimeo') -} diff --git a/src/Extractors/Facebook.js b/src/Extractors/Facebook.js deleted file mode 100644 index bbec635..0000000 --- a/src/Extractors/Facebook.js +++ /dev/null @@ -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} - */ - 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 diff --git a/src/Extractors/Vimeo.js b/src/Extractors/Vimeo.js deleted file mode 100644 index c3985a4..0000000 --- a/src/Extractors/Vimeo.js +++ /dev/null @@ -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} - */ - 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('