From a1dc6a89b19d75d1f04eaf36179ed9705808418e Mon Sep 17 00:00:00 2001 From: toast-ts <96593068+toast-ts@users.noreply.github.com> Date: Sun, 25 Jun 2023 03:47:42 +1000 Subject: [PATCH] Alpha stage of audio player system --- package.json | 5 +++ src/client.ts | 12 +++++- src/commands/music.ts | 83 +++++++++++++++++++++++++++++++++++++ src/commands/statistics.ts | 13 +----- src/index.ts | 22 ++++++++-- src/typings/interfaces.d.ts | 4 ++ 6 files changed, 124 insertions(+), 15 deletions(-) create mode 100644 src/commands/music.ts diff --git a/package.json b/package.json index eb7393b..65058bc 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,12 @@ "dependencies": { "axios": "1.4.0", "canvas": "2.11.2", + "@discord-player/extractor": "4.3.1", + "@ffmpeg-installer/ffmpeg": "1.1.0", + "@discordjs/opus": "0.9.0", + "discord-player": "6.5.0", "discord.js": "14.11.0", + "ytdl-core": "4.11.4", "moment": "2.29.4", "ms": "2.1.3", "mongoose": "7.3.1", diff --git a/src/client.ts b/src/client.ts index 26df235..bc4ba12 100644 --- a/src/client.ts +++ b/src/client.ts @@ -54,7 +54,8 @@ export default class TClient extends Client { GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers, GatewayIntentBits.GuildModeration, GatewayIntentBits.GuildInvites, GatewayIntentBits.GuildMessageReactions, GatewayIntentBits.GuildPresences, - GatewayIntentBits.MessageContent, GatewayIntentBits.GuildMessages + GatewayIntentBits.MessageContent, GatewayIntentBits.GuildMessages, + GatewayIntentBits.GuildVoiceStates ], partials: [ Partials.Channel, Partials.Reaction, Partials.Message ], allowedMentions: {users:[],roles:[]} @@ -205,6 +206,15 @@ export default class TClient extends Client { (this.channels.resolve(DCChannelID) as Discord.TextChannel).send(`**${YTChannelName}** just uploaded a video!\n${Data.feed.entry[0].link._attributes.href}`) } } + // Bytes conversion + formatBytes(bytes:number, decimals:number = 2) { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const dm = decimals < 0 ? 0 : decimals; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; + }; } export class WClient extends WebhookClient { diff --git a/src/commands/music.ts b/src/commands/music.ts new file mode 100644 index 0000000..089e15b --- /dev/null +++ b/src/commands/music.ts @@ -0,0 +1,83 @@ +import Discord from 'discord.js'; +import TClient from '../client.js'; +import {Player,useTimeline,useQueue} from 'discord-player'; +import {SpotifyExtractor} from '@discord-player/extractor'; +export default { + async run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){ + if (!client.isStaff(interaction.member) && !client.config.whitelist.includes(interaction.member.id)) return interaction.reply('This command is in early stages of development, some parts may be missing or broken.\nIt has been restricted to staff for time-being.'); + const player = Player.singleton(client); + await player.extractors.register(SpotifyExtractor, { + clientId: client.tokens.dontlookatme.client, + clientSecret: client.tokens.dontlookatme.secret + }); + const voiceCh = interaction.member.voice.channel; + if (!voiceCh) return interaction.reply('Please join a voice channel first to use the command.'); + player.nodes.create(interaction.guildId, { + metadata: { + channel: interaction.channel, + client: interaction.guild.members.me, + requestedBy: interaction.member + }, + selfDeaf: true, + volume: 75, + skipOnNoStream: true, + leaveOnEnd: false, + leaveOnEmpty: false, + bufferingTimeout: 30000, + connectionTimeout: 25000, + strategy: 'FIFO' + }); + ({ + play: async()=>{ + const url = interaction.options.getString('url'); + if (!url.includes('https://open.spotify.com/')) return interaction.reply('Sorry, I can\'t play that. I can only accept Spotify links that contains `https://open.spotify.com/`'); + player.play(interaction.member.voice.channel, url); + await interaction.reply(`Added the song to the queue.`); + }, + stop: async()=>{ + player.destroy(); + await interaction.reply('Player destroyed.') + }, + now_playing: ()=>{ + const {volume,timestamp,track} = useTimeline(interaction.guildId); + interaction.reply({embeds:[ + new client.embed().setColor(client.config.embedColor).setTitle(`${track.title} - ${track.author}`).setThumbnail(track.thumbnail).addFields( + {name: 'Timestamp', value: `**${timestamp.current.label}**/**${timestamp.total.label}**`, inline: true}, + {name: 'Volume', value: `${volume}%`, inline: true} + ) + ]}) + }, + volume: ()=>{ + const vol = interaction.options.getNumber('percentage'); + const queue = useQueue(interaction.guildId); + queue.node.setVolume(vol); + interaction.reply(`Successfully adjusted the player's volume to ${vol}%`) + } + } as any)[interaction.options.getSubcommand()](); + }, + data: new Discord.SlashCommandBuilder() + .setName('music') + .setDescription('Music module') + .addSubcommand(x=>x + .setName('play') + .setDescription('Play a Spotify song') + .addStringOption(x=>x + .setName('url') + .setDescription('Spotify URL') + .setRequired(true))) + .addSubcommand(x=>x + .setName('stop') + .setDescription('Stop playing music and disconnect the bot from voice channel')) + .addSubcommand(x=>x + .setName('now_playing') + .setDescription('Check what song is currently playing')) + .addSubcommand(x=>x + .setName('volume') + .setDescription('Adjust the player\'s volume') + .addNumberOption(x=>x + .setName('percentage') + .setDescription('Volume level to adjust, ranges from 5 to 100, default is 75') + .setMaxValue(100) + .setMinValue(5) + .setRequired(true))) +} \ No newline at end of file diff --git a/src/commands/statistics.ts b/src/commands/statistics.ts index 94f2d55..1af3745 100644 --- a/src/commands/statistics.ts +++ b/src/commands/statistics.ts @@ -6,15 +6,6 @@ import os from 'node:os'; export default { async run(client: TClient, interaction: Discord.ChatInputCommandInteraction<'cached'>){ const waitForData = await interaction.reply({content: '', fetchReply:true}) - // Bytes conversion - function formatBytes(bytes:number, decimals:number = 2) { - if (bytes === 0) return '0 Bytes'; - const k = 1024; - const dm = decimals < 0 ? 0 : decimals; - const sizes = ['Bytes', 'KB', 'MB', 'GB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; - }; const cpu = await si.cpu(); const ram = await si.mem(); const osInfo = await si.osInfo(); @@ -56,8 +47,8 @@ export default { {name: '> __Host__', value: [ `**Operating System:** ${osInfo.distro + ' ' + osInfo.release}`, `**CPU:** ${cpu.manufacturer} ${cpu.brand}`, - `**Memory:** ${formatBytes(ram.used)}/${formatBytes(ram.total)}`, - `**NodeJS:** ${formatBytes(process.memoryUsage().heapUsed)}/${formatBytes(process.memoryUsage().heapTotal)}`, + `**Memory:** ${client.formatBytes(ram.used)}/${client.formatBytes(ram.total)}`, + `**NodeJS:** ${client.formatBytes(process.memoryUsage().heapUsed)}/${client.formatBytes(process.memoryUsage().heapTotal)}`, `**Load Usage:**\nUser: ${currentLoad.currentLoadUser.toFixed(1)}%\nSystem: ${currentLoad.currentLoadSystem.toFixed(1)}%`, `**Uptime:**\nHost: ${client.formatTime((os.uptime()*1000), 2, {longNames: true, commas: true})}\nBot: ${client.formatTime(client.uptime as number, 2, {commas: true, longNames: true})}` ].join('\n')} diff --git a/src/index.ts b/src/index.ts index f099c08..86f29a3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,8 @@ import TClient from './client.js'; const client = new TClient; client.init(); import MPLoop from './MPLoop.js'; +import {Player} from 'discord-player'; +const player = Player.singleton(client); import {writeFileSync, readFileSync} from 'node:fs'; client.on('ready', async()=>{ @@ -25,18 +27,32 @@ client.on('ready', async()=>{ console.timeEnd('Startup') }) -// Handle errors -function DZ(error:Error, location:string){// Yes, I may have shiternet but I don't need to wake up to like a hundred messages or so. +// Error handler +function DZ(error:Error, type:string){// Yes, I may have shiternet but I don't need to wake up to like a hundred messages or so. if (['ENOTFOUND discord.com', 'EAI_AGAIN discord.com'].includes(error.message)) return; //console.error(error); const channel = client.channels.resolve(client.config.mainServer.channels.errors) as Discord.TextChannel | null; - channel?.send({embeds: [new client.embed().setColor('#420420').setTitle('Error caught!').setFooter({text: location}).setDescription(`**Error:** \`${error.message}\`\n\n**Stack:** \`${`${error.stack}`.slice(0, 2500)}\``)]}) + // vvv Oh yes, that looks really hot. + channel?.send({embeds: [new client.embed().setColor('#560000').setTitle('Error caught!').setFooter({text: 'Error type: ' + type}).setDescription(`**Error:**\n\`\`\`${error.message}\`\`\`**Stack:**\n\`\`\`${`${error.stack}`.slice(0, 2500)}\`\`\``)]}) } process.on('unhandledRejection', (error: Error)=>DZ(error, 'unhandledRejection')); process.on('uncaughtException', (error: Error)=>DZ(error, 'uncaughtException')); process.on('error', (error: Error)=>DZ(error, 'process-error')); client.on('error', (error: Error)=>DZ(error, 'client-error')); +// Audio Player event handling +player.events.on('playerStart', (queue,track)=>queue.channel.send({embeds:[new client.embed().setColor(client.config.embedColor).setTitle(`${track.raw.title} - ${track.raw.author}`).setFooter({text:`Playing in ${queue.channel.name}`}).setThumbnail(track.raw.thumbnail)]})); +player.events.on('playerFinish', (queue,track)=>{ + if (queue.tracks.size < 1) return queue.channel.send('There\'s no songs left in the queue, leaving voice channel in 15 seconds.'); + setTimeout(()=>queue.connection.disconnect(), 15000) +}) +player.events.on('audioTrackAdd', (queue,track)=>queue.channel.send({embeds:[new client.embed().setColor(client.config.embedColorGreen).setTitle(`${track.raw.title} - ${track.raw.author}`).setFooter({text:`Added to queue`}).setThumbnail(track.raw.thumbnail)]})); +/* player.events.on('debug', (queue,message)=>{ + console.log(client.logTime(), message) +}) */ +player.events.on('playerError', (queue, error)=>DZ(error, 'playerError')); +player.events.on('error', (queue, error)=>DZ(error, 'playerInternalError')); + // YouTube Upload notification and Daggerwin MP loop setInterval(()=>MPLoop(client, client.config.MPStatsLocation.channel, client.config.MPStatsLocation.message, 'Daggerwin'), 60000); setInterval(async()=>{ diff --git a/src/typings/interfaces.d.ts b/src/typings/interfaces.d.ts index 05e2540..16f2af8 100644 --- a/src/typings/interfaces.d.ts +++ b/src/typings/interfaces.d.ts @@ -116,6 +116,10 @@ export interface Tokens { main: string beta: string toast: string + dontlookatme: { + client: string, + secret: string + } octokit: string webhook_url: string webhook_url_test: string