2020-01-12 00:01:18 +05:00
const ytdl = require ( 'ytdl-core' ) ;
const SYA = require ( 'simple-youtube-api' ) ;
const mergeOptions = require ( 'merge-options' ) ;
2020-02-21 15:32:16 +05:00
const { VoiceChannel , version } = require ( "discord.js" ) ;
if ( version . split ( '.' ) [ 0 ] !== '12' ) throw new Error ( "Only the master branch of discord.js library is supported for now. Install it using 'npm install discordjs/discord.js'." ) ;
2020-01-12 00:01:18 +05:00
const Queue = require ( './Queue' ) ;
const Util = require ( './Util' ) ;
const Song = require ( './Song' ) ;
/ * *
* Player options .
* @ typedef { PlayerOptions }
*
* @ property { Boolean } leaveOnEnd Whether the bot should leave the current voice channel when the queue ends .
* @ property { Boolean } leaveOnStop Whether the bot should leave the current voice channel when the stop ( ) function is used .
2020-01-12 22:55:37 +05:00
* @ property { Boolean } leaveOnEmpty Whether the bot should leave the voice channel if there is no more member in it .
2020-01-12 00:01:18 +05:00
* /
const PlayerOptions = {
leaveOnEnd : true ,
2020-01-12 22:55:37 +05:00
leaveOnStop : true ,
leaveOnEmpty : true
2020-01-12 00:01:18 +05:00
} ;
class Player {
/ * *
* @ param { Client } client Your Discord Client instance .
* @ param { string } youtubeToken Your Youtube Data v3 API key .
* @ param { PlayerOptions } options The PlayerOptions object .
* /
constructor ( client , youtubeToken , options = { } ) {
if ( ! client ) throw new SyntaxError ( 'Invalid Discord client' ) ;
if ( ! youtubeToken ) throw new SyntaxError ( 'Invalid Token: Token must be a String' ) ;
/ * *
* Your Discord Client instance .
* @ type { Client }
* /
this . client = client ;
/ * *
* Your Youtube Data v3 API key .
* @ type { string }
* /
this . youtubeToken = youtubeToken ;
/ * *
* The Simple Youtube API Client .
* @ type { Youtube }
* /
this . SYA = new SYA ( this . youtubeToken ) ;
/ * *
* The guilds data .
* @ type { Queue [ ] }
* /
this . queues = [ ] ;
/ * *
* Player options .
* @ type { PlayerOptions }
* /
this . options = mergeOptions ( PlayerOptions , options ) ;
2020-01-12 22:55:37 +05:00
// Listener to check if the channel is empty
2020-01-12 23:06:44 +05:00
client . on ( 'voiceStateUpdate' , ( oldState , newState ) => {
2020-01-12 22:55:37 +05:00
if ( ! this . options . leaveOnEmpty ) return ;
// If the member leaves a voice channel
2020-01-13 00:14:26 +05:00
if ( ! oldState . channelID || newState . channelID ) return ;
2020-01-12 22:55:37 +05:00
// Search for a queue for this channel
2020-01-13 00:14:26 +05:00
let queue = this . queues . find ( ( g ) => g . connection . channel . id === oldState . channelID ) ;
2020-01-12 22:55:37 +05:00
if ( queue ) {
2020-03-08 18:34:30 +05:00
// If the channel is not empty
if ( queue . connection . channel . members . size > 1 ) return ;
2020-01-12 22:55:37 +05:00
// Disconnect from the voice channel
queue . connection . channel . leave ( ) ;
// Delete the queue
this . queues = this . queues . filter ( ( g ) => g . guildID !== queue . guildID ) ;
// Emit end event
2020-01-13 00:14:26 +05:00
queue . emit ( 'channelEmpty' ) ;
2020-01-12 22:55:37 +05:00
}
} ) ;
2020-01-12 00:01:18 +05:00
}
/ * *
* Whether a guild is currently playing songs
* @ param { string } guildID The guild ID to check
* @ returns { Boolean } Whether the guild is currently playing songs
* /
isPlaying ( guildID ) {
return this . queues . some ( ( g ) => g . guildID === guildID ) ;
}
/ * *
* Plays a song in a voice channel .
* @ param { voiceChannel } voiceChannel The voice channel in which the song will be played .
* @ param { string } songName The name of the song to play .
* @ returns { Promise < Song > }
* /
play ( voiceChannel , songName ) {
this . queues = this . queues . filter ( ( g ) => g . guildID !== voiceChannel . id ) ;
return new Promise ( async ( resolve , reject ) => {
2020-02-22 15:19:00 +05:00
if ( ! voiceChannel || typeof voiceChannel !== "object" ) return reject ( "voiceChannel must be type of VoiceChannel. value=" + voiceChannel ) ;
2020-02-21 15:32:16 +05:00
if ( typeof songName !== "string" ) return reject ( "songName must be type of string. value=" + songName ) ;
2020-01-12 00:01:18 +05:00
// Searches the song
2020-01-12 21:13:57 +05:00
let video = await Util . getFirstYoutubeResult ( songName , this . SYA ) ;
2020-02-21 15:32:16 +05:00
if ( ! video ) return reject ( 'Song not found' ) ;
2020-01-13 00:05:33 +05:00
// Joins the voice channel
let connection = await voiceChannel . join ( ) ;
2020-01-12 00:01:18 +05:00
// Creates a new guild with data
let queue = new Queue ( voiceChannel . guild . id ) ;
2020-01-13 00:05:33 +05:00
queue . connection = connection ;
2020-01-12 00:01:18 +05:00
let song = new Song ( video , queue ) ;
queue . songs . push ( song ) ;
// Add the queue to the list
this . queues . push ( queue ) ;
// Plays the song
this . _playSong ( queue . guildID , true ) ;
// Resolves the song.
resolve ( song ) ;
} ) ;
}
/ * *
* Pauses the current song .
* @ param { string } guildID
* @ returns { Promise < Song > }
* /
pause ( guildID ) {
return new Promise ( async ( resolve , reject ) => {
// Gets guild queue
let queue = this . queues . find ( ( g ) => g . guildID === guildID ) ;
2020-02-21 15:32:16 +05:00
if ( ! queue ) return reject ( 'Not playing' ) ;
2020-01-12 00:01:18 +05:00
// Pauses the dispatcher
queue . dispatcher . pause ( ) ;
queue . playing = false ;
// Resolves the guild queue
resolve ( queue . songs [ 0 ] ) ;
} ) ;
}
/ * *
* Resumes the current song .
* @ param { string } guildID
* @ returns { Promise < Song > }
* /
resume ( guildID ) {
return new Promise ( async ( resolve , reject ) => {
// Gets guild queue
let queue = this . queues . find ( ( g ) => g . guildID === guildID ) ;
2020-02-21 15:32:16 +05:00
if ( ! queue ) return reject ( 'Not playing' ) ;
2020-01-12 00:01:18 +05:00
// Pauses the dispatcher
queue . dispatcher . resume ( ) ;
queue . playing = true ;
// Resolves the guild queue
resolve ( queue . songs [ 0 ] ) ;
} ) ;
}
/ * *
* Stops playing music .
* @ param { string } guildID
* @ returns { Promise < void > }
* /
stop ( guildID ) {
return new Promise ( async ( resolve , reject ) => {
// Gets guild queue
let queue = this . queues . find ( ( g ) => g . guildID === guildID ) ;
2020-02-21 15:32:16 +05:00
if ( ! queue ) return reject ( 'Not playing' ) ;
2020-01-12 00:01:18 +05:00
// Stops the dispatcher
queue . stopped = true ;
queue . songs = [ ] ;
queue . dispatcher . end ( ) ;
// Resolves
resolve ( ) ;
} ) ;
}
/ * *
* Updates the volume .
* @ param { string } guildID
* @ param { number } percent
* @ returns { Promise < void > }
* /
setVolume ( guildID , percent ) {
return new Promise ( async ( resolve , reject ) => {
// Gets guild queue
let queue = this . queues . find ( ( g ) => g . guildID === guildID ) ;
2020-02-21 15:32:16 +05:00
if ( ! queue ) return reject ( 'Not playing' ) ;
2020-01-12 00:01:18 +05:00
// Updates volume
queue . dispatcher . setVolumeLogarithmic ( percent / 200 ) ;
// Resolves guild queue
resolve ( queue ) ;
} ) ;
}
/ * *
* Gets the guild queue .
* @ param { string } guildID
2020-01-18 22:29:53 +05:00
* @ returns { ? Queue }
2020-01-12 00:01:18 +05:00
* /
getQueue ( guildID ) {
2020-01-18 22:29:53 +05:00
// Gets guild queue
let queue = this . queues . find ( ( g ) => g . guildID === guildID ) ;
return queue ;
2020-01-12 00:01:18 +05:00
}
/ * *
* Adds a song to the guild queue .
* @ param { string } guildID
* @ param { string } songName The name of the song to add to the queue .
* @ returns { Promise < Song > }
* /
addToQueue ( guildID , songName ) {
return new Promise ( async ( resolve , reject ) => {
// Gets guild queue
let queue = this . queues . find ( ( g ) => g . guildID === guildID ) ;
2020-02-21 15:32:16 +05:00
if ( ! queue ) return reject ( 'Not playing' ) ;
2020-01-12 00:01:18 +05:00
// Searches the song
let video = await Util . getFirstYoutubeResult ( songName , this . SYA ) . catch ( ( ) => { } ) ;
2020-02-21 15:32:16 +05:00
if ( ! video ) return reject ( 'Song not found' ) ;
2020-01-12 00:01:18 +05:00
let song = new Song ( video , queue ) ;
// Updates queue
queue . songs . push ( song ) ;
// Resolves the song
resolve ( song ) ;
} ) ;
}
2020-01-18 17:04:05 +05:00
/ * *
* Sets the queue for a guild .
* @ param { string } guildID
* @ param { Array < Song > } songs The songs list
* @ returns { Promise < Queue > }
* /
setQueue ( guildID , songs ) {
return new Promise ( async ( resolve , reject ) => {
// Gets guild queue
let queue = this . queues . find ( ( g ) => g . guildID === guildID ) ;
2020-02-21 15:32:16 +05:00
if ( ! queue ) return reject ( 'Not playing' ) ;
2020-01-18 17:04:05 +05:00
// Updates queue
queue . songs = songs ;
// Resolves the queue
resolve ( queue ) ;
} ) ;
}
2020-01-12 00:01:18 +05:00
/ * *
* Clears the guild queue , but not the current song .
* @ param { string } guildID
* @ returns { Promise < Queue > }
* /
clearQueue ( guildID ) {
return new Promise ( async ( resolve , reject ) => {
// Gets guild queue
let queue = this . queues . find ( ( g ) => g . guildID === guildID ) ;
2020-02-21 15:32:16 +05:00
if ( ! queue ) return reject ( 'Not playing' ) ;
2020-01-12 00:01:18 +05:00
// Clears queue
let currentlyPlaying = queue . songs . shift ( ) ;
queue . songs = [ currentlyPlaying ] ;
// Resolves guild queue
resolve ( queue ) ;
} ) ;
}
/ * *
* Skips a song .
* @ param { string } guildID
2020-01-12 21:29:56 +05:00
* @ returns { Promise < Song > }
2020-01-12 00:01:18 +05:00
* /
skip ( guildID ) {
return new Promise ( async ( resolve , reject ) => {
// Gets guild queue
let queue = this . queues . find ( ( g ) => g . guildID === guildID ) ;
2020-02-21 15:32:16 +05:00
if ( ! queue ) return reject ( 'Not playing' ) ;
2020-01-12 21:29:56 +05:00
let currentSong = queue . songs [ 0 ] ;
2020-01-12 00:01:18 +05:00
// Ends the dispatcher
queue . dispatcher . end ( ) ;
queue . skipped = true ;
2020-01-12 21:29:56 +05:00
// Resolves the current song
resolve ( currentSong ) ;
2020-01-12 00:01:18 +05:00
} ) ;
}
2020-01-18 15:15:10 +05:00
/ * *
* Gets the currently playing song .
* @ param { string } guildID
* @ returns { Promise < Song > }
* /
nowPlaying ( guildID ) {
return new Promise ( async ( resolve , reject ) => {
// Gets guild queue
let queue = this . queues . find ( ( g ) => g . guildID === guildID ) ;
2020-02-21 15:32:16 +05:00
if ( ! queue ) return reject ( 'Not playing' ) ;
2020-01-18 15:15:10 +05:00
let currentSong = queue . songs [ 0 ] ;
// Resolves the current song
resolve ( currentSong ) ;
} ) ;
}
2020-01-18 15:03:22 +05:00
/ * *
* Enable or disable the repeat mode
2020-02-02 15:20:46 +05:00
* @ param { string } guildID
2020-01-18 15:03:22 +05:00
* @ param { Boolean } enabled Whether the repeat mode should be enabled
* @ returns { Promise < Void > }
* /
2020-02-02 15:20:46 +05:00
setRepeatMode ( guildID , enabled ) {
2020-01-18 15:03:22 +05:00
return new Promise ( async ( resolve , reject ) => {
// Gets guild queue
let queue = this . queues . find ( ( g ) => g . guildID === guildID ) ;
2020-02-21 15:32:16 +05:00
if ( ! queue ) return reject ( 'Not playing' ) ;
2020-01-18 15:03:22 +05:00
// Enable/Disable repeat mode
queue . repeatMode = enabled ;
// Resolve
resolve ( ) ;
} ) ;
}
2020-01-12 00:01:18 +05:00
/ * *
* Start playing songs in a guild .
* @ ignore
* @ param { string } guildID
* @ param { Boolean } firstPlay Whether the function was called from the play ( ) one
* /
async _playSong ( guildID , firstPlay ) {
// Gets guild queue
let queue = this . queues . find ( ( g ) => g . guildID === guildID ) ;
// If there isn't any music in the queue
2020-04-20 23:47:25 +05:00
if ( queue . songs . length < 2 && ! firstPlay && ! queue . repeatMode ) {
2020-01-12 00:01:18 +05:00
// Leaves the voice channel
2020-01-12 22:56:11 +05:00
if ( this . options . leaveOnEnd && ! queue . stopped ) queue . connection . channel . leave ( ) ;
2020-01-12 00:01:18 +05:00
// Remoces the guild from the guilds list
this . queues = this . queues . filter ( ( g ) => g . guildID !== guildID ) ;
// Emits stop event
2020-01-12 22:56:11 +05:00
if ( queue . stopped ) {
if ( this . options . leaveOnStop ) queue . connection . channel . leave ( ) ;
return queue . emit ( 'stop' ) ;
}
2020-01-12 00:01:18 +05:00
// Emits end event
return queue . emit ( 'end' ) ;
}
// Emit songChanged event
2020-01-18 15:03:22 +05:00
if ( ! firstPlay ) queue . emit ( 'songChanged' , ( ! queue . repeatMode ? queue . songs . shift ( ) : queue . songs [ 0 ] ) , queue . songs [ 0 ] , queue . skipped , queue . repeatMode ) ;
2020-01-12 00:01:18 +05:00
queue . skipped = false ;
let song = queue . songs [ 0 ] ;
// Download the song
let dispatcher = queue . connection . play ( ytdl ( song . url , { filter : "audioonly" } ) ) ;
queue . dispatcher = dispatcher ;
// Set volume
dispatcher . setVolumeLogarithmic ( queue . volume / 200 ) ;
// When the song ends
dispatcher . on ( 'finish' , ( ) => {
// Play the next song
return this . _playSong ( guildID , false ) ;
} ) ;
}
} ;
module . exports = Player ;