feat: Music support (#62)

Closes #9 via [discord-player](https://github.com/Androz2091/discord-player/)

Co-authored-by: Mylloon <kennel.anri@tutanota.com>
Reviewed-on: #62
This commit is contained in:
Mylloon 2023-02-12 01:11:07 +01:00
parent 70237b56c0
commit 2770bd15bb
25 changed files with 2328 additions and 367 deletions

View file

@ -12,10 +12,10 @@ WORKDIR /app
COPY --chown=node:node . .
RUN npm ci --only=production
RUN npm ci --only=production --legacy-peer-deps
RUN npx tsc
RUN rm -r src/ tsconfig.json
RUN npm uninstall typescript @types/sqlite3
RUN npm uninstall typescript @types/sqlite3 --legacy-peer-deps
CMD ["dumb-init", "node", "./dist/index.js"]

View file

@ -13,7 +13,7 @@
> Installer les dépendences du bot.
```bash
npm install
npm install --legacy-peer-deps
```
> Lancer le bot.

1674
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -16,13 +16,20 @@
"author": "La confrérie du Kassoulait",
"license": "GPL-3.0-only",
"dependencies": {
"@discordjs/rest": "^1.1.0",
"@discord-player/extractor": "^4.0.0",
"@discordjs/opus": "^0.9.0",
"@discordjs/rest": "^1.5.0",
"@types/sqlite3": "^3.1.8",
"@types/uuid": "^9.0.0",
"discord-api-types": "^0.36.3",
"discord.js": "^14.3.0",
"sqlite3": "^5.0.11",
"typescript": "^4.7.4",
"discord-api-types": "^0.37.32",
"discord-player": "^5.4.1-dev.0",
"discord.js": "^14.7.1",
"ffmpeg-static": "^5.1.0",
"node-fetch": "^2.6.9",
"play-dl": "^1.9.6",
"prism-media": "^1.3.4",
"sqlite3": "^5.1.4",
"typescript": "^4.9.5",
"uuid": "^9.0.0"
},
"devDependencies": {

View file

@ -0,0 +1,72 @@
import {
ActionRowBuilder,
ButtonBuilder,
ButtonStyle,
Client,
EmbedBuilder,
MessageComponentInteraction,
} from "discord.js";
import { v4 as uuidv4 } from "uuid";
import { getLocale } from "../../utils/locales";
import { getFilename } from "../../utils/misc";
import { embedListQueue } from "../../utils/music";
import { collect } from "../loader";
export default {
data: {
name: getFilename(__filename),
},
interaction: async (interaction: MessageComponentInteraction, client: Client) => {
const loc = getLocale(client, interaction.locale);
const embed_desc = interaction.message.embeds.at(0)?.author?.name as string;
// Retrieve Pages
const pageMax = Number(/(\d+)(?!.*\d)/gm.exec(embed_desc)?.[0]);
let page = Number(/(?!• \s+)\d(?=\/)/gm.exec(embed_desc)?.[0]);
if (page + 1 > pageMax) {
page = 1;
} else {
page++;
}
// Get queue
const queue = client.player.queues.get(interaction.guildId ?? "");
const embed = new EmbedBuilder();
const rows = [];
if (queue) {
// Create the embed
embedListQueue(client, embed, queue.tracks, page, interaction.locale);
// Create buttons
const idPrec = "queueList-prec_" + uuidv4();
const idNext = "queueList-next_" + uuidv4();
rows.push(
new ActionRowBuilder<ButtonBuilder>()
.addComponents(
new ButtonBuilder()
.setCustomId(idPrec)
.setLabel(loc.get("c_queue8"))
.setStyle(ButtonStyle.Primary)
)
.addComponents(
new ButtonBuilder()
.setCustomId(idNext)
.setLabel(loc.get("c_queue9"))
.setStyle(ButtonStyle.Primary)
)
);
// Buttons interactions
collect(client, interaction, idPrec);
collect(client, interaction, idNext);
} else {
// In case queue doesn't exists
embed.setDescription(loc.get("c_queue2"));
}
return {
embeds: [embed],
components: rows,
};
},
};

View file

@ -0,0 +1,72 @@
import {
ActionRowBuilder,
ButtonBuilder,
ButtonStyle,
Client,
EmbedBuilder,
MessageComponentInteraction,
} from "discord.js";
import { v4 as uuidv4 } from "uuid";
import { getLocale } from "../../utils/locales";
import { getFilename } from "../../utils/misc";
import { embedListQueue } from "../../utils/music";
import { collect } from "../loader";
export default {
data: {
name: getFilename(__filename),
},
interaction: async (interaction: MessageComponentInteraction, client: Client) => {
const loc = getLocale(client, interaction.locale);
const embed_desc = interaction.message.embeds.at(0)?.author?.name as string;
// Retrieve Pages
const pageMax = Number(/(\d+)(?!.*\d)/gm.exec(embed_desc)?.[0]);
let page = Number(/(?!• \s+)\d(?=\/)/gm.exec(embed_desc)?.[0]);
if (page - 1 == 0) {
page = pageMax;
} else {
page--;
}
// Get queue
const queue = client.player.queues.get(interaction.guildId ?? "");
const embed = new EmbedBuilder();
const rows = [];
if (queue) {
// Create the embed
embedListQueue(client, embed, queue.tracks, page, interaction.locale);
// Create buttons
const idPrec = "queueList-prec_" + uuidv4();
const idNext = "queueList-next_" + uuidv4();
rows.push(
new ActionRowBuilder<ButtonBuilder>()
.addComponents(
new ButtonBuilder()
.setCustomId(idPrec)
.setLabel(loc.get("c_queue8"))
.setStyle(ButtonStyle.Primary)
)
.addComponents(
new ButtonBuilder()
.setCustomId(idNext)
.setLabel(loc.get("c_queue9"))
.setStyle(ButtonStyle.Primary)
)
);
// Buttons interactions
collect(client, interaction, idPrec);
collect(client, interaction, idNext);
} else {
// In case queue doesn't exists
embed.setDescription(loc.get("c_queue2"));
}
return {
embeds: [embed],
components: rows,
};
},
};

View file

@ -51,6 +51,7 @@ export default {
// Load all the command per categories
// TODO: Check if the command exist in the context (guild)
// TODO: List subcommands too
client.commands.categories.forEach((commands_name, category) => {
const commands = commands_name.reduce((data, command_name) => {
return data + `\`/${command_name}\`, `;

View file

@ -0,0 +1,120 @@
import { SlashCommandBuilder } from "@discordjs/builders";
import { ChatInputCommandInteraction, Client, EmbedBuilder } from "discord.js";
import { getLocale, getLocalizations } from "../../utils/locales";
import { getFilename } from "../../utils/misc";
export default {
scope: () => [],
data: (client: Client) => {
const filename = getFilename(__filename);
const loc_default = client.locales.get(client.config.default_lang);
if (!loc_default) {
return;
}
return (
new SlashCommandBuilder()
.setName(filename.toLowerCase())
.setDescription(loc_default.get(`c_${filename}_desc`) ?? "")
.setNameLocalizations(getLocalizations(client, `c_${filename}_name`, true))
.setDescriptionLocalizations(getLocalizations(client, `c_${filename}_desc`))
// Command option
.addStringOption((option) =>
option
.setName(loc_default.get(`c_${filename}_opt1_name`)?.toLowerCase() ?? "")
.setDescription(loc_default.get(`c_${filename}_opt1_desc`) ?? "")
.setNameLocalizations(getLocalizations(client, `c_${filename}_opt1_name`, true))
.setDescriptionLocalizations(getLocalizations(client, `c_${filename}_opt1_desc`))
)
);
},
interaction: async (interaction: ChatInputCommandInteraction, client: Client) => {
const loc_default = client.locales.get(client.config.default_lang);
const filename = getFilename(__filename);
const loc = getLocale(client, interaction.locale);
const request = interaction.options.getString(
loc_default?.get(`c_${filename}_opt1_name`) as string
);
let data = null;
if (request) {
try {
data = await client.player.lyrics.search(request);
} catch {
return await interaction.reply(loc.get("c_lyrics2") + ` \`${request}\``);
}
} else {
const queue = client.player.queues.get(interaction.guildId ?? "");
if (queue) {
const title = queue.current?.title;
if (title) {
try {
data = await client.player.lyrics.search(title);
} catch {
return await interaction.reply(loc.get("c_lyrics2") + ` \`${title}\``);
}
}
}
}
if (data) {
const limit_desc = 4096;
const nb_embed = Math.ceil(data.lyrics.length / limit_desc);
// May send multiples message
await interaction.deferReply();
// TODO: If lyrics < 6000, only send one message with multiples embed
for (let i = 0, j = 0; i < nb_embed; i++, j += limit_desc) {
// TODO: Better cut in lyrics
const lyrics = data.lyrics.slice(j, j + limit_desc);
let embed;
switch (i) {
case 0: {
// First embed
embed = new EmbedBuilder();
embed
.setTitle(data.title)
.setURL(data.url)
.setAuthor({
name: data?.artist.name,
iconURL: data?.artist.image,
url: data?.artist.url,
})
.setDescription(lyrics)
.setThumbnail(data.thumbnail);
break;
}
case nb_embed - 1: {
// Footer of last embed in case of multiple embed
embed = new EmbedBuilder().setDescription(lyrics).setFooter({
text: `${data?.artist.name} · ${data.title}`,
iconURL: data?.artist.image,
});
break;
}
default: {
// Embed with only lyrics in case of multiple embed
embed = new EmbedBuilder().setDescription(lyrics);
break;
}
}
// Send embed
await interaction.followUp({ embeds: [embed] });
}
return;
}
return await interaction.reply(loc.get("c_lyrics1"));
},
};

View file

@ -0,0 +1,44 @@
import { SlashCommandBuilder } from "@discordjs/builders";
import { ChatInputCommandInteraction, Client } from "discord.js";
import { getLocale, getLocalizations } from "../../utils/locales";
import { getFilename } from "../../utils/misc";
export default {
scope: () => [],
data: (client: Client) => {
const filename = getFilename(__filename);
const loc_default = client.locales.get(client.config.default_lang);
if (!loc_default) {
return;
}
return new SlashCommandBuilder()
.setName(filename.toLowerCase())
.setDescription(loc_default.get(`c_${filename}_desc`) ?? "")
.setNameLocalizations(getLocalizations(client, `c_${filename}_name`, true))
.setDescriptionLocalizations(getLocalizations(client, `c_${filename}_desc`));
},
interaction: async (interaction: ChatInputCommandInteraction, client: Client) => {
const loc = getLocale(client, interaction.locale);
const queue = client.player.queues.get(interaction.guildId ?? "");
if (queue) {
if (queue.paused) {
queue.resume();
// TODO: Pretty embed
return await interaction.reply(loc.get("c_pause1"));
} else {
queue.pause();
// TODO: Pretty embed
return await interaction.reply(loc.get("c_pause2"));
}
}
// TODO: Pretty embed
return await interaction.reply(loc.get("c_pause3"));
},
};

140
src/commands/music/play.ts Normal file
View file

@ -0,0 +1,140 @@
import { SlashCommandBuilder } from "@discordjs/builders";
import {
ChatInputCommandInteraction,
Client,
GuildResolvable,
VoiceBasedChannel,
} from "discord.js";
import { Metadata } from "../../utils/metadata";
import { getLocale, getLocalizations } from "../../utils/locales";
import { getFilename } from "../../utils/misc";
export default {
scope: () => [],
data: (client: Client) => {
const filename = getFilename(__filename);
const loc_default = client.locales.get(client.config.default_lang);
if (!loc_default) {
return;
}
return (
new SlashCommandBuilder()
.setName(filename.toLowerCase())
.setDescription(loc_default.get(`c_${filename}_desc`) ?? "")
.setNameLocalizations(getLocalizations(client, `c_${filename}_name`, true))
.setDescriptionLocalizations(getLocalizations(client, `c_${filename}_desc`))
// Command option
.addStringOption((option) =>
option
.setName(loc_default.get(`c_${filename}_opt1_name`)?.toLowerCase() ?? "")
.setDescription(loc_default.get(`c_${filename}_opt1_desc`) ?? "")
.setNameLocalizations(getLocalizations(client, `c_${filename}_opt1_name`, true))
.setDescriptionLocalizations(getLocalizations(client, `c_${filename}_opt1_desc`))
)
);
},
interaction: async (interaction: ChatInputCommandInteraction, client: Client) => {
const loc = getLocale(client, interaction.locale);
const loc_default = client.locales.get(client.config.default_lang);
const filename = getFilename(__filename);
const member = client.guilds.cache
.get(interaction.guildId ?? "")
?.members.cache.get(interaction.member?.user.id ?? "");
if (!member?.voice.channelId) {
return await interaction.reply({
content: loc.get("c_play1"),
ephemeral: true,
});
}
if (
interaction.guild?.members.me?.voice.channelId &&
member.voice.channelId !== interaction.guild.members.me.voice.channelId
) {
return await interaction.reply({
content: loc.get("c_play2"),
ephemeral: true,
});
}
const query = interaction.options.getString(
loc_default?.get(`c_${filename}_opt1_name`) as string
);
if (!query) {
// Now playing
const queue = client.player.queues.get(interaction.guildId ?? "");
if (queue) {
const track = queue.nowPlaying();
if (track) {
// TODO: Pretty embed
// Use: createProgressBar
return await interaction.reply(
`${loc.get("c_play7")} \`${track.title}\` - *${track.author}*`
);
}
}
// TODO: Pretty embed
return await interaction.reply(loc.get("c_play6"));
}
const queue = client.player.createQueue(interaction.guild as GuildResolvable, {
metadata: {
channel: interaction.channel,
} as Metadata,
});
// Verify vc connection
try {
if (!queue.connection) await queue.connect(member.voice.channel as VoiceBasedChannel);
} catch {
queue.destroy();
return await interaction.reply({
content: loc.get("c_play3"),
ephemeral: true,
});
}
await interaction.deferReply();
const result = await client.player
.search(query, {
requestedBy: interaction.user,
})
.then((x) => x);
if (!result.tracks[0]) {
// TODO: Pretty embed
return await interaction.followUp({ content: `❌ | \`${query}\` ${loc.get("c_play4")}.` });
}
let title;
if (result.playlist) {
queue.addTracks(result.playlist.tracks);
title = result.playlist.title;
} else {
// TODO: Ask user which result to choose
const track = result.tracks[0];
queue.addTrack(track);
title = track.title;
}
if (!queue.playing) {
queue.play();
}
// TODO: Pretty embed
return await interaction.followUp({
content: `⏱️ | \`${title}\` ${loc.get("c_play5")}.`,
});
},
};

170
src/commands/music/queue.ts Normal file
View file

@ -0,0 +1,170 @@
import { SlashCommandBuilder } from "@discordjs/builders";
import {
ActionRowBuilder,
ButtonBuilder,
ButtonStyle,
ChatInputCommandInteraction,
Client,
EmbedBuilder,
} from "discord.js";
import { v4 as uuidv4 } from "uuid";
import { collect } from "../../buttons/loader";
import { getLocale, getLocalizations } from "../../utils/locales";
import { getFilename } from "../../utils/misc";
import { embedListQueue } from "../../utils/music";
export default {
scope: () => [],
data: (client: Client) => {
const filename = getFilename(__filename);
const loc_default = client.locales.get(client.config.default_lang);
if (!loc_default) {
return;
}
return (
new SlashCommandBuilder()
.setName(filename.toLowerCase())
.setDescription(loc_default.get(`c_${filename}_desc`) ?? "")
.setNameLocalizations(getLocalizations(client, `c_${filename}_name`, true))
.setDescriptionLocalizations(getLocalizations(client, `c_${filename}_desc`))
// Show the queue
.addSubcommand((subcommand) =>
subcommand
.setName(loc_default.get(`c_${filename}_sub1_name`)?.toLowerCase() ?? "")
.setDescription(loc_default.get(`c_${filename}_sub1_desc`) ?? "")
.setNameLocalizations(getLocalizations(client, `c_${filename}_sub1_name`, true))
.setDescriptionLocalizations(getLocalizations(client, `c_${filename}_sub1_desc`))
// Specified Page
.addNumberOption((option) =>
option
.setName(loc_default.get(`c_${filename}_sub1_opt1_name`)?.toLowerCase() ?? "")
.setDescription(loc_default.get(`c_${filename}_sub1_opt1_desc`) ?? "")
.setNameLocalizations(
getLocalizations(client, `c_${filename}_sub1_opt1_name`, true)
)
.setDescriptionLocalizations(
getLocalizations(client, `c_${filename}_sub1_opt1_desc`)
)
)
)
// Shuffle Queue
.addSubcommand((subcommand) =>
subcommand
.setName(loc_default.get(`c_${filename}_sub2_name`)?.toLowerCase() ?? "")
.setDescription(loc_default.get(`c_${filename}_sub2_desc`) ?? "")
.setNameLocalizations(getLocalizations(client, `c_${filename}_sub2_name`, true))
.setDescriptionLocalizations(getLocalizations(client, `c_${filename}_sub2_desc`))
)
// Remove <ID>
.addSubcommand((subcommand) =>
subcommand
.setName(loc_default.get(`c_${filename}_sub3_name`)?.toLowerCase() ?? "")
.setDescription(loc_default.get(`c_${filename}_sub3_desc`) ?? "")
.setNameLocalizations(getLocalizations(client, `c_${filename}_sub3_name`, true))
.setDescriptionLocalizations(getLocalizations(client, `c_${filename}_sub3_desc`))
// Specified ID
// TODO?: ID range -> as a string: 5-8 remove 5, 6, 7, 8
.addNumberOption((option) =>
option
.setName(loc_default.get(`c_${filename}_sub3_opt1_name`)?.toLowerCase() ?? "")
.setDescription(loc_default.get(`c_${filename}_sub3_opt1_desc`) ?? "")
.setNameLocalizations(
getLocalizations(client, `c_${filename}_sub3_opt1_name`, true)
)
.setDescriptionLocalizations(
getLocalizations(client, `c_${filename}_sub3_opt1_desc`)
)
.setRequired(true)
)
)
);
},
interaction: async (interaction: ChatInputCommandInteraction, client: Client) => {
const loc_default = client.locales.get(client.config.default_lang);
const filename = getFilename(__filename);
const loc = getLocale(client, interaction.locale);
const queue = client.player.queues.get(interaction.guildId ?? "");
const embed = new EmbedBuilder();
const rows = [];
if (queue) {
const subcommand = interaction.options.getSubcommand();
switch (subcommand) {
// Show the queue
case loc_default?.get(`c_${filename}_sub1_name`)?.toLowerCase() ?? "": {
const page =
interaction.options.getNumber(
loc_default?.get(`c_${filename}_sub1_opt1_name`) as string
) ?? 1;
embedListQueue(client, embed, queue.tracks, page, interaction.locale);
const idPrec = "queueList-prec_" + uuidv4();
const idNext = "queueList-next_" + uuidv4();
rows.push(
new ActionRowBuilder<ButtonBuilder>()
.addComponents(
new ButtonBuilder()
.setCustomId(idPrec)
.setLabel(loc.get(`c_${filename}8`))
.setStyle(ButtonStyle.Primary)
)
.addComponents(
new ButtonBuilder()
.setCustomId(idNext)
.setLabel(loc.get(`c_${filename}9`))
.setStyle(ButtonStyle.Primary)
)
);
// Buttons interactions
collect(client, interaction, idPrec);
collect(client, interaction, idNext);
break;
}
// Shuffle Queue
case loc_default?.get(`c_${filename}_sub2_name`)?.toLowerCase() ?? "": {
queue.shuffle();
embed.setDescription(loc.get("c_queue3"));
break;
}
// Remove <ID>
case loc_default?.get(`c_${filename}_sub3_name`)?.toLowerCase() ?? "": {
const id = interaction.options.getNumber(
loc_default?.get(`c_${filename}_sub3_opt1_name`) as string
) as number;
const track = queue.remove(id - 1);
if (track) {
embed.setDescription(
`${loc.get("c_queue4")} #${id} \`${track.title}\` ${loc.get("c_queue5")}`
);
} else {
embed.setDescription(loc.get("c_queue6"));
}
break;
}
}
} else {
embed.setDescription(loc.get("c_queue2"));
}
return await interaction.reply({ embeds: [embed], components: rows });
},
};

View file

@ -0,0 +1,102 @@
import { SlashCommandBuilder } from "@discordjs/builders";
import { QueueRepeatMode } from "discord-player";
import { ChatInputCommandInteraction, Client } from "discord.js";
import { getLocale, getLocalizations } from "../../utils/locales";
import { getFilename } from "../../utils/misc";
export default {
scope: () => [],
data: (client: Client) => {
const filename = getFilename(__filename);
const loc_default = client.locales.get(client.config.default_lang);
if (!loc_default) {
return;
}
return (
new SlashCommandBuilder()
.setName(filename.toLowerCase())
.setDescription(loc_default.get(`c_${filename}_desc`) ?? "")
.setNameLocalizations(getLocalizations(client, `c_${filename}_name`, true))
.setDescriptionLocalizations(getLocalizations(client, `c_${filename}_desc`))
// Disable repeat
.addSubcommand((subcommand) =>
subcommand
.setName(loc_default.get(`c_${filename}_sub1_name`)?.toLowerCase() ?? "")
.setDescription(loc_default.get(`c_${filename}_sub1_desc`) ?? "")
.setNameLocalizations(getLocalizations(client, `c_${filename}_sub1_name`, true))
.setDescriptionLocalizations(getLocalizations(client, `c_${filename}_sub1_desc`))
)
// Repeat current track
.addSubcommand((subcommand) =>
subcommand
.setName(loc_default.get(`c_${filename}_sub2_name`)?.toLowerCase() ?? "")
.setDescription(loc_default.get(`c_${filename}_sub2_desc`) ?? "")
.setNameLocalizations(getLocalizations(client, `c_${filename}_sub2_name`, true))
.setDescriptionLocalizations(getLocalizations(client, `c_${filename}_sub2_desc`))
)
// Repeat queue
.addSubcommand((subcommand) =>
subcommand
.setName(loc_default.get(`c_${filename}_sub3_name`)?.toLowerCase() ?? "")
.setDescription(loc_default.get(`c_${filename}_sub3_desc`) ?? "")
.setNameLocalizations(getLocalizations(client, `c_${filename}_sub3_name`, true))
.setDescriptionLocalizations(getLocalizations(client, `c_${filename}_sub3_desc`))
)
// Enable autoplay
.addSubcommand((subcommand) =>
subcommand
.setName(loc_default.get(`c_${filename}_sub4_name`)?.toLowerCase() ?? "")
.setDescription(loc_default.get(`c_${filename}_sub4_desc`) ?? "")
.setNameLocalizations(getLocalizations(client, `c_${filename}_sub4_name`, true))
.setDescriptionLocalizations(getLocalizations(client, `c_${filename}_sub4_desc`))
)
);
},
interaction: async (interaction: ChatInputCommandInteraction, client: Client) => {
const loc_default = client.locales.get(client.config.default_lang);
const filename = getFilename(__filename);
const loc = getLocale(client, interaction.locale);
const queue = client.player.queues.get(interaction.guildId ?? "");
if (queue) {
const subcommand = interaction.options.getSubcommand();
switch (subcommand) {
// Disable
case loc_default?.get(`c_${filename}_sub1_name`)?.toLowerCase() ?? "": {
queue.setRepeatMode(QueueRepeatMode.OFF);
return interaction.reply(loc.get("c_repeat2"));
}
// Queue Repeat
case loc_default?.get(`c_${filename}_sub3_name`)?.toLowerCase() ?? "": {
queue.setRepeatMode(QueueRepeatMode.QUEUE);
return interaction.reply(loc.get("c_repeat3") + " " + loc.get("c_repeat6"));
}
// Autoplay
case loc_default?.get(`c_${filename}_sub4_name`)?.toLowerCase() ?? "": {
queue.setRepeatMode(QueueRepeatMode.AUTOPLAY);
return interaction.reply(loc.get("c_repeat4") + " " + loc.get("c_repeat6"));
}
// Track repeat
case loc_default?.get(`c_${filename}_sub2_name`)?.toLowerCase() ?? "": {
queue.setRepeatMode(QueueRepeatMode.TRACK);
return interaction.reply(
loc.get("c_repeat5") + ` ${queue.nowPlaying()?.title} ` + loc.get("c_repeat6")
);
}
}
}
return await interaction.reply(loc.get("c_repeat1"));
},
};

View file

@ -0,0 +1,58 @@
import { SlashCommandBuilder } from "@discordjs/builders";
import { ChatInputCommandInteraction, Client } from "discord.js";
import { getLocale, getLocalizations } from "../../utils/locales";
import { getFilename } from "../../utils/misc";
export default {
scope: () => [],
data: (client: Client) => {
const filename = getFilename(__filename);
const loc_default = client.locales.get(client.config.default_lang);
if (!loc_default) {
return;
}
return (
new SlashCommandBuilder()
.setName(filename.toLowerCase())
.setDescription(loc_default.get(`c_${filename}_desc`) ?? "")
.setNameLocalizations(getLocalizations(client, `c_${filename}_name`, true))
.setDescriptionLocalizations(getLocalizations(client, `c_${filename}_desc`))
// Command option
.addNumberOption((option) =>
option
.setName(loc_default.get(`c_${filename}_opt1_name`)?.toLowerCase() ?? "")
.setDescription(loc_default.get(`c_${filename}_opt1_desc`) ?? "")
.setNameLocalizations(getLocalizations(client, `c_${filename}_opt1_name`, true))
.setDescriptionLocalizations(getLocalizations(client, `c_${filename}_opt1_desc`))
)
);
},
interaction: async (interaction: ChatInputCommandInteraction, client: Client) => {
const loc_default = client.locales.get(client.config.default_lang);
const filename = getFilename(__filename);
const loc = getLocale(client, interaction.locale);
const queue = client.player.queues.get(interaction.guildId ?? "");
const id = interaction.options.getNumber(loc_default?.get(`c_${filename}_opt1_name`) as string);
if (queue) {
let msg;
if (id) {
queue.skipTo(id - 1);
msg = loc.get("c_skip3") + " #" + id + "...";
} else {
queue.skip();
msg = loc.get("c_skip1") + "...";
}
return await interaction.reply(msg);
}
return await interaction.reply(loc.get("c_skip2"));
},
};

View file

@ -0,0 +1,41 @@
import { SlashCommandBuilder } from "@discordjs/builders";
import { ChatInputCommandInteraction, Client, GuildResolvable } from "discord.js";
import { Metadata } from "../../utils/metadata";
import { getLocale, getLocalizations } from "../../utils/locales";
import { getFilename } from "../../utils/misc";
export default {
scope: () => [],
data: (client: Client) => {
const filename = getFilename(__filename);
const loc_default = client.locales.get(client.config.default_lang);
if (!loc_default) {
return;
}
return new SlashCommandBuilder()
.setName(filename.toLowerCase())
.setDescription(loc_default.get(`c_${filename}_desc`) ?? "")
.setNameLocalizations(getLocalizations(client, `c_${filename}_name`, true))
.setDescriptionLocalizations(getLocalizations(client, `c_${filename}_desc`));
},
interaction: async (interaction: ChatInputCommandInteraction, client: Client) => {
const loc = getLocale(client, interaction.locale);
const queue = client.player.createQueue(interaction.guild as GuildResolvable, {
metadata: {
channel: interaction.channel,
} as Metadata,
});
if (!(queue.connection || queue.playing)) {
return interaction.reply(loc.get("c_stop1"));
}
queue.destroy();
interaction.reply(loc.get("c_stop2"));
},
};

View file

@ -1,3 +1,4 @@
import { PlayerEvents } from "discord-player";
import { Client } from "discord.js";
import { readdir } from "fs/promises";
@ -27,6 +28,19 @@ export default async (client: Client) => {
}
const event_type = event_type_ext.join(".");
if (event_category == "player") {
if (once) {
// eslint-disable-next-line
return client.player.once(event_type as keyof PlayerEvents, (...args: any[]) => {
execute(...args, client);
});
}
// eslint-disable-next-line
return client.player.on(event_type as keyof PlayerEvents, (...args: any[]) => {
execute(...args, client);
});
}
if (once) {
return client.once(event_type, (...args) => {
execute(...args, client);

View file

@ -0,0 +1,7 @@
import { Queue } from "discord-player";
import { Metadata } from "../../utils/metadata";
/** https://discord-player.js.org/docs/main/master/typedef/PlayerEvents */
export default (_: Queue<Metadata>, error: Error) => {
console.error(error);
};

View file

@ -0,0 +1,7 @@
import { Queue } from "discord-player";
import { Metadata } from "../../utils/metadata";
/** https://discord-player.js.org/docs/main/master/typedef/PlayerEvents */
export default (_: Queue<Metadata>, error: Error) => {
console.error(error);
};

View file

@ -0,0 +1,7 @@
import { Queue, Track } from "discord-player";
import { Metadata } from "../../utils/metadata";
/** https://discord-player.js.org/docs/main/master/typedef/PlayerEvents */
export default (queue: Queue<Metadata>, track: Track) => {
queue.metadata?.channel?.send(`🎶 | Joue \`${track.title}\` demandé par ${track.requestedBy}.`);
};

View file

@ -11,7 +11,7 @@
"c_help_name": "Aide",
"c_help_desc": "Informations sur les commandes",
"c_help_opt1_name": "commande",
"c_help_opt1_desc": "Commande voulu en détail.",
"c_help_opt1_desc": "Commande voulu en détail",
"c_help1": "Liste des catégories et des commandes associées",
"c_help2": "`/help <commande>` pour obtenir plus d'informations sur une commande.",
"c_help3": "Impossible de trouver :",
@ -31,7 +31,7 @@
"c_prep_name": "Préparation",
"c_prep_desc": "Préparation des salons généraux pour la nouvelle année",
"c_prep_opt1_name": "année",
"c_prep_opt1_desc": "Nom de l'année à préparer'",
"c_prep_opt1_desc": "Nom de l'année à préparer",
"c_prep1": "Liste des catégories soumis à la préparation",
"c_prep2": "`L1`, `L2`, `L3`, `M1`, `M2`",
"c_prep3": "Impossible de trouver/nettoyer le salon :",
@ -76,5 +76,77 @@
"c_reminder15": "Message envoyé en DM car vous avez quitté",
"c_reminder16": "Message envoyé en DM car le serveur Discord n'est plus disponible.",
"c_reminder17": "Message d'il y a",
"c_reminder18": "Pas de message"
"c_reminder18": "Pas de message",
"c_play_name": "play",
"c_play_desc": "Joue une chanson/playlist, pas de requête affiche la chanson en cours actuellement",
"c_play_opt1_name": "requête",
"c_play_opt1_desc": "Ce que vous voulez écouter",
"c_play1": "Tu n'es dans aucun salon vocal.",
"c_play2": "Je suis déjà en vocal.",
"c_play3": "Impossible de rejoindre le salon vocal.",
"c_play4": "introuvable",
"c_play5": "ajouté à la file d'attente",
"c_play6": "Le bot ne joue rien en ce moment.",
"c_play7": "Joue actuellement",
"c_stop_name": "stop",
"c_stop_desc": "Stop la musique",
"c_stop1": "Le bot ne joue rien en ce moment.",
"c_stop2": "La musique à été arrêtée.",
"c_pause_name": "pause",
"c_pause_desc": "Met en pause ou relance la musique",
"c_pause1": "Relance la musique...",
"c_pause2": "Met en pause la musique.",
"c_pause3": "Le bot ne joue rien en ce moment.",
"c_queue_name": "queue",
"c_queue_desc": "Commande relative à la file d'attente des musiques",
"c_queue_sub1_name": "affiche",
"c_queue_sub1_desc": "Affiche la file d'attente des musiques",
"c_queue_sub1_opt1_name": "page",
"c_queue_sub1_opt1_desc": "Page à afficher",
"c_queue_sub2_name": "melange",
"c_queue_sub2_desc": "Mélange la file d'attente",
"c_queue_sub3_name": "retire",
"c_queue_sub3_desc": "Retire une chanson de la file d'attente",
"c_queue_sub3_opt1_name": "id",
"c_queue_sub3_opt1_desc": "ID de la chanson a retirer",
"c_queue1": "File d'attente",
"c_queue2": "La liste est vide.",
"c_queue3": "Liste d'attente mélangée",
"c_queue4": "Musique",
"c_queue5": "supprimée",
"c_queue6": "Cette ID n'existe pas.",
"c_queue7": "Page",
"c_queue8": "Précédent",
"c_queue9": "Suivant",
"c_queue10": "Désolé, une erreur est survenue.",
"c_skip_name": "skip",
"c_skip_desc": "Passe la chanson en cours",
"c_skip_opt1_name": "id",
"c_skip_opt1_desc": "ID de la chanson que vous voulez écouter",
"c_skip1": "Passe la chanson",
"c_skip2": "Le bot ne joue rien en ce moment.",
"c_skip3": "Passe à la chanson",
"c_lyrics_name": "paroles",
"c_lyrics_desc": "Affiche les paroles d'une chanson",
"c_lyrics_opt1_name": "chanson",
"c_lyrics_opt1_desc": "Chanson recherchée",
"c_lyrics1": "Le bot ne joue rien en ce moment et aucune chanson n'est renseignée.",
"c_lyrics2": "Impossible de trouver les paroles pour",
"c_repeat_name": "repeat",
"c_repeat_desc": "Commande relative à la répétition des musiques",
"c_repeat_sub1_name": "stop",
"c_repeat_sub1_desc": "Désactive la répétition",
"c_repeat_sub2_name": "track",
"c_repeat_sub2_desc": "Active la répétition pour la chanson en cours",
"c_repeat_sub3_name": "queue",
"c_repeat_sub3_desc": "Active la répétition pour la file actuelle",
"c_repeat_sub4_name": "autoplay",
"c_repeat_sub4_desc": "Active la lecture automatique",
"c_repeat1": "Le bot ne joue rien en ce moment.",
"c_repeat2": "Répétition désactivé.",
"c_repeat3": "Répétition de la file d'attente",
"c_repeat4": "Lecture automatique",
"c_repeat5": "Répétition de la chanson",
"c_repeat6": "activé."
}

View file

@ -1,11 +1,11 @@
import { Collection } from "discord.js";
import { SlashCommandBuilder } from "@discordjs/builders";
import { Database } from "sqlite3";
import { Player } from "discord-player";
export {};
declare module "discord.js" {
// eslint-disable-next-line no-shadow
export interface Client {
/** Store the configuration */
config: {
@ -83,6 +83,8 @@ declare module "discord.js" {
}
>;
};
/** Music player */
player: Player;
/** Store all the localizations */
locales: Map<string, Map<string, string>>;
db: Database;

16
src/modules/player.ts Normal file
View file

@ -0,0 +1,16 @@
import { LyricsData } from "@discord-player/extractor";
import { Client } from "genius-lyrics";
type LyricsClient = {
search: (query: string) => Promise<LyricsData | null>;
client: Client;
};
export {};
declare module "discord-player" {
export interface Player {
/** Lyrics client */
lyrics: LyricsClient;
}
}

View file

@ -3,6 +3,8 @@ import { readFileSync } from "fs";
import { loadLocales } from "./locales";
import "../modules/client";
import { Database } from "sqlite3";
import { Player } from "discord-player";
import { lyricsExtractor } from "@discord-player/extractor";
/** Creation of the client and definition of its properties. */
export default async () => {
@ -11,6 +13,7 @@ export default async () => {
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
GatewayIntentBits.GuildVoiceStates,
],
});
@ -35,6 +38,14 @@ export default async () => {
list: new Collection(),
};
client.player = new Player(client, {
ytdlOptions: {
filter: "audioonly",
},
});
client.player.lyrics = lyricsExtractor();
console.log("Translations progression :");
client.locales = await loadLocales(client.config.default_lang);

View file

@ -50,6 +50,7 @@ export const loadLocales = async (default_lang: string) => {
* we fallback to default lang.
* @param client Client
* @param text Name of string to fetch
* @param lowercase Should the output be lowercased?
* @returns the dictionary
*/
export const getLocalizations = (client: Client, text: string, lowercase = false) => {

5
src/utils/metadata.ts Normal file
View file

@ -0,0 +1,5 @@
import { TextBasedChannel } from "discord.js";
export type Metadata = {
channel: TextBasedChannel | null;
};

28
src/utils/music.ts Normal file
View file

@ -0,0 +1,28 @@
import { EmbedBuilder } from "@discordjs/builders";
import { Track } from "discord-player";
import { Client } from "discord.js";
import { getLocale } from "./locales";
export const embedListQueue = (
client: Client,
embed: EmbedBuilder,
tracks: Track[],
page: number,
local: string
) => {
const loc = getLocale(client, local);
// Limit of discord is 25
const limit_fields = 25;
const pageMax = Math.ceil(tracks.length / limit_fields);
embed.setAuthor({ name: `${loc.get("c_queue1")}${loc.get("c_queue7")} ${page}/${pageMax}` });
tracks.slice((page - 1) * limit_fields, page * limit_fields).forEach((t, idx) =>
embed.addFields({
name: "\u200b",
value: `${(idx + 1) * page}. [${t.title}](${t.url}) (${t.duration})`,
})
);
};