add simple music functionality

This commit is contained in:
Ayden Jahola 2025-09-19 23:54:20 +01:00
parent 51dcdc7406
commit f0a936690b
7 changed files with 140 additions and 36 deletions

View file

@ -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", "."]

31
commands/music/play.js Normal file
View file

@ -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.");
}
},
};

18
commands/music/queue.js Normal file
View file

@ -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")}`);
},
};

17
commands/music/skip.js Normal file
View file

@ -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.");
}
},
};

17
commands/music/stop.js Normal file
View file

@ -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.");
}
},
};

View file

@ -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);

View file

@ -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",