discord-player
Advanced tools
Comparing version 2.3.1 to 3.0.0-beta.0
module.exports = { | ||
version: require('./package.json').version, | ||
Player: require('./src/Player') | ||
}; | ||
} |
{ | ||
"name": "discord-player", | ||
"version": "2.3.1", | ||
"version": "3.0.0-beta.0", | ||
"description": "Complete framework to facilitate music commands using discord.js v12", | ||
@@ -33,5 +33,7 @@ "main": "index.js", | ||
"merge-options": "^2.0.0", | ||
"moment": "^2.27.0", | ||
"node-fetch": "^2.6.0", | ||
"soundcloud-scraper": "^2.0.0", | ||
"spotify-url-info": "^1.3.1", | ||
"ytpl": "^0.2.0", | ||
"ytpl": "^0.2.4", | ||
"ytsr": "^0.1.19" | ||
@@ -38,0 +40,0 @@ }, |
@@ -49,2 +49,4 @@ # Discord 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}...`)) | ||
@@ -64,4 +66,4 @@ client.on("ready", () => { | ||
if(command === "play"){ | ||
let track = await client.player.play(message.member.voice.channel, args[0], message.member.user.tag); | ||
message.channel.send(`Currently playing ${track.name}! - Requested by ${track.requestedBy}`); | ||
client.player.play(message, args[0], message.member.user); | ||
// as we registered the event above, no need to send a success message here | ||
} | ||
@@ -82,26 +84,25 @@ | ||
#### Queue initialization | ||
#### Play a track | ||
* [play(voiceChannel, track, requestedBy)](https://discord-player.js.org/Player.html#play) - play a track in a server | ||
* [play(message, track, requestedBy)](https://discord-player.js.org/Player.html#play) - play a track in a server | ||
#### Queue management | ||
#### Check if a track is being played | ||
* [isPlaying(guildID)](https://discord-player.js.org/Player.html#isPlaying) - check if there is a queue for a specific server | ||
* [isPlaying(message)](https://discord-player.js.org/Player.html#isPlaying) - check if there is a queue for a specific server | ||
#### Manage tracks in your queue | ||
#### Manage the queue | ||
* [getQueue(guildID)](https://discord-player.js.org/Player.html#getQueue) - get the server queue | ||
* [addToQueue(guildID, track, requestedBy)](https://discord-player.js.org/Player.html#addToQueue) - add a track to the server queue | ||
* [clearQueue(guildID)](https://discord-player.js.org/Player.html#clearQueue) - clear the server queue | ||
* [remove(guildID, track)](https://discord-player.js.org/Player.html#remove) - remove a track from the server queue | ||
* [nowPlaying(guildID)](https://discord-player.js.org/Player.html#nowPlaying) - get the current track | ||
* [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 | ||
* [nowPlaying(message)](https://discord-player.js.org/Player.html#nowPlaying) - get the current track | ||
#### Manage music stream | ||
* [skip(guildID)](https://discord-player.js.org/Player.html#skip) - skip the current track | ||
* [pause(guildID)](https://discord-player.js.org/Player.html#pause) - pause the current track | ||
* [resume(guildID)](https://discord-player.js.org/Player.html#resume) - resume the current track | ||
* [stop(guildID)](https://discord-player.js.org/Player.html#stop) - stop the current track | ||
* [setFilters(guildID, newFilters)](https://discord-player.js.org/Player.html#setFilters) - update filters (bassboost for example) | ||
* [setRepeatMode(guildID, boolean)](https://discord-player.js.org/Player.html#setRepeatMode) - enable or disable repeat mode for the server | ||
* [skip(message)](https://discord-player.js.org/Player.html#skip) - skip the current 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 | ||
@@ -111,16 +112,47 @@ ### Event messages | ||
```js | ||
// Play the music | ||
await client.player.play(message.member.voice.channel, "Despacito") | ||
// Then add some messages that will be sent when the events will be triggered | ||
client.player | ||
// Then add some messages that will be sent when the events will be triggered | ||
client.player.getQueue(message.guild.id) | ||
.on('end', () => { | ||
message.channel.send('There is no more music in the queue!'); | ||
// 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, track) => message.channel.send(`${track.title} has been added to the queue!`)) | ||
.on('playlistAdd', (message, playlist) => message.channel.send(`${playlist.title} has been added to the queue (${playlist.items.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('trackChanged', (oldTrack, newTrack) => { | ||
message.channel.send(`Now playing ${newTrack.name}...`); | ||
.on('searchInvalidResponse', (message, query, tracks, content, collector) => 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, queue) => message.channel.send('Music stopped as I have been disconnected from the channel!')) | ||
// Error handling | ||
.on('error', (message, error) => { | ||
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; | ||
default: | ||
message.channel.send(`Something went wrong... Error: ${error}`) | ||
} | ||
}) | ||
.on('channelEmpty', () => { | ||
message.channel.send('Stop playing, there is no more member in the voice channel...'); | ||
}); | ||
``` | ||
@@ -132,3 +164,4 @@ | ||
* [AtlantaBot](https://github.com/Androz2091/AtlantaBot) by [me](https://github.com/Androz2091) | ||
* [Discord-Music](https://github.com/hydraindia/discord-music) by [hydraindia](https://github.com/hydraindia) | ||
* [Music-bot](https://github.com/ZerioDev/Music-bot) by [ZerioDev](https://github.com/ZerioDev) |
1105
src/Player.js
@@ -6,4 +6,8 @@ const ytdl = require('discord-ytdl-core') | ||
const spotify = require('spotify-url-info') | ||
const soundcloud = require('soundcloud-scraper') | ||
const moment = require('moment') | ||
const Queue = require('./Queue') | ||
const Track = require('./Track') | ||
const Util = require('./Util') | ||
const { EventEmitter } = require('events') | ||
@@ -58,2 +62,3 @@ /** | ||
* @property {boolean} [leaveOnEmpty=true] Whether the bot should leave the voice channel if there is no more member in it. | ||
* @property {number} [leaveOnEmptyCooldown=0] Used when leaveOnEmpty is enabled, to let the time to users to come back in the voice channel. | ||
*/ | ||
@@ -72,3 +77,3 @@ | ||
class Player { | ||
class Player extends EventEmitter { | ||
/** | ||
@@ -80,4 +85,10 @@ * @param {Discord.Client} client Discord.js client | ||
if (!client) throw new SyntaxError('Invalid Discord client') | ||
super() | ||
/** | ||
* Utilities | ||
* @type {Util} | ||
*/ | ||
this.util = Util | ||
/** | ||
* Discord.js client instance | ||
@@ -89,5 +100,5 @@ * @type {Discord.Client} | ||
* Player queues | ||
* @type {Queue[]} | ||
* @type {Discord.Collection<Discord.Snowflake, Queue>} | ||
*/ | ||
this.queues = [] | ||
this.queues = new Discord.Collection() | ||
/** | ||
@@ -112,33 +123,98 @@ * Player options | ||
/** | ||
* Set the filters enabled for the guild. [Full list of the filters](https://discord-player.js.org/global.html#Filters) | ||
* @param {Discord.Snowflake} guildID | ||
* @param {Filters} newFilters | ||
* | ||
* @ignore | ||
* @param {String} query | ||
*/ | ||
resolveQueryType (query) { | ||
if (this.util.isSpotifyLink(query)) { | ||
return 'spotify-song' | ||
} else if (this.util.isYTPlaylistLink(query)) { | ||
return 'youtube-playlist' | ||
} else if (this.util.isYTVideoLink(query)) { | ||
return 'youtube-video' | ||
} else if (this.util.isSoundcloudLink(query)) { | ||
return 'soundcloud-song' | ||
} else { | ||
return 'youtube-video-keywords' | ||
} | ||
} | ||
/** | ||
* Search tracks | ||
* @ignore | ||
* @param {Discord.Message} message | ||
* @param {string} query | ||
* @returns {Promise<Track>} | ||
*/ | ||
_searchTracks (message, query) { | ||
return new Promise(async (resolve) => { | ||
const tracks = [] | ||
let updatedQuery = null | ||
let queryType = this.resolveQueryType(query) | ||
if (queryType === 'spotify-song') { | ||
const matchSpotifyURL = query.match(/https?:\/\/(?:embed\.|open\.)(?:spotify\.com\/)(?:track\/|\?uri=spotify:track:)((\w|-){22})/) | ||
if (matchSpotifyURL) { | ||
const spotifyData = await spotify.getPreview(query).catch(() => {}) | ||
if (spotifyData) { | ||
updatedQuery = `${spotifyData.artist} - ${spotifyData.track}` | ||
queryType = 'youtube-video-keywords' | ||
} | ||
} | ||
} else if (queryType === 'soundcloud-song') { | ||
const soundcloudData = await soundcloud.getSongInfo(query).catch(() => {}) | ||
if (soundcloudData) { | ||
updatedQuery = `${soundcloudData.author.name} - ${soundcloudData.title}` | ||
queryType = 'youtube-video-keywords' | ||
} | ||
} | ||
if (queryType === 'youtube-video-keywords') { | ||
await ytsr(updatedQuery || query).then((results) => { | ||
if (results.items.length !== 0) { | ||
const resultsVideo = results.items.filter((i) => i.type === 'video') | ||
tracks.push(...resultsVideo.map((r) => new Track(r, message.author, null))) | ||
} | ||
}).catch(() => {}) | ||
} | ||
if (tracks.length === 0) throw new Error('No tracks found for the specified query.') | ||
this.emit('searchResults', message, query, tracks) | ||
const collector = message.channel.createMessageCollector((m) => m.author.id === message.author.id, { | ||
time: 60000, | ||
errors: ['time'] | ||
}) | ||
collector.on('collect', ({ content }) => { | ||
if (!isNaN(content) && parseInt(content) >= 1 && parseInt(content) <= tracks.length) { | ||
const index = parseInt(content, 10) | ||
const track = tracks[index - 1] | ||
collector.stop() | ||
resolve(track) | ||
} else { | ||
this.emit('searchInvalidResponse', message, query, tracks, content, collector) | ||
} | ||
}) | ||
collector.on('end', (collected, reason) => { | ||
if (reason === 'time') { | ||
this.emit('searchCancel', message, query, tracks) | ||
} | ||
}) | ||
}) | ||
} | ||
/** | ||
* Change the filters. | ||
* @param {Discord.Message} message | ||
* @param {Partial<Filters>} newFilters The filters to update and their new status. | ||
* @example | ||
* client.on('message', async (message) => { | ||
* | ||
* const args = message.content.slice(settings.prefix.length).trim().split(/ +/g); | ||
* const command = args.shift().toLowerCase(); | ||
* | ||
* if(command === 'bassboost'){ | ||
* const bassboostEnabled = client.player.getQueue(message.guild.id).filters.bassboost; | ||
* if(!bassboostEnabled){ | ||
* client.player.setFilters(message.guild.id, { | ||
* bassboost: true | ||
* }); | ||
* message.channel.send("Bassboost effect has been enabled!"); | ||
* } else { | ||
* client.player.setFilters(message.guild.id, { | ||
* bassboost: false | ||
* }); | ||
* message.channel.send("Bassboost effect has been disabled!"); | ||
* } | ||
* } | ||
* | ||
* client.player.setFilters(message, { | ||
* bassboost: true | ||
* }); | ||
*/ | ||
setFilters (guildID, newFilters) { | ||
setFilters (message, newFilters) { | ||
return new Promise((resolve, reject) => { | ||
// Get guild queue | ||
const queue = this.queues.find((g) => g.guildID === guildID) | ||
const queue = this.queues.find((g) => g.guildID === message.guild.id) | ||
if (!queue) return reject(new Error('Not playing')) | ||
@@ -148,3 +224,3 @@ Object.keys(newFilters).forEach((filterName) => { | ||
}) | ||
this._playYTDLStream(queue, true, false) | ||
this._playYTDLStream(queue, true) | ||
}) | ||
@@ -154,67 +230,61 @@ } | ||
/** | ||
* Resolve an array of tracks objects from a query string | ||
* @param {string} query The query | ||
* @param {boolean} allResults Whether all the results should be returned, or only the first one | ||
* @returns {Promise<Track[]>} | ||
* | ||
* @example | ||
* client.on('message', async (message) => { | ||
* | ||
* const args = message.content.slice(settings.prefix.length).trim().split(/ +/g); | ||
* const command = args.shift().toLowerCase(); | ||
* | ||
* if(command === 'play'){ | ||
* // Search for tracks | ||
* let tracks = await client.player.searchTracks(args[0]); | ||
* // Sends an embed with the 10 first songs | ||
* if(tracks.length > 10) tracks = tracks.substr(0, 10); | ||
* const embed = new Discord.MessageEmbed() | ||
* .setDescription(tracks.map((t, i) => `**${i+1} -** ${t.name}`).join("\n")) | ||
* .setFooter("Send the number of the track you want to play!"); | ||
* message.channel.send(embed); | ||
* // Wait for user answer | ||
* await message.channel.awaitMessages((m) => m.content > 0 && m.content < 10, { max: 1, time: 20000, errors: ["time"] }).then(async (answers) => { | ||
* let index = parseInt(answers.first().content, 10); | ||
* track = track[index-1]; | ||
* // Then play the song | ||
* client.player.play(message.member.voice.channel, track); | ||
* }); | ||
* } | ||
* | ||
* }); | ||
* Check whether there is a music played in the server | ||
* @param {Discord.Message} message | ||
*/ | ||
searchTracks (query, allResults = false) { | ||
return new Promise(async (resolve, reject) => { | ||
if (ytpl.validateURL(query)) { | ||
const playlistID = await ytpl.getPlaylistID(query).catch(() => {}) | ||
if (playlistID) { | ||
const playlist = await ytpl(playlistID).catch(() => {}) | ||
if (playlist) { | ||
return resolve(playlist.items.map((i) => new Track({ | ||
title: i.title, | ||
duration: i.duration, | ||
thumbnail: i.thumbnail, | ||
author: i.author, | ||
link: i.url, | ||
fromPlaylist: true | ||
}, null, null))) | ||
} | ||
} | ||
} | ||
const matchSpotifyURL = query.match(/https?:\/\/(?:embed\.|open\.)(?:spotify\.com\/)(?:track\/|\?uri=spotify:track:)((\w|-){22})/) | ||
if (matchSpotifyURL) { | ||
const spotifyData = await spotify.getPreview(query).catch(e => resolve([])) | ||
query = `${spotifyData.artist} - ${spotifyData.track}` | ||
} | ||
// eslint-disable-next-line no-useless-escape | ||
const matchYoutubeURL = query.match(/(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/) | ||
if (matchYoutubeURL) { | ||
query = matchYoutubeURL[1] | ||
} | ||
ytsr(query).then((results) => { | ||
if (results.items.length < 1) return resolve([]) | ||
const resultsVideo = results.items.filter((i) => i.type === 'video') | ||
resolve(allResults ? resultsVideo.map((r) => new Track(r, null, null)) : [new Track(resultsVideo[0], null, null)]) | ||
}).catch(() => { | ||
return resolve([]) | ||
isPlaying (message) { | ||
return this.queues.some((g) => g.guildID === message.guild.id) | ||
} | ||
/** | ||
* Add a track to the queue | ||
* @ignore | ||
* @param {Discord.Message} message | ||
* @param {Track} track | ||
* @returns {Queue} | ||
*/ | ||
_addTrackToQueue (message, track) { | ||
const queue = this.getQueue(message) | ||
if (!queue) throw new Error('NotPlaying') | ||
if (!track || !(track instanceof Track)) throw new Error('No track to add to the queue specified') | ||
queue.tracks.push(track) | ||
return queue | ||
} | ||
/** | ||
* Add multiple tracks to the queue | ||
* @ignore | ||
* @param {Discord.Message} message | ||
* @param {Track[]} tracks | ||
* @returns {Queue} | ||
*/ | ||
_addTracksToQueue (message, tracks) { | ||
const queue = this.getQueue(message) | ||
if (!queue) throw new Error('Cannot add tracks to queue because no song is currently played on the server.') | ||
queue.tracks.push(...tracks) | ||
return queue | ||
} | ||
/** | ||
* Create a new queue and play the first track | ||
* @ignore | ||
* @param {Discord.Message} message | ||
* @param {Track} track | ||
* @returns {Promise<Queue>} | ||
*/ | ||
_createQueue (message, track) { | ||
return new Promise((resolve, reject) => { | ||
const channel = message.member.voice ? message.member.voice.channel : null | ||
if (!channel) reject(new Error('NotConnected')) | ||
const queue = new Queue(message.guild.id, message, this.filters) | ||
this.queues.set(message.guild.id, queue) | ||
channel.join().then((connection) => { | ||
queue.voiceConnection = connection | ||
queue.tracks.push(track) | ||
this.emit('queueCreate', message, queue) | ||
resolve(queue) | ||
this._playTrack(queue, true) | ||
}).catch((err) => { | ||
console.error(err) | ||
this.queues.delete(message.guild.id) | ||
reject(new Error('UnableToJoin')) | ||
}) | ||
@@ -225,108 +295,68 @@ }) | ||
/** | ||
* Whether a guild is currently playing something | ||
* @param {Discord.Snowflake} guildID The guild ID to check | ||
* @returns {boolean} Whether the guild is currently playing tracks | ||
* Handle playlist by fetching the tracks and adding them to the queue | ||
* @ignore | ||
* @param {Discord.Message} message | ||
* @param {String} query | ||
*/ | ||
isPlaying (guildID) { | ||
return this.queues.some((g) => g.guildID === guildID) | ||
async _handlePlaylist (message, query) { | ||
const playlist = await ytpl(query).catch(() => {}) | ||
if (!playlist) return this.emit('noResults', message, query) | ||
playlist.tracks = playlist.items.map((item) => new Track(item, message.author)) | ||
playlist.duration = playlist.tracks.reduce((prev, next) => prev + next.duration, 0) | ||
playlist.thumbnail = playlist.tracks[0].thumbnail | ||
playlist.requestedBy = message.author | ||
if (this.isPlaying(message)) { | ||
const queue = this._addTracksToQueue(message, playlist.tracks) | ||
this.emit('playlistAdd', message, queue, playlist) | ||
} else { | ||
const track = new Track(playlist.tracks.shift(), message.author) | ||
const queue = await this._createQueue(message, track).catch((e) => this.emit('error', message, e)) | ||
this._addTracksToQueue(message, playlist.tracks) | ||
} | ||
} | ||
/** | ||
* Play a track in a voice channel | ||
* @param {Discord.VoiceChannel} voiceChannel The voice channel in which the track will be played | ||
* @param {Track|string} track The name of the track to play | ||
* @param {Discord.User?} user The user who requested the track | ||
* @returns {any} The played content | ||
* Play a track in the server. Supported query types are `keywords`, YouTube video links`, `YouTube playlists links`, Spotify track link` or `SoundCloud song link`. | ||
* @param {Discord.Message} message | ||
* @param {String} query | ||
* @returns {Promise<void>} | ||
* | ||
* @example | ||
* 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 "Despacito" in the member voice channel | ||
* | ||
* if(command === 'play'){ | ||
* const result = await client.player.play(message.member.voice.channel, args.join(" ")); | ||
* if(result.type === 'playlist'){ | ||
* message.channel.send(`${result.tracks.length} songs added to the queue!\nCurrently playing **${result.tracks[0].name}**...`); | ||
* } else { | ||
* message.channel.send(`Currently playing ${result.name}...`); | ||
* } | ||
* } | ||
* | ||
* }); | ||
* client.player.play(message, "Despacito"); | ||
*/ | ||
play (voiceChannel, track, user) { | ||
this.queues = this.queues.filter((g) => g.guildID !== voiceChannel.id) | ||
return new Promise(async (resolve, reject) => { | ||
if (!voiceChannel || typeof voiceChannel !== 'object') { | ||
return reject(new Error(`voiceChannel must be type of VoiceChannel. value=${voiceChannel}`)) | ||
async play (message, query) { | ||
const isPlaying = this.isPlaying(message) | ||
if (this.util.isYTPlaylistLink(query)) { | ||
return this._handlePlaylist(message, query) | ||
} | ||
let trackToPlay | ||
if (query instanceof Track) { | ||
trackToPlay = query | ||
} else if (this.util.isYTVideoLink(query)) { | ||
const videoData = await ytdl.getBasicInfo(query) | ||
trackToPlay = new Track(videoData, message.author) | ||
} else { | ||
trackToPlay = await this._searchTracks(message, query) | ||
} | ||
if (trackToPlay) { | ||
if (this.isPlaying(message)) { | ||
const queue = this._addTrackToQueue(message, trackToPlay) | ||
this.emit('trackAdd', message, queue, queue.tracks[queue.tracks.length - 1]) | ||
} else { | ||
const queue = await this._createQueue(message, trackToPlay) | ||
this.emit('trackStart', message, queue.tracks[0]) | ||
} | ||
const connection = voiceChannel.client.voice.connections.find((c) => c.channel.id === voiceChannel.id) || await voiceChannel.join() | ||
// Create a new guild with data | ||
const queue = new Queue(voiceChannel.guild.id) | ||
queue.voiceConnection = connection | ||
queue.filters = {} | ||
Object.keys(this.filters).forEach((f) => { | ||
queue.filters[f] = false | ||
}) | ||
let result = null | ||
if (typeof track === 'object') { | ||
track.requestedBy = user | ||
result = track | ||
// Add the track to the queue | ||
queue.tracks.push(track) | ||
} else if (typeof track === 'string') { | ||
const results = await this.searchTracks(track).catch(() => { | ||
return reject(new Error('Not found')) | ||
}) | ||
if (!results) return | ||
if (results.length > 1) { | ||
result = { | ||
type: 'playlist', | ||
tracks: results | ||
} | ||
} else if (results[0]) { | ||
result = results[0] | ||
} else { | ||
return reject(new Error('Not found')) | ||
} | ||
results.forEach((i) => { | ||
i.requestedBy = user | ||
queue.tracks.push(i) | ||
}) | ||
} | ||
// Add the queue to the list | ||
this.queues.push(queue) | ||
// Play the track | ||
this._playTrack(queue.guildID, true) | ||
// Resolve the track | ||
resolve(result) | ||
}) | ||
} | ||
} | ||
/** | ||
* Pause the current track | ||
* @param {Discord.Snowflake} guildID The ID of the guild where the current track should be paused | ||
* @returns {Promise<Track>} The paused track | ||
* | ||
* Pause the music in the server. | ||
* @param {Discord.Message} message | ||
* @example | ||
* client.on('message', async (message) => { | ||
* | ||
* const args = message.content.slice(settings.prefix.length).trim().split(/ +/g); | ||
* const command = args.shift().toLowerCase(); | ||
* | ||
* if(command === 'pause'){ | ||
* const track = await client.player.pause(message.guild.id); | ||
* message.channel.send(`${track.name} paused!`); | ||
* } | ||
* | ||
* }); | ||
* client.player.pause(message); | ||
*/ | ||
pause (guildID) { | ||
pause (message) { | ||
return new Promise((resolve, reject) => { | ||
// Get guild queue | ||
const queue = this.queues.find((g) => g.guildID === guildID) | ||
const queue = this.queues.find((g) => g.guildID === message.guild.id) | ||
if (!queue) return reject(new Error('Not playing')) | ||
@@ -342,127 +372,65 @@ // Pause the dispatcher | ||
/** | ||
* Resume the current track | ||
* @param {Discord.Snowflake} guildID The ID of the guild where the current track should be resumed | ||
* @returns {Promise<Track>} The resumed track | ||
* | ||
* Resume the music in the server. | ||
* @param {Discord.Message} message | ||
* @returns {Queue} | ||
* @example | ||
* client.on('message', async (message) => { | ||
* | ||
* const args = message.content.slice(settings.prefix.length).trim().split(/ +/g); | ||
* const command = args.shift().toLowerCase(); | ||
* | ||
* if(command === 'resume'){ | ||
* const track = await client.player.resume(message.guild.id); | ||
* message.channel.send(`${track.name} resumed!`); | ||
* } | ||
* | ||
* }); | ||
* client.player.resume(message); | ||
*/ | ||
resume (guildID) { | ||
return new Promise((resolve, reject) => { | ||
// Get guild queue | ||
const queue = this.queues.find((g) => g.guildID === guildID) | ||
if (!queue) return reject(new Error('Not playing')) | ||
// Pause the dispatcher | ||
queue.voiceConnection.dispatcher.resume() | ||
queue.paused = false | ||
// Resolve the guild queue | ||
resolve(queue.playing) | ||
}) | ||
resume (message) { | ||
// Get guild queue | ||
const queue = this.queues.find((g) => g.guildID === message.guild.id) | ||
if (!queue) return this.emit('error', message, 'NotPlaying') | ||
// Pause the dispatcher | ||
queue.voiceConnection.dispatcher.resume() | ||
queue.paused = false | ||
// Resolve the guild queue | ||
return queue | ||
} | ||
/** | ||
* Stop the music in the guild | ||
* @param {Discord.Snowflake} guildID The ID of the guild where the music should be stopped | ||
* @returns {Promise<void>} | ||
* | ||
* Stop the music in the server. | ||
* @param {Discord.Message} message | ||
* @example | ||
* client.on('message', (message) => { | ||
* | ||
* const args = message.content.slice(settings.prefix.length).trim().split(/ +/g); | ||
* const command = args.shift().toLowerCase(); | ||
* | ||
* if(command === 'stop'){ | ||
* client.player.stop(message.guild.id); | ||
* message.channel.send('Music stopped!'); | ||
* } | ||
* | ||
* }); | ||
* client.player.stop(message); | ||
*/ | ||
stop (guildID) { | ||
return new Promise((resolve, reject) => { | ||
// Get guild queue | ||
const queue = this.queues.find((g) => g.guildID === guildID) | ||
if (!queue) return reject(new Error('Not playing')) | ||
// Stop the dispatcher | ||
queue.stopped = true | ||
queue.tracks = [] | ||
if (queue.stream) queue.stream.destroy() | ||
queue.voiceConnection.dispatcher.end() | ||
// Resolve | ||
resolve() | ||
}) | ||
stop (message) { | ||
// Get guild queue | ||
const queue = this.queues.find((g) => g.guildID === message.guild.id) | ||
if (!queue) return this.emit('error', message, 'NotPlaying') | ||
// Stop the dispatcher | ||
queue.stopped = true | ||
queue.tracks = [] | ||
if (queue.stream) queue.stream.destroy() | ||
queue.voiceConnection.dispatcher.end() | ||
if (this.options.leaveOnStop) queue.voiceConnection.channel.leave() | ||
this.queues.delete(message.guild.id) | ||
} | ||
/** | ||
* Update the volume | ||
* @param {Discord.Snowflake} guildID The ID of the guild where the music should be modified | ||
* @param {number} percent The new volume (0-100) | ||
* @returns {Promise<void>} | ||
* | ||
* Change the server volume. | ||
* @param {Discord.Message} message | ||
* @param {number} percent | ||
* @returns {Queue} | ||
* @example | ||
* client.on('message', (message) => { | ||
* | ||
* const args = message.content.slice(settings.prefix.length).trim().split(/ +/g); | ||
* const command = args.shift().toLowerCase(); | ||
* | ||
* if(command === 'set-volume'){ | ||
* client.player.setVolume(message.guild.id, parseInt(args[0])); | ||
* message.channel.send(`Volume set to ${args[0]} !`); | ||
* } | ||
* | ||
* }); | ||
* client.player.setVolume(message, 90); | ||
*/ | ||
setVolume (guildID, percent) { | ||
return new Promise((resolve, reject) => { | ||
// Get guild queue | ||
const queue = this.queues.find((g) => g.guildID === guildID) | ||
if (!queue) return reject(new Error('Not playing')) | ||
// Update volume | ||
queue.volume = percent | ||
queue.voiceConnection.dispatcher.setVolumeLogarithmic(queue.calculatedVolume / 200) | ||
// Resolve guild queue | ||
resolve() | ||
}) | ||
setVolume (message, percent) { | ||
// Get guild queue | ||
const queue = this.queues.get(message.guild.id) | ||
if (!queue) return this.emit('error', message, 'NotPlaying') | ||
// Update volume | ||
queue.volume = percent | ||
queue.voiceConnection.dispatcher.setVolumeLogarithmic(queue.calculatedVolume / 200) | ||
// Return the queue | ||
return queue | ||
} | ||
/** | ||
* Get a guild queue | ||
* @param {Discord.Snowflake} guildID | ||
* @returns {?Queue} | ||
* | ||
* @example | ||
* client.on('message', (message) => { | ||
* | ||
* const args = message.content.slice(settings.prefix.length).trim().split(/ +/g); | ||
* const command = args.shift().toLowerCase(); | ||
* | ||
* if(command === 'queue'){ | ||
* const queue = await client.player.getQueue(message.guild.id); | ||
* message.channel.send('Server queue:\n'+(queue.tracks.map((track, i) => { | ||
* return `${i === 0 ? 'Current' : `#${i+1}`} - ${track.name} | ${track.author}`; | ||
* }).join('\n'))); | ||
* } | ||
* | ||
* // Output: | ||
* | ||
* // Server queue: | ||
* // Current - Despacito | Luis Fonsi | ||
* // #2 - Memories | Maroon 5 | ||
* // #3 - Dance Monkey | Tones And I | ||
* // #4 - Circles | Post Malone | ||
* }); | ||
* Get the server queue. | ||
* @param {Discord.Message} message | ||
* @returns {Queue} | ||
*/ | ||
getQueue (guildID) { | ||
getQueue (message) { | ||
// Gets guild queue | ||
const queue = this.queues.find((g) => g.guildID === guildID) | ||
const queue = this.queues.get(message.guild.id) | ||
return queue | ||
@@ -472,305 +440,119 @@ } | ||
/** | ||
* Add a track to the guild queue | ||
* @param {Discord.Snowflake} guildID The ID of the guild where the track should be added | ||
* @param {Track|string} trackName The name of the track to add to the queue | ||
* @param {Discord.User?} user The user who requested the track | ||
* @returns {any} The content added to the queue | ||
* | ||
* @example | ||
* client.on('message', async (message) => { | ||
* | ||
* const args = message.content.slice(settings.prefix.length).trim().split(/ +/g); | ||
* const command = args.shift().toLowerCase(); | ||
* | ||
* if(command === 'play'){ | ||
* let trackPlaying = client.player.isPlaying(message.guild.id); | ||
* // If there's already a track being played | ||
* if(trackPlaying){ | ||
* const result = await client.player.addToQueue(message.guild.id, args.join(" ")); | ||
* if(result.type === 'playlist'){ | ||
* message.channel.send(`${result.tracks.length} songs added to the queue!`); | ||
* } else { | ||
* message.channel.send(`${result.name} added to the queue!`); | ||
* } | ||
* } else { | ||
* // Else, play the track | ||
* const result = await client.player.addToQueue(message.member.voice.channel, args[0]); | ||
* if(result.type === 'playlist'){ | ||
* message.channel.send(`${result.tracks.length} songs added to the queue\nCurrently playing **${result.tracks[0].name}**!`); | ||
* } else { | ||
* message.channel.send(`Currently playing ${result.name}`); | ||
* } | ||
* } | ||
* } | ||
* | ||
* }); | ||
* Clears the server queue. | ||
* @param {Discord.Message} message | ||
* @returns {Queue} | ||
*/ | ||
addToQueue (guildID, track, user) { | ||
return new Promise(async (resolve, reject) => { | ||
// Get guild queue | ||
const queue = this.queues.find((g) => g.guildID === guildID) | ||
if (!queue) return reject(new Error('Not playing')) | ||
// Search the track | ||
let result = null | ||
if (typeof track === 'object') { | ||
track.requestedBy = user | ||
result = track | ||
// Add the track to the queue | ||
queue.tracks.push(track) | ||
} else if (typeof track === 'string') { | ||
const results = await this.searchTracks(track).catch(() => { | ||
return reject(new Error('Not found')) | ||
}) | ||
if (!results) return | ||
if (results.length > 1) { | ||
result = { | ||
type: 'playlist', | ||
tracks: results | ||
} | ||
} else if (results[0]) { | ||
result = results[0] | ||
} else { | ||
return reject(new Error('Not found')) | ||
} | ||
results.forEach((i) => { | ||
i.requestedBy = user | ||
queue.tracks.push(i) | ||
}) | ||
} | ||
// Resolve the result | ||
resolve(result) | ||
}) | ||
clearQueue (message) { | ||
// Get guild queue | ||
const queue = this.queues.get(message.guild.id) | ||
if (!queue) return this.emit('error', message, 'NotPlaying') | ||
// Clear queue | ||
queue.tracks = [] | ||
// Return the queue | ||
return queue | ||
} | ||
/** | ||
* Clear the guild queue, except the current track | ||
* @param {Discord.Snowflake} guildID The ID of the guild where the queue should be cleared | ||
* @returns {Promise<Queue>} The updated queue | ||
* | ||
* @example | ||
* client.on('message', (message) => { | ||
* | ||
* const args = message.content.slice(settings.prefix.length).trim().split(/ +/g); | ||
* const command = args.shift().toLowerCase(); | ||
* | ||
* if(command === 'clear-queue'){ | ||
* client.player.clearQueue(message.guild.id); | ||
* message.channel.send('Queue cleared!'); | ||
* } | ||
* | ||
* }); | ||
* Skips to the next song. | ||
* @param {Discord.Message} message | ||
* @returns {Queue} | ||
*/ | ||
clearQueue (guildID) { | ||
return new Promise((resolve, reject) => { | ||
// Get guild queue | ||
const queue = this.queues.find((g) => g.guildID === guildID) | ||
if (!queue) return reject(new Error('Not playing')) | ||
// Clear queue | ||
queue.tracks = [] | ||
// Resolve guild queue | ||
resolve(queue) | ||
}) | ||
skip (message) { | ||
// Get guild queue | ||
const queue = this.queues.get(message.guild.id) | ||
if (!queue) return this.emit('error', message, 'NotPlaying') | ||
const currentTrack = queue.playing | ||
// End the dispatcher | ||
queue.voiceConnection.dispatcher.end() | ||
queue.lastSkipped = true | ||
// Return the queue | ||
return queue | ||
} | ||
/** | ||
* Skip a track | ||
* @param {Discord.Snowflake} guildID The ID of the guild where the track should be skipped | ||
* @returns {Promise<Track>} | ||
* | ||
* @example | ||
* client.on('message', async (message) => { | ||
* | ||
* const args = message.content.slice(settings.prefix.length).trim().split(/ +/g); | ||
* const command = args.shift().toLowerCase(); | ||
* | ||
* if(command === 'skip'){ | ||
* const track = await client.player.skip(message.guild.id); | ||
* message.channel.send(`${track.name} skipped!`); | ||
* } | ||
* | ||
* }); | ||
* Get the played song in the server. | ||
* @param {Discord.Message} message | ||
* @returns {Track} | ||
*/ | ||
skip (guildID) { | ||
return new Promise((resolve, reject) => { | ||
// Get guild queue | ||
const queue = this.queues.find((g) => g.guildID === guildID) | ||
if (!queue) return reject(new Error('Not playing')) | ||
const currentTrack = queue.playing | ||
// End the dispatcher | ||
queue.voiceConnection.dispatcher.end() | ||
queue.lastSkipped = true | ||
// Resolve the current track | ||
resolve(currentTrack) | ||
}) | ||
nowPlaying (message) { | ||
// Get guild queue | ||
const queue = this.queues.get(message.guild.id) | ||
if (!queue) return this.emit('error', message, 'NotPlaying') | ||
const currentTrack = queue.tracks[0] | ||
// Return the current track | ||
return currentTrack | ||
} | ||
/** | ||
* Get the currently playing track | ||
* @param {Discord.Snowflake} guildID | ||
* @returns {Promise<Track>} The track which is currently played | ||
* | ||
* @example | ||
* client.on('message', async (message) => { | ||
* | ||
* const args = message.content.slice(settings.prefix.length).trim().split(/ +/g); | ||
* const command = args.shift().toLowerCase(); | ||
* | ||
* if(command === 'now-playing'){ | ||
* let track = await client.player.nowPlaying(message.guild.id); | ||
* message.channel.send(`Currently playing ${track.name}...`); | ||
* } | ||
* | ||
* }); | ||
* Enable or disable repeat mode in the server. | ||
* @param {Discord.Message} message | ||
* @param {boolean} enabled | ||
* @returns {boolean} whether the repeat mode is now enabled. | ||
*/ | ||
nowPlaying (guildID) { | ||
return new Promise((resolve, reject) => { | ||
// Get guild queue | ||
const queue = this.queues.find((g) => g.guildID === guildID) | ||
if (!queue) return reject(new Error('Not playing')) | ||
const currentTrack = queue.playing | ||
// Resolve the current track | ||
resolve(currentTrack) | ||
}) | ||
setRepeatMode (message, enabled) { | ||
// Get guild queue | ||
const queue = this.queues.get(message.guild.id) | ||
if (!queue) return this.emit('error', message, 'NotPlaying') | ||
// Enable/Disable repeat mode | ||
queue.repeatMode = enabled | ||
// Return the repeat mode | ||
return queue.repeatMode | ||
} | ||
/** | ||
* Enable or disable the repeat mode | ||
* @param {Discord.Snowflake} guildID | ||
* @param {Boolean} enabled Whether the repeat mode should be enabled | ||
* @returns {Promise<Void>} | ||
* | ||
* @example | ||
* client.on('message', async (message) => { | ||
* | ||
* const args = message.content.slice(settings.prefix.length).trim().split(/ +/g); | ||
* const command = args.shift().toLowerCase(); | ||
* | ||
* if(command === 'repeat-mode'){ | ||
* const repeatModeEnabled = client.player.getQueue(message.guild.id).repeatMode; | ||
* if(repeatModeEnabled){ | ||
* // if the repeat mode is currently enabled, disable it | ||
* client.player.setRepeatMode(message.guild.id, false); | ||
* message.channel.send("Repeat mode disabled! The current song will no longer be played again and again..."); | ||
* } else { | ||
* // if the repeat mode is currently disabled, enable it | ||
* client.player.setRepeatMode(message.guild.id, true); | ||
* message.channel.send("Repeat mode enabled! The current song will be played again and again until you run the command again!"); | ||
* } | ||
* } | ||
* | ||
* }); | ||
* Shuffle the queue of the server. | ||
* @param {Discord.Message} message | ||
* @returns {} | ||
*/ | ||
setRepeatMode (guildID, enabled) { | ||
return new Promise((resolve, reject) => { | ||
// Get guild queue | ||
const queue = this.queues.find((g) => g.guildID === guildID) | ||
if (!queue) return reject(new Error('Not playing')) | ||
// Enable/Disable repeat mode | ||
queue.repeatMode = enabled | ||
// Resolve | ||
resolve() | ||
}) | ||
shuffle (message) { | ||
// Get guild queue | ||
const queue = this.queues.get(message.guild.id) | ||
if (!queue) return this.emit('error', message, 'NotPlaying') | ||
// Shuffle the queue (except the first track) | ||
const currentTrack = queue.tracks.shift() | ||
queue.tracks = queue.tracks.sort(() => Math.random() - 0.5) | ||
queue.tracks.unshift(currentTrack) | ||
// Return the queue | ||
return queue | ||
} | ||
/** | ||
* Shuffle the guild queue (except the first track) | ||
* @param {Discord.Snowflake} guildID The ID of the guild where the queue should be shuffled | ||
* @returns {Promise<Queue>} The updated queue | ||
* | ||
* @example | ||
* client.on('message', async (message) => { | ||
* | ||
* const args = message.content.slice(settings.prefix.length).trim().split(/ +/g); | ||
* const command = args.shift().toLowerCase(); | ||
* | ||
* if(command === 'shuffle'){ | ||
* // Shuffle the server queue | ||
* client.player.shuffle(message.guild.id).then(() => { | ||
* message.channel.send('Queue shuffled!'); | ||
* }); | ||
* } | ||
* | ||
* }); | ||
* Remove a track from the queue of the server | ||
* @param {Discord.Message} message | ||
* @param {Track|number} track | ||
* @returns {Track} the removed track | ||
*/ | ||
shuffle (guildID) { | ||
return new Promise((resolve, reject) => { | ||
// Get guild queue | ||
const queue = this.queues.find((g) => g.guildID === guildID) | ||
if (!queue) return reject(new Error('Not playing')) | ||
// Shuffle the queue (except the first track) | ||
const currentTrack = queue.tracks.shift() | ||
queue.tracks = queue.tracks.sort(() => Math.random() - 0.5) | ||
queue.tracks.unshift(currentTrack) | ||
// Resolve | ||
resolve(queue) | ||
}) | ||
} | ||
/** | ||
* Remove a track from the queue | ||
* @param {Discord.Snowflake} guildID The ID of the guild where the track should be removed | ||
* @param {number|Track} track The index of the track to remove or the track to remove object | ||
* @returns {Promise<Track|null>} | ||
* | ||
* @example | ||
* client.on('message', async (message) => { | ||
* | ||
* const args = message.content.slice(settings.prefix.length).trim().split(/ +/g); | ||
* const command = args.shift().toLowerCase(); | ||
* | ||
* if(command === 'remove'){ | ||
* // Remove a track from the queue | ||
* client.player.remove(message.guild.id, args[0]).then(() => { | ||
* message.channel.send('Removed track!'); | ||
* }); | ||
* } | ||
* | ||
* }); | ||
*/ | ||
remove (guildID, track) { | ||
return new Promise((resolve, reject) => { | ||
// Gets guild queue | ||
const queue = this.queues.find((g) => g.guildID === guildID) | ||
if (!queue) return reject(new Error('Not playing')) | ||
// Remove the track from the queue | ||
let trackFound = null | ||
if (typeof track === 'number') { | ||
trackFound = queue.tracks[track] | ||
if (trackFound) { | ||
queue.tracks = queue.tracks.filter((t) => t !== trackFound) | ||
} | ||
} else { | ||
trackFound = queue.tracks.find((s) => s === track) | ||
if (trackFound) { | ||
queue.tracks = queue.tracks.filter((s) => s !== trackFound) | ||
} | ||
remove (message, track) { | ||
// Get guild queue | ||
const queue = this.queues.get(message.guild.id) | ||
if (!queue) return this.emit('error', message, 'NotPlaying') | ||
// Remove the track from the queue | ||
let trackFound = null | ||
if (typeof track === 'number') { | ||
trackFound = queue.tracks[track] | ||
if (trackFound) { | ||
queue.tracks = queue.tracks.filter((t) => t !== trackFound) | ||
} | ||
// Resolve | ||
resolve(trackFound) | ||
}) | ||
} else { | ||
trackFound = queue.tracks.find((s) => s === track) | ||
if (trackFound) { | ||
queue.tracks = queue.tracks.filter((s) => s !== trackFound) | ||
} | ||
} | ||
// Resolve | ||
return trackFound | ||
} | ||
/** | ||
* Creates progress bar of the current song | ||
* @param {Discord.Snowflake} guildID | ||
* @returns {String} | ||
* | ||
* @example | ||
* client.on('message', async (message) => { | ||
* | ||
* const args = message.content.slice(settings.prefix.length).trim().split(/ +/g); | ||
* const command = args.shift().toLowerCase(); | ||
* | ||
* if(command === 'now-playing'){ | ||
* client.player.nowPlaying(message.guild.id).then((song) => { | ||
* message.channel.send('Currently playing ' + song.name + '\n\n'+ client.player.createProgressBar(message.guild.id)); | ||
* }); | ||
* } | ||
* | ||
* }); | ||
* Create a progress bar for the queue of the server. | ||
* @param {Discord.Message} message | ||
* @param {Object} options | ||
* @param {boolean} options.timecodes | ||
* @returns {string} | ||
*/ | ||
createProgressBar (guildID) { | ||
createProgressBar (message, options) { | ||
// Gets guild queue | ||
const queue = this.queues.find((g) => g.guildID === guildID) | ||
const queue = this.queues.get(message.guild.id) | ||
if (!queue) return | ||
const timecodes = options && typeof options === 'object' ? options.timecodes : false | ||
// Stream time of the dispatcher | ||
@@ -788,5 +570,15 @@ const currentStreamTime = queue.voiceConnection.dispatcher | ||
bar.splice(index, 0, '🔘') | ||
return bar.join('') | ||
if (timecodes) { | ||
const currentTimecode = (currentStreamTime >= 3600000 ? moment(currentStreamTime).format('H:mm:ss') : moment(currentStreamTime).format('m:ss')) | ||
return `${currentTimecode} ┃ ${bar.join('')} ┃ ${queue.playing.duration}` | ||
} else { | ||
return `${bar.join('')}` | ||
} | ||
} else { | ||
return '🔘▬▬▬▬▬▬▬▬▬▬▬▬▬▬' | ||
if (timecodes) { | ||
const currentTimecode = (currentStreamTime >= 3600000 ? moment(currentStreamTime).format('H:mm:ss') : moment(currentStreamTime).format('m:ss')) | ||
return `${currentTimecode} ┃ 🔘▬▬▬▬▬▬▬▬▬▬▬▬▬▬ ┃ ${queue.playing.duration}` | ||
} else { | ||
return '🔘▬▬▬▬▬▬▬▬▬▬▬▬▬▬' | ||
} | ||
} | ||
@@ -796,5 +588,3 @@ } | ||
/** | ||
* Handle the voice state update event | ||
* @ignore | ||
* @private | ||
* Handle voiceStateUpdate event. | ||
* @param {Discord.VoiceState} oldState | ||
@@ -804,27 +594,32 @@ * @param {Discord.VoiceState} newState | ||
_handleVoiceStateUpdate (oldState, newState) { | ||
// Search for a queue for this channel | ||
const queue = this.queues.find((g) => g.guildID === oldState.guild.id) | ||
if (!queue) return | ||
// if the bot has been kicked from the channel, destroy ytdl stream and remove the queue | ||
if (newState.member.id === this.client.user.id && !newState.channelID) { | ||
queue.stream.destroy() | ||
this.queues.delete(newState.guild.id) | ||
this.emit('botDisconnect', queue.firstMessage) | ||
} | ||
// process leaveOnEmpty checks | ||
if (!this.options.leaveOnEmpty) return | ||
// If the member leaves a voice channel | ||
if (!oldState.channelID || newState.channelID) return | ||
// Search for a queue for this channel | ||
const queue = this.queues.find((g) => g.voiceConnection.channel.id === oldState.channelID) | ||
if (queue) { | ||
// If the channel is not empty | ||
if (queue.voiceConnection.channel.members.size > 1) return | ||
// If the channel is not empty | ||
if (!this.util.isVoiceEmpty(queue.voiceConnection.channel)) return | ||
setTimeout(() => { | ||
if (!this.util.isVoiceEmpty(queue.voiceConnection.channel)) return | ||
if (!this.queues.has(queue.guildID)) return | ||
// Disconnect from the voice channel | ||
queue.voiceConnection.channel.leave() | ||
// Delete the queue | ||
this.queues = this.queues.filter((g) => g.guildID !== queue.guildID) | ||
this.queues.delete(queue.guildID) | ||
// Emit end event | ||
queue.emit('channelEmpty') | ||
} | ||
queue.emit('channelEmpty', queue.firstMessage, queue) | ||
}, this.options.leaveOnEmptyCooldown ?? 0) | ||
} | ||
/** | ||
* Play a stream in a channel | ||
* @ignore | ||
* @private | ||
* @param {Queue} queue The queue to play | ||
* @param {Boolean} updateFilter Whether this method is called to update some ffmpeg filters | ||
* @returns {Promise<void>} | ||
*/ | ||
_playYTDLStream (queue, updateFilter) { | ||
@@ -872,3 +667,3 @@ return new Promise((resolve) => { | ||
// Play the next track | ||
return this._playTrack(queue.guildID, false) | ||
return this._playTrack(queue, false) | ||
}) | ||
@@ -880,34 +675,31 @@ }, 1000) | ||
/** | ||
* Start playing a track in a guild | ||
* @ignore | ||
* @private | ||
* @param {Discord.Snowflake} guildID | ||
* @param {Boolean} firstPlay Whether the function was called from the play() one | ||
* | ||
* @param {Queue} queue The queue to play. | ||
* @param {*} firstPlay | ||
*/ | ||
async _playTrack (guildID, firstPlay) { | ||
// Get guild queue | ||
const queue = this.queues.find((g) => g.guildID === guildID) | ||
// If there isn't any music in the queue | ||
if (queue.tracks.length < 1 && !firstPlay && !queue.repeatMode) { | ||
async _playTrack (queue, firstPlay) { | ||
if (this.options.leaveOnEmpty && this.util.isVoiceEmpty(queue.voiceConnection.channel)) { | ||
} | ||
if (queue.stopped) return | ||
// If there isn't next music in the queue | ||
if (queue.tracks.length === 1 && !queue.repeatMode && !firstPlay) { | ||
// Leave the voice channel | ||
if (this.options.leaveOnEnd && !queue.stopped) queue.voiceConnection.channel.leave() | ||
// Remove the guild from the guilds list | ||
this.queues = this.queues.filter((g) => g.guildID !== guildID) | ||
this.queues.delete(queue.guildID) | ||
// Emit stop event | ||
if (queue.stopped) { | ||
if (this.options.leaveOnStop) queue.voiceConnection.channel.leave() | ||
return queue.emit('stop') | ||
return queue.emit('musicStop') | ||
} | ||
// Emit end event | ||
return queue.emit('end') | ||
return queue.emit('queueEnd', queue.firstMessage, queue) | ||
} | ||
const wasPlaying = queue.playing | ||
const nowPlaying = queue.playing = queue.repeatMode ? wasPlaying : queue.tracks.shift() | ||
// if the track needs to be the next one | ||
if (!queue.repeatMode && !firstPlay) queue.tracks.shift() | ||
const track = queue.playing | ||
// Reset lastSkipped state | ||
queue.lastSkipped = false | ||
this._playYTDLStream(queue, false).then(() => { | ||
// Emit trackChanged event | ||
if (!firstPlay) { | ||
queue.emit('trackChanged', wasPlaying, nowPlaying, queue.lastSkipped, queue.repeatMode) | ||
} | ||
if (!firstPlay) this.emit('trackStart', queue.firstMessage, track, queue) | ||
}) | ||
@@ -918,1 +710,94 @@ } | ||
module.exports = Player | ||
/** | ||
* Emitted when a track starts | ||
* @event Player#trackStart | ||
* @param {Discord.Message} message | ||
* @param {Queue} queue | ||
* @param {Track} track | ||
*/ | ||
/** | ||
* Emitted when a playlist is started | ||
* @event Player#queueCreate | ||
* @param {Discord.Message} message | ||
* @param {Queue} queue | ||
* @param {Object} playlist | ||
* @param {Track} track | ||
*/ | ||
/** | ||
* Emitted when the bot is awaiting search results | ||
* @event Player#searchResults | ||
* @param {Discord.Message} message | ||
* @param {string} query | ||
* @param {Track[]} tracks | ||
*/ | ||
/** | ||
* Emitted when the user has sent an invalid response for search results | ||
* @event Player#searchInvalidResponse | ||
* @param {Discord.Message} message | ||
* @param {string} query | ||
* @param {Track[]} tracks | ||
* @param {string} invalidResponse | ||
* @param {Discord.MessageCollector} collector | ||
*/ | ||
/** | ||
* Emitted when the bot has stopped awaiting search results (timeout) | ||
* @event Player#searchCancel | ||
* @param {Discord.Message} message | ||
* @param {string} query | ||
* @param {Track[]} tracks | ||
*/ | ||
/** | ||
* Emitted when the bot can't find related results to the query | ||
* @event Player#noResults | ||
* @param {Discord.Message} message | ||
* @param {string} query | ||
*/ | ||
/** | ||
* Emitted when the bot is disconnected from the channel | ||
* @event Player#botDisconnect | ||
* @param {Discord.Message} message | ||
*/ | ||
/** | ||
* Emitted when the channel of the bot is empty | ||
* @event Player#channelEmpty | ||
* @param {Discord.Message} message | ||
* @param {Queue} queue | ||
*/ | ||
/** | ||
* Emitted when the queue of the server is ended | ||
* @event Player#queueEnd | ||
* @param {Discord.Message} message | ||
* @param {Queue} queue | ||
*/ | ||
/** | ||
* Emitted when a track is added to the queue | ||
* @event Player#trackAdd | ||
* @param {Discord.Message} message | ||
* @param {Queue} queue | ||
* @param {Track} track | ||
*/ | ||
/** | ||
* Emitted when a playlist is added to the queue | ||
* @event Player#playlistAdd | ||
* @param {Discord.Message} message | ||
* @param {Queue} queue | ||
* @param {Object} playlist | ||
*/ | ||
/** | ||
* Emitted when an error is triggered | ||
* @event Player#error | ||
* @param {Discord.Message} message | ||
* @param {string} error It can be `NotConnected`, `UnableToJoin` or `NotPlaying`. | ||
*/ |
const Discord = require('discord.js') | ||
const { EventEmitter } = require('events') | ||
const Track = require('./Track') | ||
const Player = require('./Player') | ||
const { Stream } = require('stream') | ||
@@ -11,4 +13,6 @@ /** | ||
* @param {Discord.Snowflake} guildID ID of the guild this queue is for. | ||
* @param {Discord.Message} message Message that initialized the queue | ||
* @param {import('./Player').Filters[]} filters Filters the queue should be initialized with. | ||
*/ | ||
constructor (guildID) { | ||
constructor (guildID, message, filters) { | ||
super() | ||
@@ -26,6 +30,6 @@ /** | ||
/** | ||
* The song currently played. | ||
* @type {Track} | ||
* The ytdl stream. | ||
* @type {any} | ||
*/ | ||
this.playing = null | ||
this.stream = null | ||
/** | ||
@@ -66,2 +70,5 @@ * The tracks of this queue. The first one is currenlty playing and the others are going to be played. | ||
this.filters = {} | ||
Object.keys(filters).forEach((f) => { | ||
this.filters[f] = false | ||
}) | ||
/** | ||
@@ -72,4 +79,13 @@ * Additional stream time | ||
this.additionalStreamTime = 0 | ||
/** | ||
* Message that initialized the queue | ||
* @type {Discord.Message} | ||
*/ | ||
this.firstMessage = message | ||
} | ||
get playing () { | ||
return this.tracks[0] | ||
} | ||
get calculatedVolume () { | ||
@@ -81,59 +97,1 @@ return this.filters.bassboost ? this.volume + 50 : this.volume | ||
module.exports = Queue | ||
/** | ||
* Emitted when the queue is empty. | ||
* @event Queue#end | ||
* | ||
* @example | ||
* client.on('message', (message) => { | ||
* | ||
* const args = message.content.slice(settings.prefix.length).trim().split(/ +/g); | ||
* const command = args.shift().toLowerCase(); | ||
* | ||
* if(command === 'play'){ | ||
* | ||
* let track = await client.player.play(message.member.voice.channel, args[0]); | ||
* | ||
* track.queue.on('end', () => { | ||
* message.channel.send('The queue is empty, please add new tracks!'); | ||
* }); | ||
* | ||
* } | ||
* | ||
* }); | ||
*/ | ||
/** | ||
* Emitted when the voice channel is empty. | ||
* @event Queue#channelEmpty | ||
*/ | ||
/** | ||
* Emitted when the track changes. | ||
* @event Queue#trackChanged | ||
* @param {Track} oldTrack The old track (playing before) | ||
* @param {Track} newTrack The new track (currently playing) | ||
* @param {Boolean} skipped Whether the change is due to the skip() function | ||
* | ||
* @example | ||
* client.on('message', (message) => { | ||
* | ||
* const args = message.content.slice(settings.prefix.length).trim().split(/ +/g); | ||
* const command = args.shift().toLowerCase(); | ||
* | ||
* if(command === 'play'){ | ||
* | ||
* let track = await client.player.play(message.member.voice.channel, args[0]); | ||
* | ||
* track.queue.on('trackChanged', (oldTrack, newTrack, skipped, repeatMode) => { | ||
* if(repeatMode){ | ||
* message.channel.send(`Playing ${newTrack} again...`); | ||
* } else { | ||
* message.channel.send(`Now playing ${newTrack}...`); | ||
* } | ||
* }); | ||
* | ||
* } | ||
* | ||
* }); | ||
*/ |
const Discord = require('discord.js') | ||
const Queue = require('./Queue') | ||
const Player = require('./Player') | ||
@@ -10,11 +11,16 @@ /** | ||
* @param {Object} videoData The video data for this track | ||
* @param {Discord.User?} user The user who requested the track | ||
* @param {Queue?} queue The queue in which is the track is | ||
* @param {Discord.User | null} user The user who requested the track | ||
* @param {Player} player | ||
*/ | ||
constructor (videoData, user, queue) { | ||
constructor (videoData, user, player) { | ||
/** | ||
* The track name | ||
* The player instantiating the track | ||
* @type {Player} | ||
*/ | ||
this.player = player | ||
/** | ||
* The track title | ||
* @type {string} | ||
*/ | ||
this.name = videoData.title | ||
this.title = videoData.title | ||
/** | ||
@@ -24,3 +30,3 @@ * The Youtube URL of the track | ||
*/ | ||
this.url = videoData.link | ||
this.url = videoData.link ?? videoData.url | ||
/** | ||
@@ -61,10 +67,13 @@ * The video duration (formatted). | ||
this.fromPlaylist = videoData.fromPlaylist || false | ||
/** | ||
* The queue in which the track is | ||
* @type {Queue} | ||
*/ | ||
this.queue = queue | ||
} | ||
/** | ||
* The queue in which the track is | ||
* @type {Queue} | ||
*/ | ||
get queue () { | ||
return this.player.queues.find((queue) => queue.tracks.includes(this)) | ||
} | ||
/** | ||
* The track duration | ||
@@ -71,0 +80,0 @@ * @type {number} |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
15
163
45450
8
996
1
1
+ Addedmoment@^2.27.0
+ Addedsoundcloud-scraper@^2.0.0
+ Added@tootallnate/once@1.1.2(transitive)
+ Addedabab@2.0.6(transitive)
+ Addedacorn@7.4.18.14.0(transitive)
+ Addedacorn-globals@6.0.0(transitive)
+ Addedacorn-walk@7.2.0(transitive)
+ Addedagent-base@6.0.2(transitive)
+ Addedasynckit@0.4.0(transitive)
+ Addedbrowser-process-hrtime@1.0.0(transitive)
+ Addedcombined-stream@1.0.8(transitive)
+ Addedcssom@0.3.80.4.4(transitive)
+ Addedcssstyle@2.3.0(transitive)
+ Addeddata-urls@2.0.0(transitive)
+ Addeddebug@4.3.7(transitive)
+ Addeddecimal.js@10.4.3(transitive)
+ Addeddelayed-stream@1.0.0(transitive)
+ Addeddomexception@2.0.1(transitive)
+ Addedescodegen@2.1.0(transitive)
+ Addedesprima@4.0.1(transitive)
+ Addedestraverse@5.3.0(transitive)
+ Addedesutils@2.0.3(transitive)
+ Addedform-data@3.0.2(transitive)
+ Addedhtml-encoding-sniffer@2.0.1(transitive)
+ Addedhttp-proxy-agent@4.0.1(transitive)
+ Addedhttps-proxy-agent@5.0.1(transitive)
+ Addediconv-lite@0.4.24(transitive)
+ Addedis-potential-custom-element-name@1.0.1(transitive)
+ Addedjsdom@16.7.0(transitive)
+ Addedlodash@4.17.21(transitive)
+ Addedmime-db@1.52.0(transitive)
+ Addedmime-types@2.1.35(transitive)
+ Addedmoment@2.30.1(transitive)
+ Addedms@2.1.3(transitive)
+ Addednwsapi@2.2.13(transitive)
+ Addedparse5@6.0.1(transitive)
+ Addedpsl@1.13.0(transitive)
+ Addedpunycode@2.3.1(transitive)
+ Addedquerystringify@2.2.0(transitive)
+ Addedrequires-port@1.0.0(transitive)
+ Addedsafer-buffer@2.1.2(transitive)
+ Addedsaxes@5.0.1(transitive)
+ Addedsoundcloud-scraper@2.0.0(transitive)
+ Addedsource-map@0.6.1(transitive)
+ Addedsymbol-tree@3.2.4(transitive)
+ Addedtough-cookie@4.1.4(transitive)
+ Addedtr46@2.1.0(transitive)
+ Addeduniversalify@0.2.0(transitive)
+ Addedurl-parse@1.5.10(transitive)
+ Addedw3c-hr-time@1.0.2(transitive)
+ Addedw3c-xmlserializer@2.0.0(transitive)
+ Addedwebidl-conversions@5.0.06.1.0(transitive)
+ Addedwhatwg-encoding@1.0.5(transitive)
+ Addedwhatwg-mimetype@2.3.0(transitive)
+ Addedwhatwg-url@8.7.0(transitive)
+ Addedws@7.5.10(transitive)
+ Addedxml-name-validator@3.0.0(transitive)
+ Addedxmlchars@2.2.0(transitive)
Updatedytpl@^0.2.4