diff --git a/bot.js b/bot.js index 9a3a764..e9ca33f 100644 --- a/bot.js +++ b/bot.js @@ -1,39 +1,72 @@ const Discord = require("discord.js"), + fs = require('fs'), logger = require('./lib/logging').getLogger(), - music = require('./lib/music'); + music = require('./lib/music'), cmd = require("./lib/cmd"), client = new Discord.Client(), args = require('args-parser')(process.argv), - authToken = args.token, - prefix = '~'; + authToken = args.token; + +let savedplaylists = {}; function main() { music.setLogger(logger); cmd.setLogger(logger); cmd.init(); + if (fs.existsSync('./data/savedplaylists.json')) { + savedplaylists = JSON.parse(fs.readFileSync('./data/savedplaylists.json')) + } registerCommands(); - music.setClient(client); client.login(authToken).then(()=> { logger.debug("Logged in"); }); } +function savePlaylist(url, name) { + savedplaylists[name] = url; + fs.writeFile('./data/savedplaylists.json',JSON.stringify(savedplaylists), (err) => { + if (err) logger.warn(JSON.stringify(err)); + }) +} + function registerCommands() { cmd.createCommand('~', 'play', (msg, argv) => { let vc = msg.member.voiceChannel; let url = argv['url']; if (!url) return 'No url given.'; + if (!url.match(/http/g)) { + if (savedplaylists[url]) { + url = savedplaylists[url]; + } + } try { return music.play(vc, url); } catch(err) { logger.error(err); msg.reply(`${JSON.stringify(err)}`); } - }, ['url']); + }, ['url'], "Adds the url to the YouTube video/playlist into the queue."); + + cmd.createCommand('~', 'playnext', (msg, argv) => { + let vc = msg.member.voiceChannel; + let url = argv['url']; + if (!url) return 'No url given.'; + if (!url.match(/http/g)) { + if (savedplaylists[url]) { + url = savedplaylists[url]; + } + } + try { + return music.playnext(vc, url); + } catch(err) { + logger.error(err); + msg.reply(`${JSON.stringify(err)}`); + } + }, ['url'], "Plays the YouTube video after the currently playing song."); cmd.createCommand('~', 'ping', () => { return 'Pong!'; - }); + }, [], "Try it yourself."); cmd.createCommand('~', 'join', (msg) => { if (msg.member.voiceChannel) { @@ -42,55 +75,66 @@ function registerCommands() { else { msg.reply("You are not connected to a voicechannel."); } - }); + }, [], "Joins the VC you are in."); cmd.createCommand('~', 'stop', (msg) => { - let vc = msg.member.voiceChannel; - music.stop(vc); - }); + let gid = msg.guild.id; + music.stop(gid); + }, [], "Stops playling music and leavs."); cmd.createCommand('~', 'pause', (msg) => { - let vc = msg.member.voiceChannel; - music.pause(vc); - }); + let gid = msg.guild.id; + music.pause(gid); + }, [], "Pauses playing."); cmd.createCommand('~', 'resume', (msg) => { - let vc = msg.member.voiceChannel; - music.resume(vc); - }); + let gid = msg.guild.id; + music.resume(gid); + }, [], "Resumes playing."); cmd.createCommand('~', 'skip', (msg) => { - let vc = msg.member.voiceChannel; - music.skip(vc); - }); - - cmd.createCommand('~', 'plist', (msg) => { - let vc = msg.member.voiceChannel; - music.getQueue(vc, (songs) => { - let songlist = "**Songs**\n"; - for (let i = 0; i < songs.length; i++) { - if (i > 10) break; - songlist += songs[i] + '\n'; - } - msg.reply(songlist); - }); - }); + let gid = msg.guild.id; + music.skip(gid); + }, [], "Skips the current song."); + + cmd.createCommand('~', 'clear', (msg) => { + let gid = msg.guild.id; + music.clearQueue(gid); + return "All songs have been deleted, commander :no_mouth: " + }, [],"Clears the playlist."); + + cmd.createCommand('~', 'playlist', (msg) => { + let gid = msg.guild.id; + let songs = music.getQueue(gid); + logger.debug(`found ${songs.length} songs`); + let songlist = `**${songs.length} Songs in playlist**\n`; + for (let i = 0; i < songs.length; i++) { + if (i > 10) break; + songlist += songs[i] + '\n'; + } + return songlist; + }, [], "Shows the next ten songs."); cmd.createCommand('~', 'shuffle', (msg) => { - let vc = msg.member.voiceChannel; - music.shuffle(vc); - }); + let gid = msg.guild.id; + music.shuffle(gid); + return "The queue has successfully been shuffled :slight_smile:" + }, [], "Shuffles the playlist."); cmd.createCommand('~', 'current', (msg) => { - let vc = msg.member.voiceChannel; - music.nowPlaying(vc, (title, url) => { - msg.reply(`Playing: ${title}\n ${url}`); - }); - }); - - cmd.createCommand('_', 'repeat', (msg, argv) => { - return argv['repeattext']; - }, ['repeattext']) + let gid = msg.guild.id; + let song = music.nowPlaying(gid); + return `Playing: ${song.title}\n ${song.url}`; + }, [], "Shows the currently playing song."); + + cmd.createCommand('~', 'repeatafterme', (msg, argv) => { + return argv['word']; + }, ['word'], "Repeats a single word you say."); + + cmd.createCommand('~', 'save', (msg, argv) => { + savePlaylist(argv['url'], argv['name']); + return `Saved song/playlist as ${argv['name']}` + }, ['url', 'name'], "Saves the YouTube song/playlist with a specific name. ~play [name] to play the playlist"); } // defining the client's handlers diff --git a/lib/cmd.js b/lib/cmd.js index 7e4b5cc..27f40fe 100644 --- a/lib/cmd.js +++ b/lib/cmd.js @@ -14,13 +14,14 @@ exports.setLogger = function(newLogger) { logger = newLogger; }; -exports.createCommand = function(prefix, command, call, argv) { +exports.createCommand = function(prefix, command, call, argv, description) { try { logger.debug(`Creating command ${command} with prefix ${prefix} and arguments ${argv}`); if (!commands[prefix]) commands[prefix] = {}; // create Object commands prefix commands[prefix][command] = { // assign the command - args: argv || [], // with arguments - callback: call // and function + args: argv || [], + callback: call, + description: description }; logger.debug(`Created command ${prefix}${command}`); } catch (err) { @@ -80,10 +81,13 @@ exports.init = function() { helpstr += "Commands\n---\n"; Object.keys(commands).forEach((key) => { Object.keys(commands[key]).forEach((cmd) => { - helpstr += "\n" + key + cmd + " " + JSON.stringify(commands[key][cmd].args) + "\n"; + helpstr += "\n" + key + cmd + " " + JSON.stringify(commands[key][cmd].args).replace(/"|\[\]/g, ''); + if (commands[key][cmd].description) { + helpstr += '\t' + commands[key][cmd].description + '\n'; + } }); }); helpstr += "```"; return helpstr; - }); + }, [], "Shows this help."); }; \ No newline at end of file diff --git a/lib/datacollector.js b/lib/datahandler.js similarity index 69% rename from lib/datacollector.js rename to lib/datahandler.js index 5962a62..ae56f6c 100644 --- a/lib/datacollector.js +++ b/lib/datahandler.js @@ -4,7 +4,7 @@ let logger = require('winston'); /* Function Definition */ - +// TODO: Class that handles file-data for a server, functions to get/set data for specific server id /** * Getting the logger * @param {Object} newLogger diff --git a/lib/music.js b/lib/music.js index a670582..f3f0808 100644 --- a/lib/music.js +++ b/lib/music.js @@ -5,218 +5,363 @@ const Discord = require("discord.js"), ytapiKey = "AIzaSyBLF20r-c4mXoAT2qBFB5YlCgT0D-izOaU"; /* Variable Definition */ let logger = require('winston'); -let client = null; +let djs = {}; let connections = {}; /* Function Definition */ // TODO: initCommands function that takes the cmd.js module as variable and uses it to create commands -/** - * Getting the logger; - * @param {Object} newLogger - */ -exports.setLogger = function (newLogger) { - logger = newLogger; -}; - -/** - * Sets the discord Client for the module - * @param newClient - */ -exports.setClient = function(newClient) { - client = newClient; -}; +class DJ { + constructor(voiceChannel) { + this.conn = null; + this.disp = null; + this.queue = []; + this.playing = false; + this.current = null; + this.volume = 0.5; + this.voiceChannel = voiceChannel; + } -/** - * Connects to a voicechannel - * @param voiceChannel - */ -exports.connect = function(voiceChannel) { - logger.debug(JSON.stringify()); - logger.verbose(`Connecting to voiceChannel ${voiceChannel.name}`); - if (client !== null) { - voiceChannel.join().then(connection => { - logger.info(`Connected to Voicechannel ${voiceChannel.name}`); - connections[voiceChannel.guild.id] = { - 'conn': connection, - 'disp': null, - 'queue': [], - 'playing': false, - current: null - }; + /** + * Connects to the given voice channel. Disconnects from the previous one if it exists. + * When the bot was moved and connect is executed again, it connects to the initial VoiceChannel because the + * VoiceChannel is saved as object variable. + * @returns {Promise} + */ + connect() { + if (this.conn) { + this.stop(); + } + logger.verbose(`Connecting to voiceChannel ${this.voiceChannel.name}`); + return this.voiceChannel.join().then(connection => { + logger.info(`Connected to Voicechannel ${this.voiceChannel.name}`); + this.conn = connection; }); - } else { - logger.error("Client is null"); } -}; -/** - * Plays a file - * @param filename - */ -exports.playFile = function(voiceChannel, filename) { - let gid = voiceChannel.guild.id; - let conn = connections[gid].conn; - if (conn !== null) { - connections[gid].disp = conn.playFile(filename); - connections[gid].playing = true; - } else { - this.connect(voiceChannel); - logger.warn("Not connected to a voicechannel"); + /** + * Plays a file for the given filename. + * TODO: Implement queue + * @param filename + */ + playFile(filename) { + if (this.conn !== null) { + this.disp = this.conn.playFile(filename); + this.playing = true; + } else { + logger.warn("Not connected to a voicechannel. Connection now."); + this.connect(this.voiceChannel).then(() => { + this.playFile(filename); + }); + } } -}; -exports.play = function(voiceChannel, url) { - let gid = voiceChannel.guild.id; - if (!connections[gid]) this.connect(voiceChannel); - let conn = connections[gid].conn; - if (conn !== null) { + /** + * Plays the url of the current song if there is no song playing or puts it in the queue. + * If the url is a playlist (regex match), the videos of the playlist are fetched and put + * in the queue. For each song the title is saved in the queue too. + * @param url + */ + playYouTube(url, playnext) { + if (!this.conn) this.connect(this.voiceChannel).then(this.playYouTube(url)); let plist = url.match(/(?<=\?list=)[\w\-]+/g); if (plist) { logger.debug(`Adding playlist ${plist} to queue`); ypi(ytapiKey, plist).then(items => { for (let i = 0; i < items.length; i++) { let vurl = `https://www.youtube.com/watch?v=${items[i].resourceId.videoId}`; - connections[gid].queue.push(vurl); + this.queue.push({'url': vurl, 'title': null}); + yttl(vurl.replace(/http(s)?:\/\/(www.)?youtube.com\/watch\?v=/g, ''), (err, title) => { + if (err) { + logger.debug(JSON.stringify(err)); + } else { + try { + logger.debug(`Found title: ${title} for ${vurl}`); + this.queue.find((el) => { + return (el.url === vurl); + }).title = title; + } catch (error) { + logger.verbose(JSON.stringify(error)); + } + } + }); } - this.play(voiceChannel, connections[gid].queue.shift()); + this.current = this.queue.shift(); + this.playYouTube(this.current.url); }); return; } - if (!connections[gid].playing) { + if (!this.playing) { logger.debug(`Playing ${url}`); - connections[gid].disp = conn.playStream(ytdl(url, { + this.disp = this.conn.playStream(ytdl(url, { filter: "audioonly" - }), {seek: 0, volume: 0.5}); - connections[gid].disp.on('end', () => { - connections[gid].playing = false; - connections[gid].current = null; - if (connections[gid].queue.length > 0) { - this.play(voiceChannel, connections[gid].queue.shift()); + }), {seek: 0, volume: this.volume}); + this.disp.on('end', () => { + this.playing = false; + this.current = null; + if (this.queue.length > 0) { + this.current = this.queue.shift(); + this.playYouTube(this.current.url); + } else { + this.stop(); } }); - connections[gid].playing = true; - connections[gid].current = url; + this.playing = true; } else { logger.debug(`Added ${url} to the queue`); - connections[gid].queue.push(url); + if (playnext) { + this.queue.unshift({'url': url, 'title': null}); + } else { + this.queue.push({'url': url, 'title': null}); + } + yttl(url.replace(/http(s)?:\/\/(www.)?youtube.com\/watch\?v=/g, ''), (err, title) => { + if (err) { + logger.debug(JSON.stringify(err)); + } else { + try { + logger.debug(`Found title: ${title} for ${url}`); + this.queue.find((el) => { + return (el.url === url); + }).title = title; + } catch (error) { + console.verbose(JSON.stringify(error)); + } + } + }); } - } else { - logger.warn("Not connected to a voicechannel"); } + + /** + * Sets the volume of the dispatcher to the given value + * @param percentage + */ + setVolume(percentage) { + logger.verbose(`Setting volume to ${percentage}`); + if (this.disp !== null) { + this.volume = percentage; + this.disp.setVolume(percentage); + } else { + logger.warn("No dispatcher found.") + } + } + + /** + * Pauses if a dispatcher exists + */ + pause() { + logger.verbose("Pausing music..."); + if (this.disp !== null) { + this.disp.pause(); + } else { + logger.warn("No dispatcher found"); + } + } + + /** + * Resumes if a dispatcher exists + */ + resume() { + logger.verbose("Resuming music..."); + if (this.disp !== null) { + this.disp.resume(); + } else { + logger.warn("No dispatcher found"); + } + } + + /** + * Stops playing music by ending the Dispatcher and disconnecting + */ + stop() { + this.queue = []; + logger.verbose("Stopping music..."); + if (this.disp !== null) { + this.disp.end(); + logger.debug("Ended dispatcher"); + } + if (this.conn !== null) { + this.conn.disconnect(); + logger.debug("Ended connection"); + } + } + + /** + * Skips to the next song by ending the current StreamDispatcher and thereby triggering the + * end event of the dispatcher that automatically plays the next song. + */ + skip () { + logger.debug("Skipping song"); + if (this.disp !== null) { + this.disp.end(); + } + } + + /** + * Returns the title for each song saved in the queue + * @returns {Array} + */ + get playlist() { + let songs = []; + this.queue.forEach((entry) => { + songs.push(entry.title); + }); + return songs; + } + + /** + * Returns the song saved in the private variable 'current' + * @returns {null|*} + */ + get song() { + return this.current; + } + + /** + * Shuffles the queue + */ + shuffle() { + this.queue = shuffleArray(this.queue); + } + + /** + * Clears the playlist + */ + clear() { + this.queue = []; + } +} + +/** + * Getting the logger; + * @param {Object} newLogger + */ +exports.setLogger = function (newLogger) { + logger = newLogger; }; /** - * Sets the volume of the music - * @param percentage + * Connects to a voicechannel * @param voiceChannel */ -exports.setVolume = function(voiceChannel, percentage) { - let disp = connections[voiceChannel.guild.id].disp; - logger.verbose(`Setting volume to ${percentage}`); - if (disp !== null) { - disp.setVolume(percentage); +exports.connect = function(voiceChannel) { + let gid = voiceChannel.guild.id; + let voiceDJ = new DJ(voiceChannel); + djs[gid] = voiceDJ; + return voiceDJ.connect(); +}; + +/** + * Plays a file + * @param filename + * @param guildId + */ +exports.playFile = function(guildId, filename) { + djs[guildId].playFile(filename); +}; + +/** + * Plays a YT Url + * @param voiceChannel + * @param url + */ +exports.play = function(voiceChannel, url) { + let guildId = voiceChannel.guild.id; + if (!djs[guildId]) { + this.connect(voiceChannel).then(() => { + djs[guildId].playYouTube(url); + }); } else { - logger.warn("No dispatcher found.") + djs[guildId].playYouTube(url); } }; /** - * pauses the music + * plays the given url as next song + * @param voiceChannel + * @param url */ -exports.pause = function(voiceChannel) { - let disp = connections[voiceChannel.guild.id].disp; - logger.verbose("Pausing music..."); - if (disp !== null) { - disp.pause(); +exports.playnext = function(voiceChannel, url) { + let guildId = voiceChannel.guild.id; + if (!djs[guildId]) { + this.connect(voiceChannel).then(() => { + djs[guildId].playYouTube(url, true); + }); } else { - logger.warn("No dispatcher found"); + djs[guildId].playYouTube(url, true); } }; +/** + * Sets the volume of the music + * @param percentage + * @param guildId + */ +exports.setVolume = function(guildId, percentage) { + djs[guildId].setVolume(percentage); +}; + +/** + * pauses the music + */ +exports.pause = function(guildId) { + djs[guildId].pause(); +}; + /** * Resumes the music + * @param guildId */ -exports.resume = function(voiceChannel) { - let disp = connections[voiceChannel.guild.id].disp; - logger.verbose("Resuming music..."); - if (disp !== null) { - disp.resume(); - } else { - logger.warn("No dispatcher found"); - } +exports.resume = function(guildId) { + djs[guildId].resume(); }; /** * Stops the music + * @param guildId */ -exports.stop = function(voiceChannel) { - let gid = voiceChannel.guild.id; - let disp = connections[gid].disp; - let conn = connections[gid].conn; - logger.verbose("Stopping music..."); - if (disp !== null) { - disp.end(); - logger.debug("Ended dispatcher"); - } - if (conn !== null) { - conn.disconnect(); - logger.debug("Ended connection"); - } - connections[gid].playing = false; +exports.stop = function(guildId) { + djs[guildId].stop(); + delete djs[guildId]; }; /** * Skips the song + * @param guildId */ -exports.skip = function(voiceChannel) { - let disp = connections[voiceChannel.guild.id].disp; - logger.debug("Skipping song"); - if (disp !== null) { - disp.end(); - } +exports.skip = function(guildId) { + djs[guildId].skip(); +}; + +/** + * Clears the playlist + * @param guildId + */ +exports.clearQueue = function(guildId) { + djs[guildId].clear(); }; /** - * executes the callback when the titlelist is finished + * Returns the queue + * @param guildId */ -exports.getQueue = function(voiceChannel, callback) { - let titles = []; - connections[voiceChannel.guild.id].queue.forEach((url) => { - yttl(url.replace(/http(s)?:\/\/(www.)?youtube.com\/watch\?v=/g, ''), (err, title) => { - if (err) { - logger.error(err); - } else { - titles.push(title); - } - }); - }); - setTimeout(() => callback(titles), 2000 ); +exports.getQueue = function(guildId) { + return djs[guildId].playlist; }; /** * evokes the callback function with the title of the current song - * @param callback - * @param voiceChannel + * @param guildId */ -exports.nowPlaying = function(voiceChannel, callback) { - let gid = voiceChannel.guild.id; - if (connections[gid].queue.length > 0) { - yttl(connections[gid].current.replace(/http(s)?:\/\/(www.)?youtube.com\/watch\?v=/g, ''), (err, title) => { - if (err) { - logger.error(err); - } else { - callback(title, connections[gid].current); - } - }); - } +exports.nowPlaying = function(guildId) { + return djs[guildId].song; }; /** * shuffles the queue + * @param guildId */ -exports.shuffle = function(voiceChannel) { - connections[voiceChannel.guild.id].queue = shuffle(connections[voiceChannel.guild.id].queue); +exports.shuffle = function(guildId) { + djs[guildId].shuffle(); }; /** @@ -224,7 +369,7 @@ exports.shuffle = function(voiceChannel) { * @param array * @returns {Array} */ -function shuffle(array) { +function shuffleArray(array) { let currentIndex = array.length, temporaryValue, randomIndex; // While there remain elements to shuffle...