491 lines
19 KiB
JavaScript
Executable File
491 lines
19 KiB
JavaScript
Executable File
console.log('Starting');
|
||
const Discord = require('discord.js');
|
||
const fs = require('fs');
|
||
const child_process = require('child_process');
|
||
const colors = require('colors');
|
||
const download = require('download-file');
|
||
//const ytdl = require('ytdl-core');
|
||
const youtubedl = require('youtube-dl');
|
||
//const ffprobe = require('ffprobe');
|
||
|
||
const client = new Discord.Client();
|
||
client.login(fs.readFileSync('token.txt', 'utf8'));
|
||
client.on('ready', ()=>{
|
||
console.log('Ready');
|
||
});
|
||
|
||
|
||
var music = fs.readdirSync('./music/');
|
||
var midis = fs.readdirSync('./midi/');
|
||
|
||
//var music_extensions = ["aac","aiff","aif","flac","m4a","mp3","ogg","wav","wma","webm","mkv","flv","avi","mov","qt","wmv","mp4","m4v"];
|
||
const music_extensions = ["3dostr","3g2","3gp","4xm","a64","aa","aac","ac3","acm","act","adf","adp","ads","adts","adx","aea","afc","aiff","aix","alaw","alias_pix","amr","anm","apc","ape","apng","aqtitle","asf","asf_o","asf_stream","ass","ast","au","avi","avm2","avr","avs","bethsoftvid","bfi","bfstm","bin","bink","bit","bmp_pipe","bmv","boa","brender_pix","brstm","c93","caf","cavsvideo","cdg","cdxl","cine","concat","crc","dash","data","daud","dcstr","dds_pipe","dfa","dirac","dnxhd","dpx_pipe","dsf","dsicin","dss","dts","dtshd","dv","dv1394","dvbsub","dvbtxt","dvd","dxa","ea","ea_cdata","eac3","epaf","exr_pipe","f32be","f32le","f4v","f64be","f64le","fbdev","ffm","ffmetadata","fifo","film_cpk","filmstrip","flac","flic","flv","framecrc","framehash","framemd5","frm","fsb","g722","g723_1","g729","genh","gif","gsm","gxf","h261","h263","h264","hash","hds","hevc","hls","hls","applehttp","hnm","ico","idcin","idf","iff","ilbc","image2","image2pipe","ingenient","ipmovie","ipod","ircam","ismv","iss","iv8","ivf","ivr","j2k_pipe","jacosub","jpeg_pipe","jpegls_pipe","jv","latm","lavfi","live_flv","lmlm4","loas","lrc","lvf","lxf","m4v","matroska","matroska","webm","md5","mgsts","microdvd","mjpeg","mkvtimestamp_v2","mlp","mlv","mm","mmf","mov","mov","mp4","m4a","3gp","3g2","mj2","mp2","mp3","mp4","mpc","mpc8","mpeg","mpeg1video","mpeg2video","mpegts","mpegtsraw","mpegvideo","mpjpeg","mpl2","mpsub","msf","msnwctcp","mtaf","mtv","mulaw","musx","mv","mvi","mxf","mxf_d10","mxf_opatom","mxg","nc","nistsphere","nsv","null","nut","nuv","oga","ogg","ogv","oma","opus","oss","paf","pam_pipe","pbm_pipe","pcx_pipe","pgm_pipe","pgmyuv_pipe","pictor_pipe","pjs","pmp","png_pipe","ppm_pipe","psp","psxstr","pva","pvf","qcp","qdraw_pipe","r3d","rawvideo","realtext","redspark","rl2","rm","roq","rpl","rsd","rso","rtp","rtp_mpegts","rtsp","s16be","s16le","s24be","s24le","s32be","s32le","s8","sami","sap","sbg","sdp","sdr2","segment","sgi_pipe","shn","siff","singlejpeg","sln","smjpeg","smk","smoothstreaming","smush","sol","sox","spdif","spx","srt","stl","stream_segment","ssegment","subviewer","subviewer1","sunrast_pipe","sup","svag","svcd","swf","tak","tedcaptions","tee","thp","tiertexseq","tiff_pipe","tmv","truehd","tta","tty","txd","u16be","u16le","u24be","u24le","u32be","u32le","u8","uncodedframecrc","v210","v210x","v4l2","vag","vc1","vc1test","vcd","video4linux2","v4l2","vivo","vmd","vob","vobsub","voc","vpk","vplayer","vqf","w64","wav","wc3movie","webm","webm_chunk","webm_dash_manife","webp","webp_pipe","webvtt","wsaud","wsd","wsvqa","wtv","wv","wve","x11grab","xa","xbin","xmv","xvag","xwma","yop","yuv4mpegpipe"];
|
||
const midi_extensions = ["mid","rmi","rcp","r36","g18","g36","mfi","kar","mod","wrd","xm","s3m","oct","med","ahx","it"];
|
||
|
||
const myVoiceChannelID = "339628587747639296";
|
||
const myGuildID = "321819041348190249";
|
||
const cmdChar = "!";
|
||
|
||
let myVoiceConnection;
|
||
|
||
function play(filename, type, channel) {
|
||
client.channels.get(myVoiceChannelID).join().then(connection => {
|
||
myVoiceConnection = connection;
|
||
if (type === "audio") {
|
||
|
||
const filtered_filename = filename.replace(/\//g, ':');
|
||
const path = './music/'+filtered_filename;
|
||
const metadata_path = './music_metadata/'+filtered_filename+'.json';
|
||
|
||
connection.playFile(path, {bitrate:"auto"});
|
||
connection.dispatcher.songname = filename;
|
||
|
||
let np_message;
|
||
channel.send('🎶 **Now playing:** `'+filename+'` 💿').then(m => mp_message = m);
|
||
|
||
fs.readFile(metadata_path, 'utf8', (err,data)=>{
|
||
if (err) { // make metadata file
|
||
console.log(`creating metadata file ${metadata_path}`);
|
||
const metadata = {
|
||
plays: 1,
|
||
lastPlay: new Date().toJSON(),
|
||
};
|
||
// ffprobe(path, {path: '/usr/bin/ffprobe'}, (err, info) => {
|
||
// metadata.ffprobe = info;
|
||
connection.dispatcher.meta = metadata;
|
||
fs.writeFileSync(metadata_path, JSON.stringify(metadata));
|
||
// });
|
||
} else { // load & update metadata file
|
||
console.log(`using metadata file ${metadata_path}`);
|
||
const metadata = JSON.parse(data);
|
||
metadata.plays++;
|
||
metadata.lastPlay = new Date().toJSON();
|
||
connection.dispatcher.meta = metadata;
|
||
fs.writeFileSync(metadata_path, JSON.stringify(metadata));
|
||
}
|
||
});
|
||
|
||
client.user.setGame(filename);
|
||
|
||
connection.dispatcher.on('end', () => {
|
||
client.user.setGame();
|
||
});
|
||
|
||
}
|
||
else if (type === "midi") {
|
||
const path = './midi/'+filename.split('/').join(':');
|
||
const timidity = child_process.spawn('timidity', [path, '-c', './timidity.cfg', '-o', '-']);
|
||
timidity.stderr.on('data', data => {
|
||
console.log(("[TiMidity] "+data.toString()).yellow);
|
||
});
|
||
connection.playConvertedStream(timidity.stdout, {bitrate:"auto"});
|
||
connection.dispatcher.songname = filename;
|
||
if (channel) channel.send('🎶 **Now playing:** `'+filename+'` 🎹');
|
||
client.user.setGame(filename);
|
||
connection.dispatcher.on('end', () => {
|
||
timidity.kill();
|
||
client.user.setGame();
|
||
});
|
||
}
|
||
else if (type === "yt") {
|
||
if (!filename.startsWith('http')) filename = 'ytsearch:'+filename;
|
||
const dl = youtubedl(filename, ['-f bestaudio'], {maxBuffer: Infinity});
|
||
let video_filename;
|
||
dl.on('info', function(info) {
|
||
connection.playStream(dl, {bitrate:"auto"});
|
||
if (channel) channel.send('🎶 **Now playing:** `'+info.title+'` 📺');
|
||
fs.appendFileSync('./ytplay-history.txt', info._filename+'\n'); // ¯\_(ツ)_/¯
|
||
client.user.setGame(info.title);
|
||
connection.dispatcher.on('end', ()=>{
|
||
client.user.setGame();
|
||
});
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
|
||
const commands = {
|
||
help: {
|
||
description: "Shows command list",
|
||
execute: function (message, args, txt) {
|
||
const embed = {
|
||
color: client.guilds.get(myGuildID).me.colorRole.color,
|
||
author: {name: "Music Bot Commands", icon_url: client.user.avatarURL},
|
||
fields: []
|
||
}
|
||
for (const commandName in commands) {
|
||
const command = commands[commandName];
|
||
if (command.hidden) continue;
|
||
embed.fields.push({name: cmdChar+commandName,value: command.description || "(no description)"});
|
||
}
|
||
|
||
let scl = [];
|
||
Object.keys(songCommands).forEach(c => scl.push(cmdChar+c));
|
||
embed.fields.push({name: scl.join(', '), value: "These commands control a playing song."});
|
||
|
||
message.channel.send({embed});
|
||
}
|
||
},
|
||
//join: {},
|
||
leave: {
|
||
description: "Disconnects the bot from the voice channel.",
|
||
execute: function (message, args, txt) {
|
||
if (myVoiceConnection) {
|
||
myVoiceConnection.disconnect();
|
||
message.react('🆗');
|
||
} else {
|
||
message.react('⚠');
|
||
}
|
||
}
|
||
},
|
||
play: {
|
||
aliases: ["p"],
|
||
description: "Plays something. If no arguments are given, a random audio or MIDI is played. If given the name of an existing audio or MIDI file, that will be played, else the query will be searched and a random result will be played.",
|
||
execute: function (message, args, txt) {
|
||
let query = txt(1);
|
||
if (query) {
|
||
if (music.includes(query)) play(query, 'audio', message.channel);
|
||
else if (midis.includes(query)) play(query, 'midi', message.channel);
|
||
else {
|
||
let music_search = music.search(query);
|
||
if (music_search.length > 0) play(music_search.random(), 'audio', message.channel);
|
||
else {
|
||
let midi_search = midis.search(query);
|
||
if (midi_search.length > 0) play(midi_search.random(), 'midi', message.channel);
|
||
else {
|
||
message.channel.send('⚠ **Nothing was found.** Try narrowing your keyword or use a different command (use `!help` for command list)');
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
if ([true,false].random()) play(music.random(), 'audio', message.channel);
|
||
else play(midis.random(), 'midi', message.channel);
|
||
}
|
||
}
|
||
},
|
||
playaudio: {
|
||
aliases: ["pa"],
|
||
description: "Like play but restricted to audio.",
|
||
execute: function (message, args, txt) {
|
||
let query = txt(1);
|
||
if (query) {
|
||
if (fs.existsSync('./music/'+query)) play(query, 'audio', message.channel);
|
||
else {
|
||
let search = music.search(query);
|
||
if (search.length > 0) play(search.random(), 'audio', message.channel);
|
||
else play(music.random(), 'audio', message.channel);
|
||
}
|
||
} else play(music.random(), 'audio', message.channel);
|
||
}
|
||
},
|
||
playmidi: {
|
||
aliases: ["pm"],
|
||
description: "Like play but restricted to MIDIs.",
|
||
execute: function (message, args, txt) {
|
||
let query = txt(1);
|
||
if (query) {
|
||
if (fs.existsSync('./midi/'+query)) play(query, 'midi', message.channel);
|
||
else {
|
||
let search = midis.search(query);
|
||
if (search.length > 0) play(search.random(), 'midi', message.channel);
|
||
else play(midis.random(), 'midi', message.channel);
|
||
}
|
||
} else play(midis.random(), 'midi', message.channel);
|
||
}
|
||
},
|
||
search: {
|
||
aliases: ["s"],
|
||
description: "Searches the audio and midi collections. All results that **include** the given query are returned.",
|
||
execute: function (message, args, txt) {
|
||
if (txt(1)) {
|
||
let music_search = music.search(txt(1));
|
||
let midi_search = midis.search(txt(1));
|
||
if (music_search != "" || midi_search != "") {
|
||
if (music_search != "" && music_search.length < 100) {
|
||
let sr = "💿 **Audio Search results:**\n";
|
||
music_search.forEach((item, index, array) => {
|
||
sr += '`'+item+'`\n';
|
||
try {
|
||
if (sr.length+array[index+1].length >= 1950) {
|
||
message.channel.send(sr);
|
||
sr = "";
|
||
}
|
||
} catch(e) {
|
||
message.channel.send(sr);
|
||
}
|
||
});
|
||
}
|
||
if (midi_search != "" && midi_search.length < 100) {
|
||
let sr = "🎹 **MIDI Search results:**\n";
|
||
midi_search.forEach((item, index, array) => {
|
||
sr += '`'+item+'`\n';
|
||
try {
|
||
if (sr.length+array[index+1].length >= 1950) {
|
||
message.channel.send(sr);
|
||
sr = "";
|
||
}
|
||
} catch(e) {
|
||
message.channel.send(sr);
|
||
}
|
||
});
|
||
}
|
||
} else {
|
||
message.channel.send('⚠ **No results.**');
|
||
}
|
||
} else {
|
||
message.channel.send('**Usage:** `!search <query>`');
|
||
}
|
||
}
|
||
},
|
||
upload: {
|
||
aliases: ["u"],
|
||
description: "Adds the attached file to the audio, midi, or soundfont collection.",
|
||
execute: function (message, args, txt) {
|
||
if (typeof message.attachments.first() !== 'undefined') {
|
||
let attachment_name = message.attachments.first().filename;
|
||
let attachment_extension = attachment_name.split('.').pop().toLowerCase();
|
||
if (music_extensions.includes(attachment_extension.toLowerCase())) {
|
||
if (!fs.existsSync('./music/'+attachment_name)) {
|
||
message.react('🆗');
|
||
download(message.attachments.first().url, {directory: "./music/"}, function(err) {
|
||
if (err) {message.channel.send('⚠ **An error occurred while downloading:** ```'+err+'```'); return;}
|
||
music.push(attachment_name);
|
||
message.channel.send('📁 **Added** `'+attachment_name+'` **to the music collection.** 💿');
|
||
});
|
||
} else {
|
||
message.channel.send('⚠ **File** `'+attachment_name+'` **already exists.**');
|
||
}
|
||
} else if (midi_extensions.includes(attachment_extension.toLowerCase())) {
|
||
if (!fs.existsSync('./midi/'+attachment_name)) {
|
||
message.react('🆗');
|
||
download(message.attachments.first().url, {directory: "./midi/"}, function(err) {
|
||
if (err) {message.channel.send('⚠ **An error occurred while downloading:** ```'+err+'```'); return;}
|
||
midis.push(attachment_name);
|
||
message.channel.send('📁 **Added** `'+attachment_name+'` **to the MIDI collection.** 🎹');
|
||
});
|
||
} else {
|
||
message.channel.send('⚠ **File** `'+attachment_name+'` **already exists.**');
|
||
}
|
||
} else if (["sfx","sf2", "cfg"].includes(attachment_extension.toLowerCase())) {
|
||
if (!fs.existsSync('./soundfonts/'+attachment_name)) {
|
||
message.react('🆗');
|
||
download(message.attachments.first().url, {directory: "./soundfonts/"}, function(err) {
|
||
if (err) {message.channel.send('⚠ **An error occurred while downloading:** ```'+err+'```'); return;}
|
||
message.channel.send('📁 **Added** `'+attachment_name+'` **to the soundfont collection.** 🎺');
|
||
});
|
||
} else {
|
||
message.channel.send('⚠ **File** `'+attachment_name+'` **already exists.**');
|
||
}
|
||
} else {
|
||
message.channel.send('⚠ **Format extension `'+attachment_extension+'` is not supported or unknown.**');
|
||
download(message.attachments.first().url, {directory: "./trash/"});
|
||
}
|
||
} else {
|
||
message.channel.send('ℹ **To upload your music, type `!upload` and attach the file to your message.**');
|
||
}
|
||
}
|
||
},
|
||
ytplay: {
|
||
description: "Plays something from YouTube!",
|
||
hidden: true,
|
||
execute: function (message, args, txt) {
|
||
if (txt(1)) {
|
||
message.react('🆗');
|
||
play(txt(1), 'yt', message.channel);
|
||
} else {
|
||
message.reply('ℹ **Usage:** `!ytplay <youtube URL or search query>`');
|
||
}
|
||
}
|
||
},
|
||
ytdl: {
|
||
description: "Adds a YouTube video to the audio collection.",
|
||
hidden: true,
|
||
execute: function (message, args, txt) {
|
||
if (txt(1)) {
|
||
message.react('🆗');
|
||
let query = txt(1);
|
||
if (!query.startsWith('http')) query = 'ytsearch:'+query;
|
||
const dl = youtubedl(query, ['-f bestaudio'], {maxBuffer: Infinity});
|
||
let video_filename;
|
||
dl.on('info', function(info) {
|
||
// message.channel.send('Downloading `'+info.filename+ '`\n Size: `'+info.size+'`');
|
||
dl.pipe(fs.createWriteStream('./music/'+info._filename));
|
||
video_filename = info._filename;
|
||
});
|
||
dl.on('end', function() {
|
||
music.push(video_filename);
|
||
// message.channel.send('Download finished.');
|
||
message.channel.send('📁 **Added** `'+video_filename+'` **to the music collection.** 💿')
|
||
});
|
||
} else {
|
||
message.reply('ℹ **Usage:** `!ytdl <youtube URL or search query>`');
|
||
}
|
||
}
|
||
},
|
||
soundfonts: {
|
||
aliases: ["sf"],
|
||
description: "modifies soundfont config",
|
||
hidden: true,
|
||
execute: function (message, args, txt) {
|
||
if (args[1] === 'cfg') {
|
||
fs.readFile('./soundfonts.cfg', 'utf8', (err, data)=> {
|
||
message.channel.send('Contents of soundfont config:\n`'+data+'`');
|
||
});
|
||
} else if (args[1] === 'list') {
|
||
fs.readdir('./soundfonts/', (err, files)=>{
|
||
message.channel.send('Available soundfonts: \n`'+files.join('\n')+'`');
|
||
});
|
||
} else if (args[1] === 'set') {
|
||
args.shift();
|
||
let newcfg = "";
|
||
args.forEach(filename => {
|
||
if (fs.existsSync('./soundfonts/'+filename)) {
|
||
if (filename.split('.').pop() === "cfg") {
|
||
newcfg += 'source ./soundfonts/'+filename+'\n';
|
||
} else {
|
||
newcfg += 'soundfont ./soundfonts/'+filename+'\n';
|
||
}
|
||
} else {
|
||
message.channel.send('err: soundfont `'+filename+'` doesn\'t exist');
|
||
}
|
||
});
|
||
fs.writeFile('./soundfonts.cfg', newcfg, ()=>{
|
||
message.channel.send('Saved new soundfont config with the following contents:\n`'+newcfg+'`');
|
||
});
|
||
} else {
|
||
message.channel.send('`!sf cfg` shows soundfont config; `!sf list` shows available soundfonts; `!sf set` writes new soundfont config');
|
||
}
|
||
}
|
||
},
|
||
}
|
||
|
||
const songCommands = {
|
||
song: function (message, args, txt) {
|
||
message.channel.send('🎶 **Currently playing:** `'+myVoiceConnection.dispatcher.songname+'`');
|
||
/*const embed = {
|
||
color: client.guilds.get(myGuildID).me.colorRole.color,
|
||
author: {name: "🎶 **Currently playing**"},
|
||
description: `**${myVoiceConnection.dispatcher.songname}**`,
|
||
fields: []
|
||
}
|
||
const metadata = myVoiceConnection.dispatcher.meta;
|
||
if (metadata) {
|
||
embed.fields.push({name: "Duration", value: metadata.duration});
|
||
}
|
||
message.channel.send({embed});*/
|
||
},
|
||
pause: function (message, args, txt) {
|
||
myVoiceConnection.dispatcher.pause();
|
||
message.react('🆗');
|
||
},
|
||
resume: function (message, args, txt) {
|
||
myVoiceConnection.dispatcher.resume();
|
||
message.react('🆗');
|
||
},
|
||
volume: function (message, args, txt) {
|
||
if (!isNaN(args[1])) myVoiceConnection.dispatcher.setVolume(args[1]*0.01);
|
||
message.channel.send('🔊 **Volume:** `'+myVoiceConnection.dispatcher._volume*100+'%`');
|
||
},
|
||
time: function (message, args, txt) {
|
||
message.channel.send('⏱ **Time Elapsed:** `'+getYoutubeLikeToDisplay(myVoiceConnection.dispatcher.time)+'`');
|
||
},
|
||
stop: function (message, args, txt) {
|
||
myVoiceConnection.dispatcher.end('!stop command');
|
||
message.react('🆗');
|
||
}
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
client.on('message', message => {
|
||
if (!message.content.startsWith(cmdChar)) return;
|
||
try {
|
||
const args = message.content.split(' ');
|
||
const cmd = args[0].slice(1).toLowerCase();
|
||
const txt = (i) => {return args.slice(i).join(' ')};
|
||
|
||
for (const commandName in commands) {
|
||
const command = commands[commandName];
|
||
if ( commandName === cmd ||command.aliases && (command.aliases.includes(cmd)) )
|
||
command.execute(message, args, txt);
|
||
}
|
||
|
||
for (const commandName in songCommands) {
|
||
if (commandName === cmd) {
|
||
if (myVoiceConnection && myVoiceConnection.dispatcher) {
|
||
songCommands[commandName](message, args, txt);
|
||
} else {
|
||
message.channel.send('🚫 **Nothing is playing.**');
|
||
}
|
||
}
|
||
}
|
||
} catch (e) {
|
||
message.reply('💥 **An error has been encountered while processing your command.** 💥');
|
||
console.error(colors.red(`Command failure with message "${message.content}": `+e.stack));
|
||
}
|
||
});
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
// Utility Functions
|
||
////////////////////////////////////////////////////////////////////////////////
|
||
Array.prototype.random = function () {
|
||
return this[Math.floor(Math.random()*this.length)];
|
||
}
|
||
|
||
/*Array.prototype.search = function (query) {
|
||
let results = [];
|
||
this.forEach(item => {
|
||
if (item.toLowerCase().includes(query.toLowerCase())) results.push(item);
|
||
});
|
||
return results;
|
||
}*/
|
||
|
||
Array.prototype.search = function (query) {
|
||
return this.filter( item => item.toLowerCase().includes(query.toLowerCase()) );
|
||
}
|
||
|
||
|
||
function getYoutubeLikeToDisplay(millisec) {
|
||
var seconds = (millisec / 1000).toFixed(0);
|
||
var minutes = Math.floor(seconds / 60);
|
||
var hours = "";
|
||
if (minutes > 59) {
|
||
hours = Math.floor(minutes / 60);
|
||
hours = (hours >= 10) ? hours : "0" + hours;
|
||
minutes = minutes - (hours * 60);
|
||
minutes = (minutes >= 10) ? minutes : "0" + minutes;
|
||
}
|
||
|
||
seconds = Math.floor(seconds % 60);
|
||
seconds = (seconds >= 10) ? seconds : "0" + seconds;
|
||
if (hours != "") {
|
||
return hours + ":" + minutes + ":" + seconds;
|
||
}
|
||
return minutes + ":" + seconds;
|
||
}
|
||
////////////////////////////////////////////////////////////////////////////////
|
||
client.on('error', error => console.error(error));
|