diff --git a/Dockerfile b/Dockerfile index 96fcb68..caae92a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,16 @@ +FROM jrottenberg/ffmpeg:8-alpine AS ffmpeg + FROM node:current-alpine +COPY --from=ffmpeg /usr/local/bin/ffmpeg /usr/local/bin/ +COPY --from=ffmpeg /usr/local/bin/ffprobe /usr/local/bin/ + +RUN apk add --no-cache \ + python3 \ + make \ + g++ \ + && npm install -g npm@latest + WORKDIR /usr/src/app COPY package*.json ./ @@ -8,4 +19,4 @@ RUN npm install COPY . . -CMD ["node", "."] +CMD ["node", "."] \ No newline at end of file diff --git a/commands/music/play.js b/commands/music/play.js new file mode 100644 index 0000000..7ea7c4b --- /dev/null +++ b/commands/music/play.js @@ -0,0 +1,31 @@ +const { SlashCommandBuilder } = require("discord.js"); + +module.exports = { + data: new SlashCommandBuilder() + .setName("play") + .setDescription("Plays a song from YouTube, Spotify, or SoundCloud.") + .addStringOption((option) => + option + .setName("query") + .setDescription("Song name or URL") + .setRequired(true) + ), + async execute(interaction, client) { + await interaction.deferReply(); + const query = interaction.options.getString("query"); + const voiceChannel = interaction.member.voice.channel; + if (!voiceChannel) { + return interaction.followUp("❌ You need to be in a voice channel!"); + } + try { + await client.distube.play(voiceChannel, query, { + textChannel: interaction.channel, + member: interaction.member, + }); + await interaction.followUp(`🔍 Searching: \`${query}\``); + } catch (error) { + console.error(error); + interaction.followUp("❌ Failed to play the song."); + } + }, +}; diff --git a/commands/music/queue.js b/commands/music/queue.js new file mode 100644 index 0000000..2573f3b --- /dev/null +++ b/commands/music/queue.js @@ -0,0 +1,18 @@ +const { SlashCommandBuilder } = require("discord.js"); + +module.exports = { + data: new SlashCommandBuilder() + .setName("queue") + .setDescription("Shows the current music queue."), + async execute(interaction, client) { + const queue = client.distube.getQueue(interaction.guildId); + if (!queue || queue.songs.length === 0) { + return interaction.reply("❌ The queue is empty!"); + } + const tracks = queue.songs.map( + (song, index) => + `${index + 1}. ${song.name} - \`${song.formattedDuration}\`` + ); + interaction.reply(`**Current Queue:**\n${tracks.join("\n")}`); + }, +}; diff --git a/commands/music/skip.js b/commands/music/skip.js new file mode 100644 index 0000000..4a60cc6 --- /dev/null +++ b/commands/music/skip.js @@ -0,0 +1,17 @@ +const { SlashCommandBuilder } = require("discord.js"); + +module.exports = { + data: new SlashCommandBuilder() + .setName("skip") + .setDescription("Skips the current song."), + async execute(interaction, client) { + const queue = client.distube.getQueue(interaction.guildId); + if (!queue) return interaction.reply("❌ No songs in queue!"); + try { + await queue.skip(); + interaction.reply("⏭️ Skipped the current song!"); + } catch (error) { + interaction.reply("❌ Failed to skip."); + } + }, +}; diff --git a/commands/music/stop.js b/commands/music/stop.js new file mode 100644 index 0000000..1a6522b --- /dev/null +++ b/commands/music/stop.js @@ -0,0 +1,17 @@ +const { SlashCommandBuilder } = require("discord.js"); + +module.exports = { + data: new SlashCommandBuilder() + .setName("stop") + .setDescription("Stops the music and clears the queue."), + async execute(interaction, client) { + const queue = client.distube.getQueue(interaction.guildId); + if (!queue) return interaction.reply("❌ No music is playing!"); + try { + await client.distube.stop(interaction.guildId); + interaction.reply("🛑 Stopped the player and cleared the queue!"); + } catch (error) { + interaction.reply("❌ Failed to stop."); + } + }, +}; diff --git a/index.js b/index.js index 60f3037..480f1f3 100644 --- a/index.js +++ b/index.js @@ -7,6 +7,9 @@ const { Routes, PresenceUpdateStatus, } = require("discord.js"); +const { DisTube } = require("distube"); +const { SpotifyPlugin } = require("@distube/spotify"); +const { SoundCloudPlugin } = require("@distube/soundcloud"); const mongoose = require("mongoose"); const fs = require("fs"); const path = require("path"); @@ -19,41 +22,62 @@ const client = new Client({ GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent, + GatewayIntentBits.GuildVoiceStates, ], }); client.commands = new Collection(); +// Initialize DisTube +client.distube = new DisTube(client, { + emitNewSongOnly: true, + plugins: [new SpotifyPlugin(), new SoundCloudPlugin()], +}); + +// DisTube event listeners +client.distube + .on("playSong", (queue, song) => { + queue.textChannel.send( + `🎶 Playing: **${song.name}** - \`${song.formattedDuration}\`` + ); + }) + .on("addSong", (queue, song) => { + queue.textChannel.send( + `✅ Added: **${song.name}** - \`${song.formattedDuration}\`` + ); + }) + .on("error", (channel, error) => { + console.error("DisTube error:", error); + channel.send("❌ An error occurred: " + error.message); + }) + .on("finish", (queue) => { + queue.textChannel.send("🎵 Queue finished!"); + }); + // Function to recursively read commands from subdirectories function loadCommands(dir) { const files = fs.readdirSync(dir); - for (const file of files) { const filePath = path.join(dir, file); - if (fs.statSync(filePath).isDirectory()) { - // If it's a directory, recurse into it loadCommands(filePath); } else if (file.endsWith(".js")) { - // If it's a JavaScript file, load the command const command = require(filePath); client.commands.set(command.data.name, command); } } } -// Load all commands from the commands directory and its subdirectories loadCommands(path.join(__dirname, "commands")); async function registerCommands(guildId) { const commands = client.commands.map((cmd) => cmd.data.toJSON()); const rest = new REST({ version: "10" }).setToken(process.env.BOT_TOKEN); - try { await rest.put(Routes.applicationGuildCommands(client.user.id, guildId), { body: commands, }); - console.log(`🔄 Successfully registered commands for guild: ${guildId}`); + console.log(`🔄 Registered commands for guild: ${guildId}`); } catch (error) { console.error("Error registering commands:", error); } @@ -64,9 +88,7 @@ client.once("ready", async () => { console.log(`🤖 Logged in as ${client.user.tag}`); console.log(`==============================`); - // Register commands for all existing guilds const guilds = client.guilds.cache.map((guild) => guild.id); - await Promise.all( guilds.map(async (guildId) => { await seedShopItems(guildId); @@ -75,29 +97,19 @@ client.once("ready", async () => { }) ); - // Set bot status and activity client.user.setPresence({ activities: [{ name: "Degenerate Gamers!", type: 3 }], status: PresenceUpdateStatus.Online, }); - console.log(`\n==============================\n`); }); -// Listen for new guild joins and register the guild ID in the database client.on("guildCreate", async (guild) => { try { - // Create a new entry in the ServerSettings collection with just the guildId await ServerSettings.create({ guildId: guild.id }); console.log(`✅ Registered new server: ${guild.name} (ID: ${guild.id})`); - - // seed items for new guild with guildId await seedShopItems(guild.id); - - // Seed spyfall locations for the new guild await seedSpyfallLocations(guild.id); - - // Register slash commands for the new guild await registerCommands(guild.id); } catch (error) { console.error("Error registering new server or commands:", error); @@ -112,31 +124,23 @@ mongoose client.on("interactionCreate", async (interaction) => { if (!interaction.isCommand()) return; - const command = client.commands.get(interaction.commandName); - if (!command) return; - try { await command.execute(interaction, client); } catch (err) { console.error("Error executing command:", err); - if (interaction.deferred || interaction.ephemeral) { - await interaction.followUp({ - content: "There was an error while executing this command!", - ephemeral: true, - }); + const replyOptions = { + content: "Error executing command!", + ephemeral: true, + }; + if (interaction.deferred || interaction.replied) { + await interaction.followUp(replyOptions); } else { - await interaction.reply({ - content: "There was an error while executing this command!", - ephemeral: true, - }); + await interaction.reply(replyOptions); } } }); -client.on("error", (err) => { - console.error("Client error:", err); -}); - +client.on("error", (err) => console.error("Client error:", err)); client.login(process.env.BOT_TOKEN); diff --git a/package.json b/package.json index 7437df2..9fc574f 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,14 @@ "keywords": [], "license": "MIT", "dependencies": { + "@discordjs/opus": "^0.10.0", + "@discordjs/voice": "^0.19.0", + "@distube/soundcloud": "^2.0.4", + "@distube/spotify": "^2.0.2", + "@snazzah/davey": "^0.1.6", "axios": "^1.7.7", "discord.js": "^14.15.3", + "distube": "^5.0.7", "dotenv": "^16.4.5", "express": "^4.19.2", "html-entities": "^2.5.2",