diff --git a/.gitignore b/.gitignore index e9cd46f..1b50942 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ node_modules docs .env.asli +config.js # Logs logs diff --git a/config.js.example b/config.js.example new file mode 100644 index 0000000..b7b35f9 --- /dev/null +++ b/config.js.example @@ -0,0 +1,137 @@ +module.exports = { + OWNER_IDS: [""], // Bot owner ID's + SUPPORT_SERVER: "", // Your bot support server + PREFIX_COMMANDS: { + ENABLED: true, // Enable/Disable prefix commands + DEFAULT_PREFIX: "kt?", // Default prefix for the bot + }, + INTERACTIONS: { + SLASH: true, // Should the interactions be enabled + CONTEXT: true, // Should contexts be enabled + GLOBAL: true, // Should the interactions be registered globally + TEST_GUILD_ID: "", // Guild ID where the interactions should be registered. [** Test you commands here first **] + }, + EMBED_COLORS: { + BOT_EMBED: "#068ADD", + TRANSPARENT: "#36393F", + SUCCESS: "#00A56A", + ERROR: "#D61A3C", + WARNING: "#F7E919", + }, + CACHE_SIZE: { + GUILDS: 10000, + USERS: 1000000, + MEMBERS: 1000000, + }, + MESSAGES: { + API_ERROR: "Unexpected Backend Error! Try again later or contact support server", + }, + + // PLUGINS + + AUTOMOD: { + ENABLED: true, + LOG_EMBED: "#36393F", + DM_EMBED: "#36393F", + }, + + DASHBOARD: { + enabled: true, // enable or disable dashboard + baseURL: "http://localhost:8080", // base url + failureURL: "http://localhost:8080", // failure redirect url + port: "8080", // port to run the bot on + }, + + ECONOMY: { + ENABLED: true, + CURRENCY: "β‚ͺ", + DAILY_COINS: 1000, // coins to be received by daily command + MIN_BEG_AMOUNT: 1000, // minimum coins to be received when beg command is used + MAX_BEG_AMOUNT: 25000, // maximum coins to be received when beg command is used + }, + + MUSIC: { + ENABLED: true, + IDLE_TIME: 120, // Time in seconds before the bot disconnects from an idle voice channel + DEFAULT_VOLUME: 60, // Default player volume 1-100 + MAX_SEARCH_RESULTS: 100, + DEFAULT_SOURCE: "ytsearch", // ytsearch = Youtube, ytmsearch = Youtube Music, scsearch = SoundCloud, spsearch = Spotify + // Add any number of lavalink nodes here + // Refer to https://github.com/lavalink-devs/Lavalink to host your own lavalink server + LAVALINK_NODES: [ + { + id: "Local Node", + host: "localhost", + port: 2333, + authorization: "youshallnotpass", + secure: false, + retryAmount: 20, // Number of reconnection attempts + retryDelay: 30000 // Delay (in ms) between reconnection attempts + }, + ], + }, + + GIVEAWAYS: { + ENABLED: true, + REACTION: "🎁", + START_EMBED: "#FF468A", + END_EMBED: "#FF468A", + }, + + IMAGE: { + ENABLED: true, + BASE_API: "https://strangeapi.hostz.me/api", + }, + + INVITE: { + ENABLED: true, + }, + + MODERATION: { + ENABLED: true, + EMBED_COLORS: { + TIMEOUT: "#102027", + UNTIMEOUT: "#4B636E", + KICK: "#FF7961", + SOFTBAN: "#AF4448", + BAN: "#D32F2F", + UNBAN: "#00C853", + VMUTE: "#102027", + VUNMUTE: "#4B636E", + DEAFEN: "#102027", + UNDEAFEN: "#4B636E", + DISCONNECT: "RANDOM", + MOVE: "RANDOM", + }, + }, + + PRESENCE: { + ENABLED: true, // Whether or not the bot should update its status + STATUS: "online", // The bot's status [online, idle, dnd, invisible] + TYPE: "LISTENING", // Status type for the bot [ CUSTOM | PLAYING | LISTENING | WATCHING | COMPETING ] + MESSAGE: "/play with {members} members in {servers} servers", // Your bot status message (note: in custom status type you won't have "Playing", "Listening", "Competing" prefix) + }, + + STATS: { + ENABLED: true, + XP_COOLDOWN: 5, // Cooldown in seconds between messages + DEFAULT_LVL_UP_MSG: "{member:tag}, You just advanced to **Level {level}**", + }, + + SUGGESTIONS: { + ENABLED: true, // Should the suggestion system be enabled + EMOJI: { + UP_VOTE: "⬆️", + DOWN_VOTE: "⬇️", + }, + DEFAULT_EMBED: "#4F545C", + APPROVED_EMBED: "#43B581", + DENIED_EMBED: "#F04747", + }, + + TICKET: { + ENABLED: true, + CREATE_EMBED: "#068ADD", + CLOSE_EMBED: "#068ADD", + }, +}; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 1b216a1..ddaf533 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,16 @@ { "name": "TinashaBot", - "version": "5.6.2", + "version": "5.7.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "TinashaBot", - "version": "5.6.0", + "version": "5.6.2", "license": "ISC", "dependencies": { "@google/generative-ai": "^0.14.0", "@lavaclient/plugin-queue": "^0.0.1", - "@lavaclient/queue": "^2.1.1", - "@lavaclient/spotify": "^3.1.0", "@lavaclient/types": "^2.1.1", "@vitalets/google-translate-api": "^9.2.0", "common-tags": "^1.8.2", @@ -28,7 +26,7 @@ "express-session": "^1.18.0", "fixedsize-map": "^1.0.1", "iso-639-1": "^3.1.0", - "lavaclient": "^5.0.0-rc.3", + "lavalink-client": "^2.4.0", "module-alias": "^2.2.3", "moment": "^2.30.1", "mongoose": "^8.1.1", @@ -192,13 +190,15 @@ "version": "0.17.6", "resolved": "https://registry.npmjs.org/@effect/data/-/data-0.17.6.tgz", "integrity": "sha512-/vwz7Jh05eS0qY8kczR/YyJd18d0C+PMtUkAealh4f6gwvhABLGCnktNJTcq/+UHxY0Cbv18r5uaJ4+7PPC+WQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@effect/schema": { "version": "0.49.4", "resolved": "https://registry.npmjs.org/@effect/schema/-/schema-0.49.4.tgz", "integrity": "sha512-Em5qFV7kXfHpt6n89B2Zwd0ccGgfFpZbBAfQuGPdw/zY18k01Tl3ufKfBA6fFphKQiWrU6JS9btTlq1+/WRRIg==", "license": "MIT", + "peer": true, "peerDependencies": { "effect": "2.0.0-next.56", "fast-check": "^3.13.2" @@ -343,30 +343,6 @@ "lavalink-protocol": "1.0.1" } }, - "node_modules/@lavaclient/queue": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@lavaclient/queue/-/queue-2.1.1.tgz", - "integrity": "sha512-2I+T+4xpb6I4xh4GjCCY/F2zazoHw/RectbKWlBZYSEMGoIGbHfOu81yi4fModigSVG0+4br8NnwHLiQX+XNcg==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.4.0" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/@lavaclient/spotify": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@lavaclient/spotify/-/spotify-3.1.0.tgz", - "integrity": "sha512-B9AwZVyxScjJnJWJa4zMylF2i2/UOvDKL7lHWMxcezBMvOqjM+rZMr4ZTJj179qdpOaQ7zc8mBfRYSXGWJIWmA==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.4.0" - }, - "engines": { - "node": ">=16" - } - }, "node_modules/@lavaclient/types": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@lavaclient/types/-/types-2.1.1.tgz", @@ -2469,6 +2445,7 @@ "resolved": "https://registry.npmjs.org/lavaclient/-/lavaclient-5.0.0-rc.3.tgz", "integrity": "sha512-zSB0wK9SEhXwWd918ije4UjjqMeDlP7Nry8mo5UFvQlD2YMFKxJJuKTJKc9VwgBhBjRVB0cCujAlru7NsTRdDg==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@effect/schema": "^0.49.4", "lavalink-api-client": "1.0.1", @@ -2486,6 +2463,7 @@ "resolved": "https://registry.npmjs.org/lavalink-api-client/-/lavalink-api-client-1.0.1.tgz", "integrity": "sha512-ur4rrOf68mEKQQ6SwDdzG/8W+ew6zpguLPxny14DjEGxDz/mXvDtILovXpFRR5S4d8Ko9xZygIJ9YqEQfgKC/w==", "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.6.0" }, @@ -2501,6 +2479,7 @@ "resolved": "https://registry.npmjs.org/lavalink-protocol/-/lavalink-protocol-1.0.2.tgz", "integrity": "sha512-OLd4ZDrYeT35UxJY1K/Pu6PjqPnHN0X5HutcQ7E/UNz+3YBvieaki9z0gk08gPwsxkBhYjRKDNIVNTAT0sH0fA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@effect/data": "0.17.6", "@effect/schema": "^0.49.4", @@ -2515,6 +2494,7 @@ "resolved": "https://registry.npmjs.org/lavalink-ws-client/-/lavalink-ws-client-1.0.1.tgz", "integrity": "sha512-jXntoMJe/lk4vuO3RVfSVQPNx28CwammqP4I3+pi5uIF/WHBAJxzwyIzDCmUPIIXcuEveUH53bs2aE8uCq9Sig==", "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.6.0", "typed-emitter": "^2.1.0", @@ -2528,6 +2508,20 @@ "lavalink-protocol": "1.0.2" } }, + "node_modules/lavalink-client": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lavalink-client/-/lavalink-client-2.4.1.tgz", + "integrity": "sha512-wrRkbzILdjRzadAbdGM9O3bgLEabAebGci2YOUCcQoCumEjAMPz+rj7Xepa+y0oydnodyYZvBrB9ZeSdy7rzuw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.7.0", + "ws": "^8.18.0" + }, + "engines": { + "bun": ">=1.0.0", + "node": ">=18.0.0" + } + }, "node_modules/lavalink-protocol": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/lavalink-protocol/-/lavalink-protocol-1.0.1.tgz", @@ -3597,6 +3591,7 @@ "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", "license": "Apache-2.0", "optional": true, + "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -4150,9 +4145,9 @@ "license": "MIT" }, "node_modules/tslib": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, "node_modules/twemoji-parser": { @@ -4204,6 +4199,7 @@ "resolved": "https://registry.npmjs.org/typed-emitter/-/typed-emitter-2.1.0.tgz", "integrity": "sha512-g/KzbYKbH5C2vPkaXGu8DJlHrGKHLsM25Zg9WuC9pMGfuvT+X25tZQWo5fK1BjBm8+UrVE9LDCvaY0CQk+fXDA==", "license": "MIT", + "peer": true, "optionalDependencies": { "rxjs": "*" } @@ -4356,9 +4352,10 @@ "license": "ISC" }, "node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "license": "MIT", "engines": { "node": ">=10.0.0" }, diff --git a/package.json b/package.json index ab26202..e24a45f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "TinashaBot", - "version": "5.6.2", + "version": "5.7.0", "description": "multipurpose discord bot built using discord-js", "main": "bot.js", "author": "Amane", @@ -24,8 +24,6 @@ "dependencies": { "@google/generative-ai": "^0.14.0", "@lavaclient/plugin-queue": "^0.0.1", - "@lavaclient/queue": "^2.1.1", - "@lavaclient/spotify": "^3.1.0", "@lavaclient/types": "^2.1.1", "@vitalets/google-translate-api": "^9.2.0", "common-tags": "^1.8.2", @@ -41,7 +39,7 @@ "express-session": "^1.18.0", "fixedsize-map": "^1.0.1", "iso-639-1": "^3.1.0", - "lavaclient": "^5.0.0-rc.3", + "lavalink-client": "^2.4.0", "module-alias": "^2.2.3", "moment": "^2.30.1", "mongoose": "^8.1.1", diff --git a/src/commands/music/autoplay.js b/src/commands/music/autoplay.js new file mode 100644 index 0000000..a1327b7 --- /dev/null +++ b/src/commands/music/autoplay.js @@ -0,0 +1,51 @@ +const { musicValidations } = require("@helpers/BotUtils"); +const { autoplayFunction } = require("@handlers/player"); + +/** + * @type {import("@structures/Command")} + */ +module.exports = { + name: "autoplay", + description: "Toggle autoplay feature for music player", + category: "MUSIC", + validations: musicValidations, + command: { + enabled: true, + aliases: ["ap"], + usage: "", + }, + slashCommand: { + enabled: true, + options: [], + }, + + async messageRun(message, args) { + const response = await toggleAutoplay(message); + await message.safeReply(response); + }, + + async interactionRun(interaction) { + const response = await toggleAutoplay(interaction); + await interaction.followUp(response); + }, +}; + +async function toggleAutoplay({ client, guildId }) { + const player = client.musicManager.getPlayer(guildId); + + if (!player || !player.queue.current) { + return "🚫 No song is currently playing"; + } + + const autoplayState = player.get("autoplay"); + + if (autoplayState) { + player.set("autoplay", false); + return "Autoplay deactivated"; + } + + player.set("autoplay", true); + autoplayFunction(client, player.queue.current, player); + + return "Autoplay activated!"; +} diff --git a/src/commands/music/bassboost.js b/src/commands/music/bassboost.js index d384756..8194656 100644 --- a/src/commands/music/bassboost.js +++ b/src/commands/music/bassboost.js @@ -1,23 +1,18 @@ const { musicValidations } = require("@helpers/BotUtils"); const { ApplicationCommandOptionType } = require("discord.js"); - -const levels = { - none: 0.0, - low: 0.1, - medium: 0.15, - high: 0.25, -}; +const { EQList } = require("lavalink-client"); /** * @type {import("@structures/Command")} */ module.exports = { name: "bassboost", - description: "set bassboost level", + description: "Set bassboost level", category: "MUSIC", validations: musicValidations, command: { enabled: true, + aliases: ["bb"], minArgsCount: 1, usage: "", }, @@ -30,48 +25,64 @@ module.exports = { type: ApplicationCommandOptionType.String, required: true, choices: [ - { - name: "none", - value: "none", - }, - { - name: "low", - value: "low", - }, - { - name: "medium", - value: "medium", - }, - { - name: "high", - value: "high", - }, + { name: "High", value: "high" }, + { name: "Medium", value: "medium" }, + { name: "Low", value: "low" }, + { name: "Off", value: "off" }, ], }, ], }, async messageRun(message, args) { - let level = "none"; - if (args.length && args[0].toLowerCase() in levels) level = args[0].toLowerCase(); - const response = setBassBoost(message, level); + let level = "off"; + if (args.length) { + const input = args[0].toLowerCase(); + if (["high", "medium", "low", "off"].includes(input)) { + level = input; + } + } + const response = await setBassBoost(message, level); await message.safeReply(response); }, async interactionRun(interaction) { let level = interaction.options.getString("level"); - const response = setBassBoost(interaction, level); + const response = await setBassBoost(interaction, level); await interaction.followUp(response); }, }; /** * @param {import("discord.js").CommandInteraction|import("discord.js").Message} arg0 - * @param {number} level + * @param {string} level */ -function setBassBoost({ client, guildId }, level) { - const player = client.musicManager.players.resolve(guildId); - const bands = new Array(3).fill(null).map((_, i) => ({ band: i, gain: levels[level] })); - player.setFilters(...bands); +async function setBassBoost({ client, guildId }, level) { + const player = client.musicManager.getPlayer(guildId); + + if (!player || !player.queue.current) { + return "🚫 No song is currently playing"; + } + + // Clear any existing EQ + await player.filterManager.clearEQ(); + + switch (level) { + case "high": + await player.filterManager.setEQ(EQList.BassboostHigh); + break; + case "medium": + await player.filterManager.setEQ(EQList.BassboostMedium); + break; + case "low": + await player.filterManager.setEQ(EQList.BassboostLow); + break; + case "off": + await player.filterManager.clearEQ(); + break; + default: + return "Invalid bassboost level"; + } + return `> Set the bassboost level to \`${level}\``; } diff --git a/src/commands/music/leave.js b/src/commands/music/leave.js new file mode 100644 index 0000000..61e3c15 --- /dev/null +++ b/src/commands/music/leave.js @@ -0,0 +1,41 @@ +const { musicValidations } = require("@helpers/BotUtils"); + +/** + * @type {import("@structures/Command")} + */ +module.exports = { + name: "leave", + description: "Disconnects the bot from the voice channel", + category: "MUSIC", + validations: musicValidations, + command: { + enabled: true, + aliases: ["dc"], + minArgsCount: 0, + usage: "", + }, + slashCommand: { + enabled: true, + options: [], + }, + + async messageRun(message, args) { + const response = await leave(message); + await message.safeReply(response); + }, + + async interactionRun(interaction) { + const response = await leave(interaction); + await interaction.followUp(response); + }, +}; + +/** + * @param {import("discord.js").CommandInteraction|import("discord.js").Message} arg0 + */ +async function leave({ client, guildId, member }) { + const player = client.musicManager.getPlayer(guildId); + + player.destroy(); + return "πŸ‘‹ Disconnected from the voice channel"; +} diff --git a/src/commands/music/loop.js b/src/commands/music/loop.js index 5e99fd2..61e3a1e 100644 --- a/src/commands/music/loop.js +++ b/src/commands/music/loop.js @@ -1,5 +1,4 @@ const { musicValidations } = require("@helpers/BotUtils"); -const { LoopType } = require("@lavaclient/plugin-queue"); const { ApplicationCommandOptionType } = require("discord.js"); /** @@ -12,8 +11,9 @@ module.exports = { validations: musicValidations, command: { enabled: true, + aliases: ["lp"], minArgsCount: 1, - usage: "", + usage: "", }, slashCommand: { enabled: true, @@ -21,17 +21,12 @@ module.exports = { { name: "type", type: ApplicationCommandOptionType.String, - description: "The entity you want to loop", + description: "Select loop type", required: false, choices: [ - { - name: "queue", - value: "queue", - }, - { - name: "track", - value: "track", - }, + { name: "Track", value: "track" }, + { name: "Queue", value: "queue" }, + { name: "Off", value: "off" }, ], }, ], @@ -39,34 +34,47 @@ module.exports = { async messageRun(message, args) { const input = args[0].toLowerCase(); - const type = input === "queue" ? "queue" : "track"; - const response = toggleLoop(message, type); + const type = input === "queue" ? "queue" : input === "track" ? "track" : "off"; + const response = await toggleLoop(message, type); await message.safeReply(response); }, async interactionRun(interaction) { const type = interaction.options.getString("type") || "track"; - const response = toggleLoop(interaction, type); + const response = await toggleLoop(interaction, type); await interaction.followUp(response); }, }; /** * @param {import("discord.js").CommandInteraction|import("discord.js").Message} arg0 - * @param {"queue"|"track"} type + * @param {"queue"|"track"|"off"} type */ -function toggleLoop({ client, guildId }, type) { - const player = client.musicManager.players.resolve(guildId); +async function toggleLoop({ client, guildId }, type) { + const player = client.musicManager.getPlayer(guildId); - // track - if (type === "track") { - player.queue.setLoop(LoopType.Song); - return "Loop mode is set to `track`"; + if (!player || !player.queue.current) { + return "🚫 No song is currently playing"; } - // queue - else if (type === "queue") { - player.queue.setLoop(LoopType.Queue); - return "Loop mode is set to `queue`"; + switch (type) { + case "track": + player.setRepeatMode("track"); + return "Loop mode is set to `track`"; + + case "queue": + if (player.queue.tracks.length > 1) { + player.setRepeatMode("queue"); + return "Loop mode is set to `queue`"; + } else { + return "🚫 Queue is too short to be looped"; + } + + case "off": + player.setRepeatMode("off"); + return "Loop mode is disabled"; + + default: + return "Invalid loop type"; } } diff --git a/src/commands/music/lyric.js b/src/commands/music/lyric.js index ae1274c..92a8196 100644 --- a/src/commands/music/lyric.js +++ b/src/commands/music/lyric.js @@ -48,7 +48,7 @@ async function getLyric({ client, guild, member }, query) { try { if (!query) { - /** @type { import('lavaclient').Player } */ + /** @type { import('lavalink-client').Player } */ const player = client.musicManager.players.resolve(guild.id); if (!player?.track) return "🚫 There's no active music player in this server."; diff --git a/src/commands/music/np.js b/src/commands/music/np.js index 4e2620d..b4fdd16 100644 --- a/src/commands/music/np.js +++ b/src/commands/music/np.js @@ -1,13 +1,13 @@ const { EMBED_COLORS } = require("@root/config"); const { EmbedBuilder } = require("discord.js"); -const { formatTime } = require("@helpers/Utils"); +const { splitBar } = require("string-progressbar"); /** * @type {import("@structures/Command")} */ module.exports = { name: "np", - description: "show's what track is currently being played", + description: "Shows what track is currently being played", category: "MUSIC", botPermissions: ["EmbedLinks"], command: { @@ -18,7 +18,7 @@ module.exports = { enabled: true, }, - async messageRun(message) { + async messageRun(message, args) { const response = nowPlaying(message); await message.safeReply(response); }, @@ -32,35 +32,38 @@ module.exports = { /** * @param {import("discord.js").CommandInteraction|import("discord.js").Message} arg0 */ -function nowPlaying({ client, guildId, member }) { - const player = client.musicManager.players.resolve(guildId); - if (!player || !player.queue.current) return "🚫 No music is being played!"; +function nowPlaying({ client, guildId }) { + const player = client.musicManager.getPlayer(guildId); + if (!player || !player.queue.current) { + return "🚫 No music is being played!"; + } const track = player.queue.current; - const totalLength = track.info.length; - const position = player.position; - const progress = Math.round((position / totalLength) * 15); - const progressBar = `${formatTime(position)} [${"β–¬".repeat( - progress)}πŸ”˜${"β–¬".repeat(15 - progress)}] ${formatTime(totalLength)}`; + const end = track.info.isStream ? "Live" : client.utils.formatTime(track.info.duration); const embed = new EmbedBuilder() .setColor(EMBED_COLORS.BOT_EMBED) - .setAuthor({ name: "Now playing" }) + .setAuthor({ name: "Now Playing" }) .setDescription(`[${track.info.title}](${track.info.uri})`) .addFields( { name: "Song Duration", - value: `\`${formatTime(track.info.length)}\``, + value: client.utils.formatTime(track.info.duration), inline: true, }, { name: "Requested By", - value: track.requesterId ? track.requesterId : member.user.displayName, + value: track.requester.displayName || "Unknown", inline: true, }, { name: "\u200b", - value: progressBar, + value: + client.utils.formatTime(player.position) + + " [" + + splitBar(track.info.isStream ? player.position : track.info.duration, player.position, 15)[0] + + "] " + + end, inline: false, } ); diff --git a/src/commands/music/pause.js b/src/commands/music/pause.js index 79416a7..3e90987 100644 --- a/src/commands/music/pause.js +++ b/src/commands/music/pause.js @@ -5,7 +5,7 @@ const { musicValidations } = require("@helpers/BotUtils"); */ module.exports = { name: "pause", - description: "pause the music player", + description: "Pause the music player", category: "MUSIC", validations: musicValidations, command: { @@ -16,12 +16,12 @@ module.exports = { }, async messageRun(message, args) { - const response = pause(message); + const response = await pause(message); await message.safeReply(response); }, async interactionRun(interaction) { - const response = pause(interaction); + const response = await pause(interaction); await interaction.followUp(response); }, }; @@ -29,10 +29,17 @@ module.exports = { /** * @param {import("discord.js").CommandInteraction|import("discord.js").Message} arg0 */ -function pause({ client, guildId }) { - const player = client.musicManager.players.resolve(guildId); - if (player.paused) return "The player is already paused."; +async function pause({ client, guildId }) { + const player = client.musicManager.getPlayer(guildId); - player.pause(true); - return "⏸️ Paused the music player."; + if (!player || !player.queue.current) { + return "🚫 No song is currently playing"; + } + + if (player.paused) { + return "The player is already paused"; + } + + player.pause(); + return "⏸️ Paused the music player"; } diff --git a/src/commands/music/play.js b/src/commands/music/play.js index 176b88d..278100b 100644 --- a/src/commands/music/play.js +++ b/src/commands/music/play.js @@ -1,6 +1,4 @@ const { EmbedBuilder, ApplicationCommandOptionType } = require("discord.js"); -const { formatTime } = require("@helpers/Utils"); -require("@lavaclient/plugin-queue") const { EMBED_COLORS, MUSIC } = require("@root/config"); /** @@ -8,11 +6,12 @@ const { EMBED_COLORS, MUSIC } = require("@root/config"); */ module.exports = { name: "play", - description: "play a song", + description: "Play or queue your favorite song!", category: "MUSIC", botPermissions: ["EmbedLinks"], command: { enabled: true, + aliases: ["p"], usage: "", minArgsCount: 1, }, @@ -29,14 +28,14 @@ module.exports = { }, async messageRun(message, args) { - const query = args.join(" "); - const response = await play(message, query); + const searchQuery = args.join(" "); + const response = await play(message, searchQuery); await message.safeReply(response); }, async interactionRun(interaction) { - const query = interaction.options.getString("query"); - const response = await play(interaction, query); + const searchQuery = interaction.options.getString("query"); + const response = await play(interaction, searchQuery); await interaction.followUp(response); }, }; @@ -45,134 +44,106 @@ module.exports = { * @param {import("discord.js").CommandInteraction|import("discord.js").Message} arg0 * @param {string} query */ -async function play({ member, guild, channel }, query) { +async function play({ member, guild, channel }, searchQuery) { if (!member.voice.channel) return "🚫 You need to join a voice channel first"; - let player = guild.client.musicManager.players.resolve(guild.id); - if (player && !guild.members.me.voice.channel) { - player.voice.disconnect(); - await guild.client.musicManager.players.destroy(guild.id); - } + let player = guild.client.musicManager.getPlayer(guild.id); if (player && member.voice.channel !== guild.members.me.voice.channel) { return "🚫 You must be in the same voice channel as mine"; } - let embed = new EmbedBuilder().setColor(EMBED_COLORS.BOT_EMBED); - let tracks; - let description = ""; - let thumbnail; + if (!player) { + player = await guild.client.musicManager.createPlayer({ + guildId: guild.id, + voiceChannelId: member.voice.channel.id, + textChannelId: channel.id, + selfMute: false, + selfDeaf: true, + volume: MUSIC.DEFAULT_VOLUME, + }); + } + + if (!player.connected) await player.connect(); try { - const res = await guild.client.musicManager.api.loadTracks( - /^https?:\/\//.test(query) ? query : `${MUSIC.DEFAULT_SOURCE}:${query}` - ); + const res = await player.search({ query: searchQuery }, member.user); - let track; + if (!res || res.loadType === "empty") { + return `🚫 No results found for "${searchQuery}"`; + } - switch (res.loadType) { + switch (res?.loadType) { case "error": - guild.client.logger.error("Search Exception", res.data); + guild.client.logger.error("Search Exception", res.exception); return "🚫 There was an error while searching"; - case "empty": - return `No results found matching ${query}`; + case "playlist": { + player.queue.add(res.tracks); - case "playlist": - tracks = res.data.tracks; - description = res.data.info.name; - thumbnail = res.data.pluginInfo.artworkUrl; - break; + const playlistEmbed = new EmbedBuilder() + .setAuthor({ name: "Added Playlist to queue" }) + .setThumbnail(res.playlist.thumbnail) + .setColor(EMBED_COLORS.BOT_EMBED) + .setDescription(`[${res.playlist.name}](${res.playlist.uri})`) + .addFields( + { name: "Enqueued", value: `${res.tracks.length} songs`, inline: true }, + { + name: "Playlist duration", + value: + "`" + + guild.client.utils.formatTime(res.tracks.map((t) => t.info.duration).reduce((a, b) => a + b, 0)) + + "`", + inline: true, + } + ) + .setFooter({ text: `Requested By: ${member.user.displayName}` }); + + if (!player.playing && player.queue.tracks.length > 0) { + await player.play({ paused: false }); + } + + return { embeds: [playlistEmbed] }; + } case "track": - track = res.data; - tracks = [track]; - break; + case "search": { + const track = res.tracks[0]; + player.queue.add(track); - case "search": - track = res.data[0]; - tracks = [track]; - break; + const trackEmbed = new EmbedBuilder() + .setAuthor({ name: "Added Track to queue" }) + .setColor(EMBED_COLORS.BOT_EMBED) + .setDescription(`[${track.info.title}](${track.info.uri})`) + .setThumbnail(track.info.artworkUrl) + .addFields({ + name: "Song Duration", + value: "`" + guild.client.utils.formatTime(track.info.duration) + "`", + inline: true, + }) + .setFooter({ text: `Requested By: ${track.requester.displayName}` }); + + if (player.queue?.tracks?.length > 1) { + trackEmbed.addFields({ + name: "Position in Queue", + value: player.queue.tracks.length.toString(), + inline: true, + }); + } + + if (!player.playing && player.queue.tracks.length > 0) { + await player.play({ paused: false }); + } + + return { embeds: [trackEmbed] }; + } default: guild.client.logger.debug("Unknown loadType", res); return "🚫 An error occurred while searching for the song"; } - - if (!tracks) guild.client.logger.debug({ query, res }); } catch (error) { - guild.client.logger.error("Search Exception", typeof error === "object" ? JSON.stringify(error) : error); + guild.client.logger.error("Search Exception", JSON.stringify(error)); return "🚫 An error occurred while searching for the song"; } - - if (!tracks) return "🚫 An error occurred while searching for the song"; - - if (tracks.length === 1) { - const track = tracks[0]; - if (!player?.playing && !player?.paused && !player?.queue.tracks.length) { - embed.setAuthor({ name: "Added Track to queue" }); - } else { - const fields = []; - embed - .setAuthor({ name: "Added Track to queue" }) - .setDescription(`[${track.info.title}](${track.info.uri})`) - .setThumbnail(track.info.artworkUrl) - .setFooter({ text: `Requested By: ${member.user.displayName}` }); - - fields.push({ - name: "Song Duration", - value: "`" + formatTime(track.info.length) + "`", - inline: true, - }); - - if (player?.queue?.tracks?.length > 0) { - fields.push({ - name: "Position in Queue", - value: (player.queue.tracks.length + 1).toString(), - inline: true, - }); - } - embed.addFields(fields); - } - } else { - embed - .setAuthor({ name: "Added Playlist to queue" }) - .setThumbnail(thumbnail) - .setDescription(description) - .addFields( - { - name: "Enqueued", - value: `${tracks.length} songs`, - inline: true, - }, - { - name: "Playlist duration", - value: - "`" + - formatTime( - tracks.map((t) => t.info.length).reduce((a, b) => a + b, 0), - ) + - "`", - inline: true, - } - ) - .setFooter({ text: `Requested By: ${member.user.displayName}` }); - } - - // create a player and/or join the member's vc - if (!player?.connected) { - player = guild.client.musicManager.players.create(guild.id); - player.queue.data.channel = channel; - player.voice.connect(member.voice.channel.id, { deafened: true }); - player.setVolume(MUSIC.DEFAULT_VOLUME); - } - - // do queue things - const started = player.playing || player.paused; - player.queue.add(tracks, { requester: member.user.displayName, next: false }); - if (!started) { - await player.queue.start(); - } - - return { embeds: [embed] }; } \ No newline at end of file diff --git a/src/commands/music/queue.js b/src/commands/music/queue.js index 7cefaf0..ca310e1 100644 --- a/src/commands/music/queue.js +++ b/src/commands/music/queue.js @@ -1,6 +1,5 @@ const { EMBED_COLORS } = require("@root/config"); const { EmbedBuilder, ApplicationCommandOptionType } = require("discord.js"); -const { formatTime } = require("@helpers/Utils"); /** * @type {import("@structures/Command")} @@ -12,6 +11,7 @@ module.exports = { botPermissions: ["EmbedLinks"], command: { enabled: true, + aliases: ["q"], usage: "[page]", }, slashCommand: { @@ -44,46 +44,39 @@ module.exports = { * @param {number} pgNo */ async function getQueue({ client, guild }, pgNo) { - const player = client.musicManager.players.resolve(guild.id); - if (!player) return "🚫 There is no music playing in this guild."; - - const queue = player.queue; - const embed = new EmbedBuilder() - .setColor(EMBED_COLORS.BOT_EMBED) - .setAuthor({ name: `Queue for ${guild.name}` }); - - // change for the amount of tracks per page - const multiple = 10; - const page = pgNo || 1; - - const end = page * multiple; - const start = end - multiple; - - const tracks = queue.tracks.slice(start, end); - - if (queue.current) { - const currentTrack = queue.current; - embed.addFields({ - name: "Current", - value: `[${currentTrack.info.title}](${currentTrack.info.uri}) \`[${formatTime(currentTrack.info.length)}]\`` - }); + const player = client.musicManager.getPlayer(guild.id); + if (!player || !player.queue.current) { + return "🚫 No song is currently playing"; } - const queueList = tracks.map((track, index) => { - const title = track.info.title; - const uri = track.info.uri; - const duration = formatTime(track.info.length); - return `${start + index + 1}. [${title}](${uri}) \`[${duration}]\``; - }); + const embed = new EmbedBuilder().setColor(EMBED_COLORS.BOT_EMBED).setAuthor({ name: `Queue for ${guild.name}` }); - if (!queueList.length) { - embed.setDescription(`No tracks in ${page > 1 ? `page ${page}` : "the queue"}.`); - } else { - embed.setDescription(queueList.join("\n")); + const end = (pgNo || 1) * 10; + const start = end - 10; + + const tracks = player.queue.tracks.slice(start, end); + + if (player.queue.current) { + const current = player.queue.current; + embed + .addFields({ + name: "Current", + value: `[${current.info.title}](${current.info.uri}) \`[${client.utils.formatTime(current.info.duration)}]\``, + }) + .setThumbnail(current.info.artworkUrl); } - const maxPages = Math.ceil(queue.tracks.length / multiple); - embed.setFooter({ text: `Page ${page > maxPages ? maxPages : page} of ${maxPages}` }); + const queueList = tracks.map( + (track, index) => + `${start + index + 1}. [${track.info.title}](${track.info.uri}) \`[${client.utils.formatTime(track.info.duration)}]\`` + ); + + embed.setDescription( + queueList.length ? queueList.join("\n") : `No tracks in ${pgNo > 1 ? `page ${pgNo}` : "the queue"}.` + ); + + const maxPages = Math.ceil(player.queue.tracks.length / 10); + embed.setFooter({ text: `Page ${pgNo > maxPages ? maxPages : pgNo} of ${maxPages}` }); return { embeds: [embed] }; } diff --git a/src/commands/music/resume.js b/src/commands/music/resume.js index 702ec82..8c9a513 100644 --- a/src/commands/music/resume.js +++ b/src/commands/music/resume.js @@ -5,7 +5,7 @@ const { musicValidations } = require("@helpers/BotUtils"); */ module.exports = { name: "resume", - description: "resumes the music player", + description: "Resumes the music player", category: "MUSIC", validations: musicValidations, command: { @@ -16,12 +16,12 @@ module.exports = { }, async messageRun(message, args) { - const response = resumePlayer(message); + const response = await resumePlayer(message); await message.safeReply(response); }, async interactionRun(interaction) { - const response = resumePlayer(interaction); + const response = await resumePlayer(interaction); await interaction.followUp(response); }, }; @@ -29,9 +29,15 @@ module.exports = { /** * @param {import("discord.js").CommandInteraction|import("discord.js").Message} arg0 */ -function resumePlayer({ client, guildId }) { - const player = client.musicManager.players.resolve(guildId); +async function resumePlayer({ client, guildId }) { + const player = client.musicManager.getPlayer(guildId); + + if (!player || !player.queue.current) { + return "🚫 No song is currently playing"; + } + if (!player.paused) return "The player is already resumed"; + player.resume(); return "▢️ Resumed the music player"; } diff --git a/src/commands/music/search.js b/src/commands/music/search.js index 47d8f8a..1a6356b 100644 --- a/src/commands/music/search.js +++ b/src/commands/music/search.js @@ -5,7 +5,6 @@ const { ApplicationCommandOptionType, ComponentType, } = require("discord.js"); -const { formatTime } = require("@helpers/Utils"); const { EMBED_COLORS, MUSIC } = require("@root/config"); /** @@ -13,11 +12,12 @@ const { EMBED_COLORS, MUSIC } = require("@root/config"); */ module.exports = { name: "search", - description: "search for matching songs", + description: "search for matching songs on YouTube", category: "MUSIC", botPermissions: ["EmbedLinks"], command: { enabled: true, + aliases: ["sc"], usage: "", minArgsCount: 1, }, @@ -48,171 +48,100 @@ module.exports = { }; /** - * @param {import("discord.js").CommandInteraction|import("discord.js").Message} arg0 + * @param {import("discord.js").CommandInteraction|import("discord.js").Message} interaction * @param {string} query */ async function search({ member, guild, channel }, query) { - if (!member.voice.channel) return "🚫 You need to join a voice channel first"; + if (!member.voice.channel) return "Γ°ΕΈΕ‘Β« You need to join a voice channel first"; + + let player = guild.client.musicManager.getPlayer(guild.id); - let player = guild.client.musicManager.players.resolve(guild.id); - if (player && !guild.members.me.voice.channel) { - player.voice.disconnect(); - await guild.client.musicManager.players.destroy(guild.id); - } if (player && member.voice.channel !== guild.members.me.voice.channel) { - return "🚫 You must be in the same voice channel as mine"; + return "Γ°ΕΈΕ‘Β« You must be in the same voice channel as me."; } - let res; + if (!player) { + player = await guild.client.musicManager.createPlayer({ + guildId: guild.id, + voiceChannelId: member.voice.channel.id, + textChannelId: channel.id, + selfMute: false, + selfDeaf: true, + volume: MUSIC.DEFAULT_VOLUME, + }); + } + + if (!player.connected) await player.connect(); + + const res = await player.search({ query }, member.user); + + if (!res || !res.tracks?.length) { + return { + embeds: [new EmbedBuilder().setColor(EMBED_COLORS.ERROR).setDescription(`No results found for \`${query}\``)], + }; + } + + let maxResults = MUSIC.MAX_SEARCH_RESULTS; + if (res.tracks.length < maxResults) maxResults = res.tracks.length; + + const results = res.tracks.slice(0, maxResults); + const options = results.map((track, index) => ({ + label: track.info.title, + value: index.toString(), + })); + + const menuRow = new ActionRowBuilder().addComponents( + new StringSelectMenuBuilder() + .setCustomId("search-results") + .setPlaceholder("Choose Search Results") + .setMaxValues(1) + .addOptions(options) + ); + + const searchEmbed = new EmbedBuilder() + .setColor(EMBED_COLORS.BOT_EMBED) + .setAuthor({ name: "Search Results" }) + .setDescription(`Select the song you wish to add to the queue`); + + const searchMessage = await channel.send({ + embeds: [searchEmbed], + components: [menuRow], + }); + try { - res = await guild.client.musicManager.api.loadTracks( - /^https?:\/\//.test(query) ? query : `${MUSIC.DEFAULT_SOURCE}:${query}` - ); - } catch (err) { - return "🚫 There was an error while searching"; - } + const response = await channel.awaitMessageComponent({ + filter: (i) => i.user.id === member.id && i.message.id === searchMessage.id, + componentType: ComponentType.StringSelect, + idle: 30 * 1000, + }); - let embed = new EmbedBuilder().setColor(EMBED_COLORS.BOT_EMBED); - let tracks; + if (response.customId !== "search-results") return; - switch (res.loadType) { - case "error": - guild.client.logger.error("Search Exception", res.data); - return "🚫 There was an error while searching"; + await searchMessage.delete(); + if (!response) return "🚫 You took too long to select the songs"; - case "empty": - return `No results found matching ${query}`; + const selectedTrack = results[response.values[0]]; + player.queue.add(selectedTrack); - case "track": { - const [track] = res.data[0]; - tracks = [track]; - if (!player?.playing && !player?.paused && !player?.queue.tracks.length) { - embed.setAuthor({ name: "Added Song to queue" }); - break; - } - - const fields = []; - embed - .setAuthor({ name: "Added Song to queue" }) - .setDescription(`[${track.info.title}](${track.info.uri})`) - .setFooter({ text: `Requested By: ${member.user.displayName}` }); - - fields.push({ + const trackEmbed = new EmbedBuilder() + .setAuthor({ name: "Added Track to queue" }) + .setDescription(`[${selectedTrack.info.title}](${selectedTrack.info.uri})`) + .setThumbnail(selectedTrack.info.artworkUrl) + .addFields({ name: "Song Duration", - value: "`" + formatTime(track.info.length) + "`", + value: "`" + guild.client.utils.formatTime(selectedTrack.info.duration) + "`", inline: true, - }); + }) + .setFooter({ text: `Requested By: ${member.user.displayName}` }); - if (player?.queue?.tracks?.length > 0) { - fields.push({ - name: "Position in Queue", - value: (player.queue.tracks.length + 1).toString(), - inline: true, - }); - } - embed.addFields(fields); - break; + if (!player.playing && player.queue.tracks.length > 0) { + await player.play({ paused: false }); } - case "playlist": - tracks = res.data.tracks; - embed - .setAuthor({ name: "Added Playlist to queue" }) - .setDescription(res.data.info.name) - .addFields( - { - name: "Enqueued", - value: `${res.data.tracks.length} songs`, - inline: true, - }, - { - name: "Playlist duration", - value: - "`" + - formatTime( - res.data.tracks.map((t) => t.info.length).reduce((a, b) => a + b, 0), - ) + - "`", - inline: true, - } - ) - .setFooter({ text: `Requested By: ${member.user.displayName}` }); - break; - - case "search": { - let max = guild.client.config.MUSIC.MAX_SEARCH_RESULTS; - if (res.data.length < max) max = res.data.length; - - const results = res.data.slice(0, max); - const options = results.map((result, index) => ({ - label: result.info.title, - value: index.toString(), - })); - - const menuRow = new ActionRowBuilder().addComponents( - new StringSelectMenuBuilder() - .setCustomId("search-results") - .setPlaceholder("Choose Search Results") - .setMaxValues(max) - .addOptions(options) - ); - - const tempEmbed = new EmbedBuilder() - .setColor(EMBED_COLORS.BOT_EMBED) - .setAuthor({ name: "Search Results" }) - .setDescription(`Please select the songs you wish to add to queue`); - - const sentMsg = await channel.send({ - embeds: [tempEmbed], - components: [menuRow], - }); - - try { - const response = await channel.awaitMessageComponent({ - filter: (reactor) => reactor.message.id === sentMsg.id && reactor.user.id === member.id, - idle: 30 * 1000, - componentType: ComponentType.StringSelect, - }); - - await sentMsg.delete(); - if (!response) return "🚫 You took too long to select the songs"; - - if (response.customId !== "search-results") return; - const toAdd = []; - response.values.forEach((v) => toAdd.push(results[v])); - - // Only 1 song is selected - if (toAdd.length === 1) { - tracks = [toAdd[0]]; - embed.setAuthor({ name: "Added Song to queue" }); - } else { - tracks = toAdd; - embed - .setDescription(`🎢 Added ${toAdd.length} songs to queue`) - .setFooter({ text: `Requested By: ${member.user.displayName}` }); - } - } catch (err) { - console.log(err); - await sentMsg.delete(); - return "🚫 Failed to register your response"; - } - break; - } + return { embeds: [trackEmbed] }; + } catch (err) { + console.error("Error handling response:", err); + await searchMessage.delete(); + return "🚫 Failed to register your response"; } - - // create a player and/or join the member's vc - if (!player?.connected) { - player = guild.client.musicManager.players.create(guild.id); - player.queue.data.channel = channel; - player.voice.connect(member.voice.channel.id, { deafened: true }); - } - - // do queue things - const started = player.playing || player.paused; - player.queue.add(tracks, { requester: member.user.username, next: false }); - if (!started) { - await player.queue.start(); - } - - return { embeds: [embed] }; } diff --git a/src/commands/music/seek.js b/src/commands/music/seek.js index 7d23bdc..5947df0 100644 --- a/src/commands/music/seek.js +++ b/src/commands/music/seek.js @@ -1,6 +1,4 @@ const { musicValidations } = require("@helpers/BotUtils"); -const prettyMs = require("pretty-ms"); -const { durationToMillis } = require("@helpers/Utils"); const { ApplicationCommandOptionType } = require("discord.js"); /** @@ -8,7 +6,7 @@ const { ApplicationCommandOptionType } = require("discord.js"); */ module.exports = { name: "seek", - description: "sets the playing track's position to the specified position", + description: "Sets the position of the current track", category: "MUSIC", validations: musicValidations, command: { @@ -20,7 +18,7 @@ module.exports = { options: [ { name: "time", - description: "The time you want to seek to.", + description: "The time you want to seek to", type: ApplicationCommandOptionType.String, required: true, }, @@ -28,14 +26,20 @@ module.exports = { }, async messageRun(message, args) { - const time = args.join(" "); - const response = seekTo(message, time); + const time = message.client.utils.parseTime(args.join(" ")); + if (!time) { + return await message.safeReply("Invalid time format. Use 10s, 1m 50s, 1h"); + } + const response = await seekTo(message, time); await message.safeReply(response); }, async interactionRun(interaction) { - const time = interaction.options.getString("time"); - const response = seekTo(interaction, time); + const time = interaction.client.utils.parseTime(interaction.options.getString("time")); + if (!time) { + return await interaction.followUp("Invalid time format. Use 10s, 1m 50s, 1h"); + } + const response = await seekTo(interaction, time); await interaction.followUp(response); }, }; @@ -44,14 +48,17 @@ module.exports = { * @param {import("discord.js").CommandInteraction|import("discord.js").Message} arg0 * @param {number} time */ -function seekTo({ client, guildId }, time) { - const player = client.musicManager?.players.resolve(guildId); - const seekTo = durationToMillis(time); +async function seekTo({ client, guildId }, time) { + const player = client.musicManager.getPlayer(guildId); - if (seekTo > player.queue.current.info.length) { - return "The duration you provide exceeds the duration of the current track"; + if (!player || !player.queue.current) { + return "🚫 There's no music currently playing"; } - player.seek(seekTo); - return `Seeked to ${prettyMs(seekTo, { colonNotation: true, secondsDecimalDigits: 0 })}`; + if (time > player.queue.current.length) { + return "The duration you provided exceeds the duration of the current track"; + } + + player.seek(time); + return `Seeked song duration to **${client.utils.formatTime(time)}**`; } diff --git a/src/commands/music/shuffle.js b/src/commands/music/shuffle.js index 02c7b7c..733a5bd 100644 --- a/src/commands/music/shuffle.js +++ b/src/commands/music/shuffle.js @@ -30,7 +30,16 @@ module.exports = { * @param {import("discord.js").CommandInteraction|import("discord.js").Message} arg0 */ function shuffle({ client, guildId }) { - const player = client.musicManager.players.resolve(guildId); + const player = client.musicManager.getPlayer(guildId); + + if (!player || !player.queue.curren) { + return "🚫 There's no music currently playing"; + } + + if (player.queue.tracks.length < 2) { + return "🚫 Not enough tracks to shuffle"; + } + player.queue.shuffle(); return "🎢 Queue has been shuffled"; } diff --git a/src/commands/music/skip.js b/src/commands/music/skip.js index 8bb0640..c4c1396 100644 --- a/src/commands/music/skip.js +++ b/src/commands/music/skip.js @@ -5,12 +5,12 @@ const { musicValidations } = require("@helpers/BotUtils"); */ module.exports = { name: "skip", - description: "skip the current song", + description: "Skip the current song", category: "MUSIC", validations: musicValidations, command: { enabled: true, - aliases: ["next"], + aliases: ["next", "s"], }, slashCommand: { enabled: true, @@ -31,20 +31,18 @@ module.exports = { * @param {import("discord.js").CommandInteraction|import("discord.js").Message} arg0 */ async function skip({ client, guildId }) { - const player = client.musicManager.players.resolve(guildId); + const player = client.musicManager.getPlayer(guildId); if (!player || !player.queue.current) { - return "There is no song currently being played."; + return "🚫 There's no music currently playing"; } - + const title = player.queue.current.info.title; - // Check if there is a next song in the queue - if (player.queue.tracks.length === 0) { - return "There is no next song to skip to."; -} + if (player.queue.tracks.length === 0) { + return "There is no next song to skip to"; + } -// Skip to the next song -player.queue.next(); -return `⏯️ ${title} was skipped successfully.`; -} \ No newline at end of file + await player.skip(); + return `⏯️ ${title} was skipped`; +} diff --git a/src/commands/music/stop.js b/src/commands/music/stop.js index 679430c..509626a 100644 --- a/src/commands/music/stop.js +++ b/src/commands/music/stop.js @@ -5,24 +5,24 @@ const { musicValidations } = require("@helpers/BotUtils"); */ module.exports = { name: "stop", - description: "stop the music player", + description: "Stop the music player", category: "MUSIC", validations: musicValidations, command: { enabled: true, - aliases: ["leave"], + aliases: [""], }, slashCommand: { enabled: true, }, - async messageRun(message, args) { - const response = await stop(message); + async messageRun(message, args, data) { + const response = await stop(message, data.settings); await message.safeReply(response); }, - async interactionRun(interaction) { - const response = await stop(interaction); + async interactionRun(interaction, data) { + const response = await stop(interaction, data.settings); await interaction.followUp(response); }, }; @@ -31,8 +31,17 @@ module.exports = { * @param {import("discord.js").CommandInteraction|import("discord.js").Message} arg0 */ async function stop({ client, guildId }) { - const player = client.musicManager.players.resolve(guildId); - player.voice.disconnect(); - await client.musicManager.players.destroy(guildId); - return "🎢 The music player is stopped and queue has been cleared"; + const player = client.musicManager.getPlayer(guildId); + + if (!player || !player.queue.current) { + return "🚫 There's no music currently playing"; + } + + if (player.get("autoplay") === true) { + player.set("autoplay", false); + } + + player.stopPlaying(true, false); + + return "🎢 The music player is stopped, and the queue has been cleared"; } diff --git a/src/commands/music/volume.js b/src/commands/music/volume.js index f383443..a062b42 100644 --- a/src/commands/music/volume.js +++ b/src/commands/music/volume.js @@ -11,14 +11,15 @@ module.exports = { validations: musicValidations, command: { enabled: true, - usage: "<1-100>", + aliases: ["vol"], + usage: "<0-100>", }, slashCommand: { enabled: true, options: [ { name: "amount", - description: "Enter a value to set [1 to 100]", + description: "Enter a value to set [0 to 100]", type: ApplicationCommandOptionType.Integer, required: false, }, @@ -42,15 +43,19 @@ module.exports = { * @param {import("discord.js").CommandInteraction|import("discord.js").Message} arg0 */ async function getVolume({ client, guildId }, amount) { - const player = client.musicManager.players.resolve(guildId); + const player = client.musicManager.getPlayer(guildId); - if (!amount) return `> The player volume is \`${player.volume}\`.`; + if (!player || !player.queue.current) { + return "🚫 There's no music currently playing"; + } + + if (!amount) return `> The player volume is \`${player.volume}\``; if (isNaN(amount) || amount < 0 || amount > 100) { - return "You need to give me a volume between 1 and 100."; + return "You need to give me a volume between 0 and 100"; } // Set the player volume await player.setVolume(amount); - return `🎢 Music player volume is set to \`${amount}\`.`; + return `🎢 Music player volume is set to \`${amount}\``; } diff --git a/src/commands/utility/nodes.js b/src/commands/utility/nodes.js index 4fbc779..0ea9910 100644 --- a/src/commands/utility/nodes.js +++ b/src/commands/utility/nodes.js @@ -41,13 +41,13 @@ async function fetchNodesAndTurnItToTable(nodes) { const res = await fetchNodeStat(node).then(HttpUtil.assertStatusOk) stat = await res.json() } catch(e) { - error(node.identifier, e) + error(node.id, e) } if (stat) { const { uptime, memory: { free, reservable }, cpu: { lavalinkLoad }, playingPlayers, players } = stat return { - name: `:green_circle: ${node.identifier}`, + name: `:green_circle: ${node.id}`, value: [ `❯ **Uptime**: ${Utils.formatTime(uptime)}`, `❯ **Memory**: ${byteUnit(free)} (${(free / reservable * 100).toPrecision(3)}%) of ${byteUnit(reservable)}`, @@ -57,7 +57,7 @@ async function fetchNodesAndTurnItToTable(nodes) { } } else { return { - name: `:red_circle: ${node.identifier}`, + name: `:red_circle: ${node.id}`, value: 'Node is offline' } } @@ -71,13 +71,13 @@ async function fetchNodesAndTurnItToTable(nodes) { } async function fetchNodeStat(node, connTimeout = 5000) { - const baseUrl = (node.info.port !== 80 && node.info.secure) - ? `https://${node.info.host}:${node.info.port}` - : `http://${node.info.host}:${node.info.port}`; + const baseUrl = (node.port !== 80 && node.secure) + ? `https://${node.host}:${node.port}` + : `http://${node.host}:${node.port}`; return HttpUtil.fetchWithConnTimeout( `${baseUrl}/v4/stats`, - { headers: { Authorization: node.info.auth } }, + { headers: { Authorization: node.authorization } }, connTimeout ); } diff --git a/src/events/player/playerDestroy.js b/src/events/player/playerDestroy.js new file mode 100644 index 0000000..c25f8ad --- /dev/null +++ b/src/events/player/playerDestroy.js @@ -0,0 +1,13 @@ +module.exports = async (client, player) => { + const guild = client.guilds.cache.get(player.guildId); + if (!guild) return; + + if (player.voiceChannelId) { + await client.utils.setVoiceStatus(client, player.voiceChannelId, ""); + } + + const msg = player.get("message"); + if (msg && msg.deletable) { + await msg.delete().catch(() => {}); + } + }; \ No newline at end of file diff --git a/src/events/player/playerDisconnect.js b/src/events/player/playerDisconnect.js new file mode 100644 index 0000000..c25f8ad --- /dev/null +++ b/src/events/player/playerDisconnect.js @@ -0,0 +1,13 @@ +module.exports = async (client, player) => { + const guild = client.guilds.cache.get(player.guildId); + if (!guild) return; + + if (player.voiceChannelId) { + await client.utils.setVoiceStatus(client, player.voiceChannelId, ""); + } + + const msg = player.get("message"); + if (msg && msg.deletable) { + await msg.delete().catch(() => {}); + } + }; \ No newline at end of file diff --git a/src/events/player/queueEnd.js b/src/events/player/queueEnd.js new file mode 100644 index 0000000..a8e63b5 --- /dev/null +++ b/src/events/player/queueEnd.js @@ -0,0 +1,30 @@ +const { MUSIC, EMBED_COLORS } = require("@root/config"); +const { EmbedBuilder } = require("discord.js"); + +module.exports = async (client, player) => { + const guild = client.guilds.cache.get(player.guildId); + if (!guild) return; + + if (player.voiceChannelId) { + await client.utils.setVoiceStatus(client, player.voiceChannelId, "Silence? Use /play to start the beat!"); + } + + if (player.volume > 100) { + await player.setVolume(MUSIC.DEFAULT_VOLUME); + } + + const msg = player.get("message"); + if (msg && msg.deletable) { + await msg.delete().catch(() => {}); + } + + const channel = guild.channels.cache.get(player.textChannelId); + if (channel) { + await channel.safeSend( + { + embeds: [new EmbedBuilder().setColor(EMBED_COLORS.BOT_EMBED).setDescription("Queue has ended.")], + }, + 10 + ); + } +}; \ No newline at end of file diff --git a/src/events/player/trackEnd.js b/src/events/player/trackEnd.js new file mode 100644 index 0000000..b13f72a --- /dev/null +++ b/src/events/player/trackEnd.js @@ -0,0 +1,20 @@ +const { autoplayFunction } = require("@handlers/player"); +const { MUSIC } = require("@root/config.js"); + +module.exports = async (client, player, track) => { + const guild = client.guilds.cache.get(player.guildId); + if (!guild) return; + + if (player.volume > 100) { + await player.setVolume(MUSIC.DEFAULT_VOLUME); + } + + const msg = player.get("message"); + if (msg && msg.deletable) { + await msg.delete().catch(() => {}); + } + + if (player.get("autoplay") === true) { + await autoplayFunction(client, track, player); + } +}; \ No newline at end of file diff --git a/src/events/player/trackStart.js b/src/events/player/trackStart.js new file mode 100644 index 0000000..924c90d --- /dev/null +++ b/src/events/player/trackStart.js @@ -0,0 +1,112 @@ +const { EmbedBuilder, ButtonBuilder, ButtonStyle, ActionRowBuilder } = require("discord.js"); +const { EMBED_COLORS } = require("@root/config"); + +module.exports = async (client, player, track) => { + const guild = client.guilds.cache.get(player.guildId); + if (!guild) return; + + if (!player.textChannelId || !track) return; + + const channel = guild.channels.cache.get(player.textChannelId); + if (!channel) return; + + if (player.get("autoplay") === true) { + await player.queue.previous.push(track); + } + + if (player.voiceChannelId) { + await client.utils.setVoiceStatus(client, player.voiceChannelId, `Paying: **${track.info.title}**`); + } + + const previous = await player.queue.shiftPrevious(); + + const row = (player) => + new ActionRowBuilder().addComponents( + new ButtonBuilder().setCustomId("previous").setEmoji("βͺ").setStyle(ButtonStyle.Secondary).setDisabled(!previous), + new ButtonBuilder() + .setCustomId("pause") + .setEmoji(player.paused ? "▢️" : "⏸️") + .setStyle(player.paused ? ButtonStyle.Success : ButtonStyle.Secondary), + new ButtonBuilder().setCustomId("stop").setEmoji("⏹️").setStyle(ButtonStyle.Secondary), + new ButtonBuilder().setCustomId("skip").setEmoji("⏩").setStyle(ButtonStyle.Secondary), + new ButtonBuilder().setCustomId("shuffle").setEmoji("πŸ”€").setStyle(ButtonStyle.Secondary) + ); + + const msg = await channel.safeSend({ + embeds: [ + new EmbedBuilder() + .setColor(EMBED_COLORS.BOT_EMBED) + .setAuthor({ name: "Track Started" }) + .setDescription(`🎢 **Now playing [${track.info.title}](${track.info.uri})**`) + .setThumbnail(track.info.artworkUrl) + .setFooter({ + text: `Requested by: ${track.requester.displayName}`, + }) + .addFields( + { + name: "Duration", + value: track.info.isStream ? "Live" : client.utils.formatTime(track.info.duration), + inline: true, + }, + { name: "Author", value: track.info.author || "Unknown", inline: true } + ), + ], + components: [row(player)], + }); + + if (msg) player.set("message", msg); + + const collector = msg.createMessageComponentCollector({ + filter: async (int) => { + const sameVc = int.guild.members.me.voice.channelId === int.member.voice.channelId; + return sameVc; + }, + }); + + collector.on("collect", async (int) => { + if (!int.isButton()) return; + + await int.deferReply({ ephemeral: true }); + let description; + + switch (int.customId) { + case "previous": + description = previous ? "Playing the previous track..." : "No previous track available"; + if (previous) player.play({ clientTrack: previous }); + break; + + case "pause": + if (player.paused) { + player.resume(); + description = "Track resumed"; + } else { + player.pause(); + description = "Track paused"; + } + await msg.edit({ components: [row(player)] }); + break; + + case "stop": + player.stopPlaying(true, false); + description = "Playback stopped"; + break; + + case "skip": + description = player.queue.tracks.length > 0 ? "Skipped to the next track" : "The queue is empty!"; + if (player.queue.tracks.length > 0) player.skip(); + break; + + case "shuffle": + if (player.queue.tracks.length < 2) { + description = "The queue is too short to shuffle!"; + } else { + player.queue.shuffle(); + description = "The queue has been shuffled!"; + } + break; + } + await int.followUp({ + embeds: [new EmbedBuilder().setDescription(description).setColor(EMBED_COLORS.BOT_EMBED)], + }); + }); +}; \ No newline at end of file diff --git a/src/events/raw.js b/src/events/raw.js new file mode 100644 index 0000000..e163ba8 --- /dev/null +++ b/src/events/raw.js @@ -0,0 +1,3 @@ +module.exports = async (client, data) => { + client.musicManager.sendRawData(data); + }; \ No newline at end of file diff --git a/src/events/ready.js b/src/events/ready.js index 4b42462..3e2a822 100644 --- a/src/events/ready.js +++ b/src/events/ready.js @@ -10,7 +10,7 @@ module.exports = async (client) => { // Initialize Music Manager if (client.config.MUSIC.ENABLED) { - client.musicManager.connect({ userId: client.user.id }); + client.musicManager.init({ ...client.user, shards: "auto" }); client.logger.success("Music Manager initialized"); } diff --git a/src/events/voice/voiceStateUpdate.js b/src/events/voice/voiceStateUpdate.js index e8ddf4e..bb943bc 100644 --- a/src/events/voice/voiceStateUpdate.js +++ b/src/events/voice/voiceStateUpdate.js @@ -24,7 +24,7 @@ module.exports = async (client, oldState, newState) => { // if 1 (you), wait 1 minute if (!oldState.channel.members.size - 1) { const player = client.musicManager.players.resolve(guild.id); - if (player) client.musicManager.players.destroy(guild.id).then(player.voice.disconnect()); // destroy the player + if (player) client.musicManager.getPlayer(guild.id).destroy(); // destroy the player } }, client.config.MUSIC.IDLE_TIME * 1000); } diff --git a/src/handlers/index.js b/src/handlers/index.js index 022c1d0..c4d5c86 100644 --- a/src/handlers/index.js +++ b/src/handlers/index.js @@ -11,4 +11,6 @@ module.exports = { suggestionHandler: require("./suggestion"), ticketHandler: require("./ticket"), translationHandler: require("./translation"), + managerHandler: require("./manager"), + playerHandler: require("./player"), }; diff --git a/src/handlers/lavaclient.js b/src/handlers/lavaclient.js deleted file mode 100644 index e098e06..0000000 --- a/src/handlers/lavaclient.js +++ /dev/null @@ -1,75 +0,0 @@ -const { EmbedBuilder, GatewayDispatchEvents } = require("discord.js"); -const { Cluster } = require("lavaclient"); -const { formatTime } = require("@helpers/Utils"); -require("@lavaclient/plugin-queue").load(); - -/** - * @param {import("@structures/BotClient")} client - */ -module.exports = (client) => { - const lavaclient = new Cluster({ - nodes: client.config.MUSIC.LAVALINK_NODES, - ws: client.config.MUSIC.LAVALINK_WS, - discord: { - sendGatewayCommand: (id, payload) => client.guilds.cache.get(id)?.shard?.send(payload), - }, - }); - - client.ws.on(GatewayDispatchEvents.VoiceStateUpdate, (data) => lavaclient.players.handleVoiceUpdate(data)); - client.ws.on(GatewayDispatchEvents.VoiceServerUpdate, (data) => lavaclient.players.handleVoiceUpdate(data)); - - lavaclient.on("nodeConnected", (node, event) => { - client.logger.log(`Node "${node.identifier}" connected`); - }); - - lavaclient.on("nodeDisconnected", (node, event) => { - client.logger.log(`Node "${node.identifier}" disconnected`); - const reconnectInterval = 20000; // Time in MS, change as needed. - setTimeout(() => { - node.connect(); - }, reconnectInterval); - }); - - lavaclient.on("nodeError", (node, error) => { - client.logger.error(`Node "${node.identifier}" encountered an error: ${error.message}.`, error); - }); - - lavaclient.on("nodeDebug", (node, event) => { - client.logger.debug(`Node "${node.identifier}" debug: ${event.message}`); - }); - - lavaclient.on("nodeTrackStart", async (_node, queue, track) => { - const fields = []; - - const embed = new EmbedBuilder() - .setAuthor({ name: "Now Playing" }) - .setColor(client.config.EMBED_COLORS.BOT_EMBED) - .setDescription(`[${track.info.title}](${track.info.uri})`) - .setFooter({ text: `Requested By: ${track.requesterId}` }) - .setThumbnail(track.info.artworkUrl); - - fields.push({ - name: "Song Duration", - value: "`" + formatTime(track.info.length) + "`", - inline: true, - }); - - if (queue.tracks.length > 0) { - fields.push({ - name: "Position in Queue", - value: (queue.tracks.length + 1).toString(), - inline: true, - }); - } - - embed.setFields(fields); - queue.data.channel.safeSend({ embeds: [embed] }); - }); - - lavaclient.on("nodeQueueFinish", async (_node, queue) => { - queue.data.channel.safeSend("Queue has ended."); - await client.musicManager.players.destroy(queue.player.guildId).then(() => queue.player.voice.disconnect()); - }); - - return lavaclient; -}; \ No newline at end of file diff --git a/src/handlers/manager.js b/src/handlers/manager.js new file mode 100644 index 0000000..ba78b32 --- /dev/null +++ b/src/handlers/manager.js @@ -0,0 +1,43 @@ +const { LavalinkManager } = require("lavalink-client"); +const { MUSIC } = require("@root/config.js"); + +class Manager extends LavalinkManager { + constructor(client) { + super({ + nodes: MUSIC.LAVALINK_NODES, + sendToShard: (guildId, payload) => client.guilds.cache.get(guildId)?.shard?.send(payload), + emitNewSongsOnly: false, + queueOptions: { + maxPreviousTracks: 30, + }, + playerOptions: { + defaultSearchPlatform: MUSIC.DEFAULT_SOURCE, + onDisconnect: { + autoReconnect: true, + destroyPlayer: false, + }, + }, + linksAllowed: true, + linksBlacklist: ["porn"], + linksWhitelist: [], + }); + + this.nodeManager.on("connect", (node) => { + client.logger.success(`Lavalink node ${node.id} connected!`); + }); + + this.nodeManager.on("disconnect", (node, reason) => { + client.logger.warn(`Lavalink node "${node.id}" disconnected. Reason: ${JSON.stringify(reason)}`); + }); + + this.nodeManager.on("error", (node, error) => { + client.logger.error(`Error occurred on Lavalink node "${node.id}": ${error.message}`); + }); + + this.nodeManager.on("destroy", (node) => { + client.logger.warn(`Lavalink node "${node.id}" destroyed`); + }); + } +} + +module.exports = Manager; \ No newline at end of file diff --git a/src/handlers/player.js b/src/handlers/player.js new file mode 100644 index 0000000..1381006 --- /dev/null +++ b/src/handlers/player.js @@ -0,0 +1,71 @@ +const { EMBED_COLORS, MUSIC } = require("@root/config"); +const { EmbedBuilder } = require("discord.js"); + +module.exports = { + autoplayFunction: async (client, track, player) => { + if (player.queue.tracks.length > 0) return; + if (!track) return; + + const channel = client.guilds.cache.get(player.guildId)?.channels.cache.get(player.textChannelId); + + let url, source; + + switch (track.info.sourceName) { + case "spotify": + url = `seed_tracks=${track.info.identifier ?? "11xbo8bGa5XTrXrGP77zwc"}&limit=20&min_popularity=30`; + source = "sprec"; + break; + + case "youtube": + url = `https://youtube.com/watch?v=${track.info.identifier ?? "lpeuIu-ZYJY"}&list=RD${track.info.identifier ?? "lpeuIu-ZYJY"}`; + source = "ytsearch"; + break; + + case "jiosaavn": + url = `${track.info.identifier ?? "Hvma-gqd"}`; + source = "jsrec"; + break; + + default: + url = track.info.author; + source = MUSIC.DEFAULT_SOURCE; + break; + } + + const res = await player.search({ query: url, source: source }, track.requester); + + if (!res || res.tracks.length === 0) { + await channel.safeSend( + { + embeds: [new EmbedBuilder().setColor(EMBED_COLORS.WARNING).setDescription("> Autoplay, No results found")], + }, + 10 + ); + return player.destroy(); + } + + for (let songs = 0; songs < 3; ) { + const chosen = res.tracks[Math.floor(Math.random() * res.tracks.length)]; + + if ( + !player.queue.previous?.some((o) => o.info.identifier === chosen.info.identifier) && + !player.queue.tracks.some((o) => o.info.identifier === chosen.info.identifier) + ) { + await player.queue.add(chosen); + songs++; + } + } + + if (player.queue.tracks.length === 0) { + await channel?.safeSend( + { + embeds: [ + new EmbedBuilder().setColor(EMBED_COLORS.WARNING).setDescription("> Autoplay, No unique track found"), + ], + }, + 10 + ); + return player.destroy(); + } + }, +}; \ No newline at end of file diff --git a/src/helpers/BotUtils.js b/src/helpers/BotUtils.js index 17e72e1..d6171b4 100644 --- a/src/helpers/BotUtils.js +++ b/src/helpers/BotUtils.js @@ -63,8 +63,8 @@ module.exports = class BotUtils { static get musicValidations() { return [ { - callback: ({ client, guildId }) => client.musicManager.players.resolve(guildId), - message: "🚫 No music is being played!", + callback: ({ client, guildId }) => client.musicManager.getPlayer(guildId), + message: "🚫 I'm not in a voice channel.", }, { callback: ({ member }) => member.voice?.channelId, @@ -72,7 +72,7 @@ module.exports = class BotUtils { }, { callback: ({ member, client, guildId }) => - member.voice?.channelId === client.musicManager.players.resolve(guildId)?.voice.channelId, + member.voice?.channelId === client.musicManager.getPlayer(guildId)?.voiceChannelId, message: "🚫 You're not in the same voice channel.", }, ]; diff --git a/src/helpers/Utils.js b/src/helpers/Utils.js index 57c4b28..345f7af 100644 --- a/src/helpers/Utils.js +++ b/src/helpers/Utils.js @@ -162,21 +162,43 @@ module.exports = class Utils { * @returns {string} - Formatted time string */ static formatTime(ms) { - const minuteMs = 60 * 1000; - const hourMs = 60 * minuteMs; - const dayMs = 24 * hourMs; - if (ms < minuteMs) { - return `${ms / 1000}s`; - } else if (ms < hourMs) { - return `${Math.floor(ms / minuteMs)}m ${Math.floor( - (ms % minuteMs) / 1000 - )}s`; - } else if (ms < dayMs) { - return `${Math.floor(ms / hourMs)}h ${Math.floor( - (ms % hourMs) / minuteMs - )}m`; - } else { - return `${Math.floor(ms / dayMs)}d ${Math.floor((ms % dayMs) / hourMs)}h`; - } + return ms < 1000 + ? `${ms / 1000}s` + : ["d", "h", "m", "s"] + .map((unit, i) => { + const value = [864e5, 36e5, 6e4, 1e3][i]; + const amount = Math.floor(ms / value); + ms %= value; + return amount ? `${amount}${unit}` : null; + }) + .filter((x) => x !== null) + .join(" ") || "0s"; + } + + /** + * Parses a time string into milliseconds + * @param {string} string - The time string (e.g., "1d", "2h", "3m", "4s") + * @returns {number} - The time in milliseconds + */ + static parseTime(string) { + const time = string.match(/([0-9]+[dhms])/g); + if (!time) return 0; + return time.reduce((ms, t) => { + const unit = t[t.length - 1]; + const amount = Number(t.slice(0, -1)); + return ms + amount * { d: 864e5, h: 36e5, m: 6e4, s: 1e3 }[unit]; + }, 0); + } + + /** + * Updates voice channel status + * @param {string} channel - The voice channel ID + * @param {string} status - The status to update + * @param {object} client - The bot client + */ + static async setVoiceStatus(client, channelId, message) { + const url = `/channels/${channelId}/voice-status`; + const payload = { status: message }; + await client.rest.put(url, { body: payload }).catch(() => {}); } }; diff --git a/src/helpers/Validator.js b/src/helpers/Validator.js index dd3143d..3021074 100644 --- a/src/helpers/Validator.js +++ b/src/helpers/Validator.js @@ -44,14 +44,15 @@ module.exports = class Validator { // Music if (config.MUSIC.ENABLED) { - if (!process.env.SPOTIFY_CLIENT_ID || !process.env.SPOTIFY_CLIENT_SECRET) { - warn("env: SPOTIFY_CLIENT_ID or SPOTIFY_CLIENT_SECRET are missing. Spotify music links won't work"); - } if (config.MUSIC.LAVALINK_NODES.length == 0) { warn("config.js: There must be at least one node for Lavalink"); } - if (!["ytsearch", "ytmsearch", "spsearch", "scsearch"].includes(config.MUSIC.DEFAULT_SOURCE)) { - warn("config.js: MUSIC.DEFAULT_SOURCE must be either ytsearch, ytmsearch, spsearch or scsearch"); + if ( + !["ytsearch", "ytmsearch", "scsearch", "spsearch", "dzsearch", "jssearch"].includes(config.MUSIC.DEFAULT_SOURCE) + ) { + warn( + "config.js: MUSIC.DEFAULT_SOURCE must be either ytsearch, ytmsearch, scsearch, spsearch, dzsearch or jssearch" + ); } } diff --git a/src/structures/BotClient.js b/src/structures/BotClient.js index 1c501ea..aabb569 100644 --- a/src/structures/BotClient.js +++ b/src/structures/BotClient.js @@ -13,7 +13,7 @@ const { recursiveReadDirSync } = require("../helpers/Utils"); const { validateCommand, validateContext } = require("../helpers/Validator"); const { schemas } = require("@src/database/mongoose"); const CommandCategory = require("./CommandCategory"); -const lavaclient = require("../handlers/lavaclient"); +const Manager = require("../handlers/manager"); const giveawaysHandler = require("../handlers/giveaway"); const { DiscordTogether } = require("discord-together"); @@ -65,7 +65,7 @@ module.exports = class BotClient extends Client { : undefined; // Music Player - if (this.config.MUSIC.ENABLED) this.musicManager = lavaclient(this); + if (this.config.MUSIC.ENABLED) this.musicManager = new Manager(this); // Giveaways if (this.config.GIVEAWAYS.ENABLED) this.giveawaysManager = giveawaysHandler(this); @@ -76,6 +76,9 @@ module.exports = class BotClient extends Client { // Database this.database = schemas; + // Utils + this.utils = require("../helpers/Utils"); + // Discord Together this.discordTogether = new DiscordTogether(this); } @@ -92,18 +95,23 @@ module.exports = class BotClient extends Client { recursiveReadDirSync(directory).forEach((filePath) => { const file = path.basename(filePath); + const dir = path.basename(path.dirname(filePath)); try { const eventName = path.basename(file, ".js"); const event = require(filePath); - this.on(eventName, event.bind(null, this)); + if (dir === "player") { + this.musicManager.on(eventName, event.bind(null, this)); + } else { + this.on(eventName, event.bind(null, this)); + } clientEvents.push([file, "βœ“"]); delete require.cache[require.resolve(filePath)]; success += 1; } catch (ex) { failed += 1; - this.logger.error(`loadEvent - ${file}`, ex); + this.logger.error(`Failed to load event - ${file}`, ex); } });