UNPKG

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