music-start-pro
Version:
Music Start Pro is a discord bot that can play YouTube music by slash command.
343 lines (330 loc) • 19.2 kB
text/typescript
import {
Client,
GatewayIntentBits,
Interaction,
Guild,
MessagePayload,
MessageEditOptions,
Options,
Events
} from 'discord.js';
import ytdl from '@distube/ytdl-core';
import { CachedMusicInfo, MusicInfo } from './musicInfo';
import { Util } from './util';
import { Bucket } from './bucket';
import { Commands } from './commands';
import { messages } from './language.json';
import * as fs from 'node:fs';
import YAML from 'yaml';
import path from 'node:path';
interface langMap {
[key: string]: string;
}
const logFile = 'data.json';
export function main(token: string, useLog: boolean) {
// load buckets
if (useLog) {
console.log("The log file will be read/written at:", logFile);
console.log("You can disable log file by -d or --disable-log");
Bucket.load(logFile);
} else {
console.log("Log file: disable");
Bucket.disableLog();
}
const progressBarLen = 35;
const client: Client = new Client({
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.GuildVoiceStates]
});
client.login(token);
client.once(Events.ClientReady, (client: Client<true>) => {
console.log(`Logged in as ${client.user?.tag}!`);
});
client.on(Events.GuildCreate, async (guild: Guild) => {
console.log(`guild crated! ${guild.id}`);
Commands.register(guild);
});
client.on(Events.Error, (e: Error) => {
console.log('client on error', e);
});
client.on(Events.InteractionCreate, async (interaction: Interaction) => {
if (!interaction.guildId) return;
const bucket = Bucket.find(interaction.guildId);
if (interaction.isButton()) {
const ids = interaction.customId.split('-');
if (ids.length === 2 || ids.length === 3) {
const currentPage = parseInt(ids[1]);
switch (ids[0]) {
case 'next':
await interaction.update(bucket.queue.showList(bucket.lang, bucket.queue.genericPage(currentPage + 1), false) as MessageEditOptions);
break;
case 'pre':
await interaction.update(bucket.queue.showList(bucket.lang, bucket.queue.genericPage(currentPage - 1), false) as MessageEditOptions);
break;
case 'nextSearch':
await interaction.update(bucket.queue.showList(bucket.lang, bucket.queue.genericPage(currentPage + 1), true) as MessageEditOptions);
break;
case 'preSearch':
await interaction.update(bucket.queue.showList(bucket.lang, bucket.queue.genericPage(currentPage - 1), true) as MessageEditOptions);
break;
}
} else if (ids.length === 1) {
if (ids[0] === 'refresh') {
await interaction.update(bucket.queue.showList(bucket.lang) as MessageEditOptions);
} else if (ids[0] === 'refreshSearch') {
// search again
bucket.queue.search(null);
await interaction.update(bucket.queue.showList(bucket.lang, 0, true) as MessageEditOptions);
}
}
} else if (interaction.isCommand()) {
if (interaction.commandName === 'attach') {
Commands.register(interaction.guild, bucket?.lang);
if (bucket.connect(interaction)) {
await interaction.reply(`☆${(messages.hello as langMap)[bucket.lang]} ${Util.randomHappy()} ☆`);
} else {
await interaction.reply(`${(messages.attach_fail as langMap)[bucket.lang]} ${Util.randomCry()}`);
}
} else if (interaction.commandName === 'detach') {
bucket.player.pause();
bucket.disconnect();
await interaction.reply(`${(messages.detach as langMap)[bucket.lang]} ${Util.randomHappy()}`);
} else if (interaction.commandName === 'append') {
await interaction.deferReply();
const url = interaction.options.get('youtube')?.value as string;
if (!ytdl.validateURL(url) && !ytdl.validateID(url)) {
await interaction.editReply(Util.createEmbedMessage((messages.error as langMap)[bucket.lang],
`${(messages.song_is_not_found as langMap)[bucket.lang]} ${Util.randomCry()}`, true));
return;
}
const res = await ytdl.getInfo(url);
const info = MusicInfo.fromDetails(res);
if (info !== null) {
bucket.queue.add(info);
await interaction.editReply(new MessagePayload(interaction, Util.createEmbedMessage((messages.appended_to_the_playlist as langMap)[bucket.lang], info.title)));
} else {
await interaction.editReply(new MessagePayload(interaction, Util.createEmbedMessage((messages.error as langMap)[bucket.lang],
`${(messages.song_is_not_found as langMap)[bucket.lang]} ${Util.randomCry()}`, true)));
}
} else if (interaction.commandName === 'pause' || interaction.commandName === 'resume') {
if (bucket.queue.isEmpty()) {
await interaction.reply((messages.playlist_is_empty_error as langMap)[bucket.lang]);
} else if (interaction.commandName === 'pause' && bucket.player.pause()) {
await interaction.reply((messages.paused as langMap)[bucket.lang]);
} else if (interaction.commandName === 'resume' && bucket.player.unpause()) {
await interaction.reply((messages.resume as langMap)[bucket.lang]);
} else {
await interaction.reply(new MessagePayload(interaction, Util.createEmbedMessage((messages.error as langMap)[bucket.lang],
`${(messages.pause_error as langMap)[bucket.lang]} ${Util.randomCry()}`, true)));
}
} else if (interaction.commandName === 'stop') {
if (bucket.queue.isEmpty()) {
await interaction.reply((messages.playlist_is_empty_error as langMap)[bucket.lang]);
} else {
bucket.player.pause();
bucket.queue.jump(0);
await interaction.reply((messages.stopped as langMap)[bucket.lang]);
}
} else if (interaction.commandName === 'repeat') {
if (bucket.queue.isEmpty()) {
await interaction.reply((messages.playlist_is_empty_error as langMap)[bucket.lang]);
} else {
const currentState: boolean = bucket.toggleRepeat();
if (currentState) {
await interaction.reply((messages.repeat_on as langMap)[bucket.lang]);
} else {
await interaction.reply((messages.repeat_off as langMap)[bucket.lang]);
}
}
} else if (interaction.commandName === 'list' || interaction.commandName === 'distinct') {
if (interaction.commandName === 'distinct') {
// remove duplicate songs and show list
bucket.queue.removeDuplicate();
}
await interaction.reply(new MessagePayload(interaction, bucket.queue.showList(bucket.lang) as Options));
} else if (interaction.commandName === 'search') {
let query: RegExp | null = null;
try {
query = new RegExp(interaction.options.get('regexp')?.value as string, "i");
} catch (e) {
await interaction.reply(new MessagePayload(interaction, Util.createEmbedMessage((messages.error as langMap)[bucket.lang],
`Invalid Regular Expression ${Util.randomCry()}\n${e}`, true)));
return;
}
bucket.queue.search(query);
await interaction.reply(new MessagePayload(interaction, bucket.queue.showList(bucket.lang, 0, true)));
} else if (interaction.commandName === 'current') {
let description: string = `Volume: ${bucket.volume}\n`;
description += `Is playing: ${bucket.playing ? "yes" : "no"}\n`;
description += `Number of songs: ${bucket.queue.len}`;
await interaction.reply(new MessagePayload(interaction, Util.createEmbedMessage("current", description)));
if (bucket.playing) {
await interaction.followUp(new MessagePayload(interaction, Util.createMusicInfoMessage(bucket.queue.current)));
}
} else if (interaction.commandName === 'jump' || interaction.commandName === 'go') {
if (bucket.queue.isEmpty()) {
await interaction.reply((messages.playlist_is_empty_error as langMap)[bucket.lang]);
return;
}
await interaction.deferReply();
const index = interaction.options.get('index')?.value as number;
bucket.queue.jump(index);
await bucket.playAndEditReplyDefault(bucket.queue.current, interaction);
} else if (interaction.commandName === 'swap') {
if (bucket.queue.isEmpty()) {
await interaction.reply((messages.playlist_is_empty_error as langMap)[bucket.lang]);
return;
}
const index1 = interaction.options.get('index1')?.value as number;
const index2 = interaction.options.get('index2')?.value as number;
bucket.queue.swap(index1, index2);
await interaction.reply("Done!");
} else if (interaction.commandName === 'remove') {
if (bucket.queue.isEmpty()) {
await interaction.reply((messages.playlist_is_empty_error as langMap)[bucket.lang]);
return;
}
const index = interaction.options.get('index')?.value as number;
// if remove success
if (bucket.queue.remove(index, bucket.playing)) {
await interaction.reply(new MessagePayload(interaction, Util.createEmbedMessage((messages.success as langMap)[bucket.lang],
`${(messages.removed_successfully as langMap)[bucket.lang]} ${Util.randomHappy()}`)));
return;
}
await interaction.reply(new MessagePayload(interaction, Util.createEmbedMessage((messages.error as langMap)[bucket.lang],
`${(messages.cannot_remove_the_playing_song as langMap)[bucket.lang]} ${Util.randomCry()}`, true)));
} else if (interaction.commandName === 'clear') {
if (bucket.playing) {
bucket.player.stop();
}
bucket.queue.removeAll();
bucket.store();
await interaction.reply((messages.playlist_is_reset as langMap)[bucket.lang]);
} else if (interaction.commandName === 'sort') {
if (bucket.queue.isEmpty()) {
await interaction.reply((messages.playlist_is_empty_error as langMap)[bucket.lang]);
return;
}
bucket.queue.sort();
await interaction.reply(`${(messages.is_sorted as langMap)[bucket.lang]} ${Util.randomHappy()}`);
} else if (interaction.commandName === 'shuffle') {
bucket.queue.shuffle();
await interaction.reply(`${(messages.is_shuffled as langMap)[bucket.lang]} ${Util.randomHappy()}`);
} else if (interaction.commandName === 'next') {
if (bucket.queue.isEmpty()) {
await interaction.reply((messages.playlist_is_empty_error as langMap)[bucket.lang]);
return;
}
let offset: number = 1;
if (interaction.options.get("offset") != null) {
offset = interaction.options.get("offset")!.value as number;
}
await interaction.deferReply();
bucket.queue.next(offset);
await bucket.playAndEditReplyDefault(bucket.queue.current, interaction);
} else if (interaction.commandName === 'pre') {
if (bucket.queue.isEmpty()) {
await interaction.reply((messages.playlist_is_empty_error as langMap)[bucket.lang]);
return;
}
await interaction.deferReply();
bucket.queue.next(-1);
await bucket.playAndEditReplyDefault(bucket.queue.current, interaction);
} else if (interaction.commandName === 'vol') {
await interaction.deferReply();
if (interaction.options.get('volume') === null) {
interaction.editReply(`${(messages.current_volume as langMap)[bucket.lang]}${bucket.volume} ${Util.randomHappy()}`);
} else {
const vol = interaction.options.get('volume')!.value as number;
if (vol < 0 || vol > 1) {
interaction.editReply(`${(messages.volume_format_error as langMap)[bucket.lang]} ${Util.randomCry()}`);
} else {
bucket.volume = vol;
interaction.editReply(`${(messages.volume_is_changed_to as langMap)[bucket.lang]} ${vol} ${Util.randomHappy()}`);
}
}
} else if (interaction.commandName === 'verbose') {
bucket.verbose = interaction.options.get('truth')?.value as boolean;
interaction.reply(new MessagePayload(interaction, Util.createEmbedMessage('verbose: ', bucket.verbose ? 'on' : 'off')));
} else if (interaction.commandName === 'json') {
await interaction.deferReply();
if (interaction.options.get('json') === null) {
const batch = 100;
if (bucket.queue.isEmpty()) {
await interaction.reply((messages.playlist_is_empty as langMap)[bucket.lang]);
}
for (let j = 0; j < bucket.queue.len / batch; j++) {
const url: Array<string> = [];
for (let i = batch * j; i < Math.min(bucket.queue.len, batch * (j + 1)); i++) {
url.push(bucket.queue.list[i].url.replace('https://www.youtube.com/watch?v=', ''));
}
if (j == 0) {
await interaction.editReply('```\n' + JSON.stringify(url, null, '') + '\n```');
} else {
await interaction.followUp(`${j * batch}~${Math.min(bucket.queue.len, batch * (j + 1))}`);
await interaction.followUp('```\n' + JSON.stringify(url, null, '') + '\n```');
}
}
} else {
try {
const list = JSON.parse(interaction.options.get('json')!.value as string);
const downloadListener = Util.sequentialEnqueueWithBatchListener();
downloadListener.on('progress', (current, all) => {
interaction.editReply('```yaml\n' + Util.progressBar(current, all, progressBarLen) + '\n```');
});
downloadListener.once('done', (all, fail) => {
downloadListener.removeAllListeners();
interaction.editReply('```yaml\n' + Util.progressBar(all, all, progressBarLen) + ' ✅\n\n'+`✅: ${all-fail} ❌: ${fail}`+'```');
});
downloadListener.once('error', (e) => {
downloadListener.removeAllListeners();
interaction.editReply(Util.createEmbedMessage('Error sequentialEnQueueWithBatch()', `${Util.randomCry()}\n${e}`, true));
});
Util.sequentialEnQueueWithBatch(list, bucket.queue, downloadListener);
} catch (e) {
await interaction.editReply(Util.createEmbedMessage('Error JSON.parse()', `${Util.randomCry()}\n${e}`, true));
return;
}
}
} else if (interaction.commandName === 'aqours' ||
interaction.commandName === 'llss' ||
interaction.commandName === 'genjitsu' ||
interaction.commandName === 'azalea' ||
interaction.commandName === 'muse' ||
interaction.commandName === 'liella' ||
interaction.commandName === 'nijigasaki' ||
interaction.commandName === 'q4' ||
interaction.commandName === 'hasunosora' ||
interaction.commandName === '5yncri5e') {
// fetch recommend music list
const file = fs.readFileSync(path.join(__dirname, `../recommend/${interaction.commandName}.yaml`), 'utf-8');
const data: {
list: CachedMusicInfo[]
} = YAML.parse(file);
// done message
let done: string = "";
if (interaction.commandName === 'aqours' || interaction.commandName === 'llss' || interaction.commandName === 'genjitsu') {
done = 'Aqours SunShine!';
} else if (interaction.commandName === 'azalea') {
done = '恋の喜び咲かせます、AZALEAです! ';
} else if (interaction.commandName === 'muse') {
done = "μ's music start!";
} else if (interaction.commandName === 'liella') {
done = 'Song for me, song for you, song for all!';
}
await interaction.deferReply();
Util.enQueueCached(data.list, bucket.queue);
interaction.editReply('```yaml\n' + Util.progressBar(data.list.length, data.list.length, 35) + ' ✅\n```' + `${done}`);
} else if (interaction.commandName === 'lang') {
const lang: string = interaction.options.get('language')?.value as string;
if (['zh', 'en'].includes(lang)) {
bucket.lang = lang;
await interaction.reply((messages.language_changed_successfully as langMap)[bucket.lang]);
} else {
await interaction.reply("Should be either zh or en!");
}
} else {
await interaction.reply("Command not found");
}
}
});
}