music wrapper change

• Changed music wrapper lavaclient to lavalink-client
• Added lavalink v4 support
• Added custom player events
• Added autoplay feature.
This commit is contained in:
Amane Serenetia 2024-12-06 06:32:09 +07:00
parent bd020a8e71
commit 03705ad4e8
37 changed files with 1026 additions and 582 deletions

1
.gitignore vendored
View File

@ -1,6 +1,7 @@
node_modules
docs
.env.asli
config.js
# Logs
logs

137
config.js.example Normal file
View File

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

69
package-lock.json generated
View File

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

View File

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

View File

@ -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!";
}

View File

@ -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: "<none|low|medium|high>",
},
@ -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}\``;
}

View File

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

View File

@ -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: "<queue|track>",
usage: "<queue|track|off>",
},
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";
}
}

View File

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

View File

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

View File

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

View File

@ -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: "<song-name>",
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] };
}

View File

@ -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] };
}

View File

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

View File

@ -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: "<song-name>",
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] };
}

View File

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

View File

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

View File

@ -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.`;
}
await player.skip();
return `⏯️ ${title} was skipped`;
}

View File

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

View File

@ -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}\``;
}

View File

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

View File

@ -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(() => {});
}
};

View File

@ -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(() => {});
}
};

View File

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

View File

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

View File

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

3
src/events/raw.js Normal file
View File

@ -0,0 +1,3 @@
module.exports = async (client, data) => {
client.musicManager.sendRawData(data);
};

View File

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

View File

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

View File

@ -11,4 +11,6 @@ module.exports = {
suggestionHandler: require("./suggestion"),
ticketHandler: require("./ticket"),
translationHandler: require("./translation"),
managerHandler: require("./manager"),
playerHandler: require("./player"),
};

View File

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

43
src/handlers/manager.js Normal file
View File

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

71
src/handlers/player.js Normal file
View File

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

View File

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

View File

@ -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(() => {});
}
};

View File

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

View File

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