// Require the necessary discord.js classes const Discord = require('discord.js'); const Voice = require('@discordjs/voice'); const { token } = require('../config.json'); const { exec } = require('child_process'); const { promisify } = require('util'); const got = require('got'); const youtubedl = require('youtube-dl') const bot = new Discord.Client({ intents: [ Discord.Intents.FLAGS.GUILDS, Discord.Intents.FLAGS.GUILD_MESSAGES, Discord.Intents.FLAGS.GUILD_VOICE_STATES, Discord.Intents.FLAGS.GUILD_MESSAGE_REACTIONS ], }); bot.once('ready', () => { console.log('Ready!'); }); const prefix = ';'; const embedColor = 0xa0ffa5; const foggyImg = 'https://cdn.discordapp.com/attachments/789023763396165633/898908687299657728/552bb191-93e3-4eaa-b935-d5031f3845e7.gif'; const ffprobeRegex = /Audio: ([^,\n]+), (\d+) ?Hz, ?([^,\n]+), ?([^,\n]+)(, ?([^,\n]+))?/; async function checkVoiceChannel(msg) { if (!msg.guild) return; const clientVoiceState = msg.guild.me.voice; const memberVoiceState = msg.member.voice; const voiceConnection = await Voice.getVoiceConnection(msg.guild.id); if (clientVoiceState.channelId === memberVoiceState.channelId && voiceConnection) { console.log('reusing connection'); return voiceConnection; } if (!memberVoiceState.channelId) { msg.channel.send('you aren\'t in a voice channel!'); return; } if (clientVoiceState.channelId && clientVoiceState.channelId !== memberVoiceState.channelId) { msg.channel.send('you are in a different voice channel!'); return; } console.log('creating new connection'); return Voice.joinVoiceChannel({ channelId: msg.member.voice.channelId, guildId: msg.guild.id, adapterCreator: msg.guild.voiceAdapterCreator, }); } /* { url: string, } */ let queue = {}; let players = {}; let playerStartTime = {}; function advanceQueue(id, channel, subscription, connection) { if (queue[id][1]) { const song = queue[id][1]; const embed = new Discord.MessageEmbed() .setDescription(`now playing: **${song.titleFormat}**`) .setThumbnail(song.thumbnail) .setColor(embedColor) .setAuthor('foggy ♫', foggyImg); channel.send({embeds: [embed]}); play(queue[id][1], id, connection); queue[id] = queue[id].slice(1); } else { channel.send('no songs left, leaving'); console.log('destroyig'); subscription.unsubscribe(); delete players[id]; connection.destroy(); queue[id] = []; } } async function play(song, id, connection, channel) { // const stream = ytdl(url, { filter: 'audioonly' }); let player; let subscription; let url = song.directUrl; if (song.redownload) { let err, out try { err, out = await promisify(exec)('youtube-dl -f bestaudio --youtube-skip-dash-manifest --force-ipv4 -g "' + song.originalUrl + '"'); } catch(err_) { err = err_; } if (err) { console.log(err); channel.send(`failed to retrieve youtube-dl info!: \`\`\`${err}\`\`\``); advanceQueue(id, channel, subscription, connection); return; } url = out.stdout; } const stream = got(url, {isStream: true}); const resource = Voice.createAudioResource(stream, { inputType: Voice.StreamType.Arbitrary }); if (players[id]) { console.log('reusing player'); player = players[id]; } else { console.log('creating new player'); player = Voice.createAudioPlayer(); players[id] = player; subscription = connection.subscribe(player); player.on(Voice.AudioPlayerStatus.Idle, () => { advanceQueue(id, channel, subscription, connection); }); player.on(Voice.AudioPlayerStatus.Playing, () => { playerStartTime[id] = Date.now(); }) } player.play(resource); return player; } function rawYTDLQueue(info) { let song = { originalUrl: info.webpage_url || url, directUrl: info.url, redownload: true, title: info.title, thumbnail: info.thumbnail, titleFormat: info.title, duration: info._duration_raw, fileType: info.acodec, bitrate: info.abr, }; if (song.track && song.artist && song.album) { song.titleFormat = `${song.artist} - [${song.track}](${info.webpage_url}) from ${song.album}`; } else { if (song.originalUrl.startsWith('http')) song.titleFormat = `[${song.title}](${song.originalUrl})`; } return song; } async function queueUp(url, id) { if (!queue[id]) queue[id] = []; let song = {}; let err, info; try { err, info = await promisify(youtubedl.getInfo)(url, ['--force-ipv4']); } catch(err_) { err = err_; } if (err) { return [null, `failed to retrieve youtube-dl info!: \`\`\`${err}\`\`\``]; } else { if (info.length) { info.forEach(i => { let song = rawYTDLQueue(i); queue[id].push(song); }); return [info.length, null]; } else { song = rawYTDLQueue(info); } } queue[id].push(song) return [1, null]; } bot.on('messageCreate', async (msg) => { const content = msg.content; const params = content.replace(prefix, '').split(' '); const cmd = params[0]; if (!msg.guild) return; if ((cmd === 'play' || cmd === 'p') && params[1]) { const connection = await checkVoiceChannel(msg); if (!connection) return; const url = params.slice(1).join(" "); console.log('queueing'); msg.channel.send('queueing...'); let [q, e] = await queueUp(url, msg.guild.id); if (!q && e) [q, e] = await queueUp('ytsearch:' + url, msg.guild.id); if (!q && e) return msg.channel.send(e); const song = queue[msg.guild.id][queue[msg.guild.id].length - 1]; if (queue[msg.guild.id].length === 1) { const embed = new Discord.MessageEmbed() .setDescription(`now playing: **${song.titleFormat}**`) .setThumbnail(song.thumbnail) .setColor(embedColor) .setAuthor('foggy ♫', foggyImg); msg.channel.send({embeds: [embed]}); play(queue[msg.guild.id][0], msg.guild.id, connection, msg.channel); } else { let queueString = `queued: **${song.titleFormat}** _at position ${queue[msg.guild.id].length - 1}_`; if (q > 1) queueString = `queued **${q} songs** _at positions ${queue[msg.guild.id].length - q} - ${queue[msg.guild.id].length - 1}_`; const embed = new Discord.MessageEmbed() .setDescription(queueString) .setColor(embedColor) .setAuthor('foggy ♫', foggyImg); msg.channel.send({embeds: [embed]}); if (!players[msg.guild.id] || players[msg.guild.id].state === Voice.AudioPlayerStatus.Idle || players[msg.guild.id].state === Voice.AudioPlayerStatus.Paused) { play(queue[msg.guild.id][0], msg.guild.id, connection, msg.channel); } } } else if (cmd === 'skip' || cmd === 's') { const player = players[msg.guild.id]; if (!player) return msg.channel.send('the bot isn\'t playing any music!'); const song = queue[msg.guild.id][0]; const embed = new Discord.MessageEmbed() .setDescription(`skipped **${song.titleFormat}**`) .setColor(embedColor) .setAuthor('foggy ♫', foggyImg); msg.channel.send({embeds: [embed]}); player.stop(); } else if (cmd === 'queue' || cmd === 'q') { const q = queue[msg.guild.id] || []; if (q.length === 0) { msg.channel.send('no songs queued!'); } else { msg.channel.send('```' + q.map((m, i) => `${i === 0 ? 'now playing:' : i + '.'} ${m.title}`).join('\n') + '```'); } } else if (cmd === 'np' || cmd === 'nowplaying') { const song = (queue[msg.guild.id] || [])[0]; if (!song) return msg.channel.send('no song playing!'); let progress = (Date.now() - (playerStartTime[msg.guild.id] || 0)) / 1000 / song.duration; let progressLength = 20; let progressStr = ''; if (song.duration === -1) { progressStr = '🔘 `where?..`'; } else if (progress < 0 || progress > 1) { progressStr = '🔘 `buffering,,`'; } else { progressStr = `${'▬'.repeat(Math.floor(Math.abs(progress) * progressLength))}🔘${'▬'.repeat(Math.floor((1 - Math.abs(progress)) * progressLength))}`; } let embed = new Discord.MessageEmbed() .setDescription(`now playing: **${song.titleFormat}**\n${progressStr}`) .setFooter(`${song.fileType} • ${song.bitrate}KB/s`) .setThumbnail(song.thumbnail) .setColor(embedColor) .setAuthor('foggy ♫', foggyImg); if (queue[msg.guild.id].length !== 1) { embed = embed.addField('up next', '```' + queue[msg.guild.id].slice(1, 5).map((m, i) => { if (i === 3) { return '...'; } else { return `${i + 1}. ${m.title}`; } }).join('\n') + '```') } msg.channel.send({embeds: [embed]}); } }); // Login to Discord with your bot's token bot.login(token);