Added Music Bot Funationality 🎶
This commit is contained in:
parent
0998cb71d8
commit
bb67bcb79a
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"defaultPrefix": "d!",
|
||||
"defaultPrefix": "h!",
|
||||
"disabledCommandCatagories": [],
|
||||
"githubLink": "https://github.com/cktsun1031/DraconianBot",
|
||||
"githubLink": "https://git.itsmebravo.dev/bravo68web/b68",
|
||||
"name": "Hanako",
|
||||
"ownerId": "457039372009865226",
|
||||
"useShards": false
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"MAX_PLAYLIST_SIZE": 10,
|
||||
"PREFIX": "u&",
|
||||
"PRUNING": false,
|
||||
"LOCALE": "en",
|
||||
"STAY_TIME": 30,
|
||||
"DEFAULT_VOLUME": 100
|
||||
}
|
|
@ -0,0 +1,179 @@
|
|||
{
|
||||
"clip": {
|
||||
"description": "Plays a clip sound",
|
||||
"usagesReply": "Usage: {prefix}clip <name>",
|
||||
"errorQueue": "Can't play clip because there is an active queue.",
|
||||
"errorNotChannel": "You need to join a voice channel first!"
|
||||
},
|
||||
"clips": {
|
||||
"description": "List all clips"
|
||||
},
|
||||
"help": {
|
||||
"description": "Display all commands and descriptions",
|
||||
"embedTitle": "{botname} Help",
|
||||
"embedDescription": "List of all commands"
|
||||
},
|
||||
"invite": {
|
||||
"description": "Send bot invite link"
|
||||
},
|
||||
"loop": {
|
||||
"description": "Toggle music loop",
|
||||
"errorNotQueue": "There is nothing playing.",
|
||||
"result": "Loop is now {loop}"
|
||||
},
|
||||
"lyrics": {
|
||||
"description": "Get lyrics for the currently playing song",
|
||||
"errorNotQueue": "There is nothing playing.",
|
||||
"lyricsNotFound": "No lyrics found for {title}.",
|
||||
"embedTitle": "{title} - Lyrics"
|
||||
},
|
||||
"move": {
|
||||
"description": "Move songs around in the queue",
|
||||
"errorNotQueue": "There is no queue.",
|
||||
"usagesReply": "Usage: {prefix}move <Queue Number>",
|
||||
"result": "<@{author}> 🚚 moved **{title}** to {index} in the queue.",
|
||||
"args": {
|
||||
"movefrom": "Slot to move from",
|
||||
"moveto": "Slot to move to"
|
||||
}
|
||||
},
|
||||
"nowplaying": {
|
||||
"description": "Show now playing song",
|
||||
"errorNotQueue": "There is nothing playing.",
|
||||
"embedTitle": "Now playing",
|
||||
"live": " ◉ LIVE",
|
||||
"timeRemaining": "Time Remaining: {time}"
|
||||
},
|
||||
"pause": {
|
||||
"description": "Pause the currently playing music",
|
||||
"errorNotQueue": "There is nothing playing.",
|
||||
"result": "<@{author}> ⏸ paused the music."
|
||||
},
|
||||
"ping": {
|
||||
"description": "Show the bot's average ping",
|
||||
"result": "📈 Average ping to API: {ping} ms"
|
||||
},
|
||||
"play": {
|
||||
"description": "Plays audio from YouTube",
|
||||
"errorNotChannel": "You need to join a voice channel first!",
|
||||
"errorNotInSameChannel": "You must be in the same channel as {user}",
|
||||
"usageReply": "Usage: {prefix}play <YouTube URL | Video Name>",
|
||||
"missingPermissionConnect": "Cannot connect to voice channel, missing permissions",
|
||||
"missingPermissionSpeak": "I cannot speak in this voice channel, make sure I have the proper permissions!",
|
||||
"queueAdded": "✅ **{title}** has been added to the queue by <@{author}>",
|
||||
"cantJoinChannel": "Could not join the channel: {error}",
|
||||
"queueEnded": "❌ Music queue ended.",
|
||||
"queueError": "Error: {error}",
|
||||
"startedPlaying": "🎶 Started playing: **{title}** {url}",
|
||||
"skipSong": "<@{author}> ⏩ skipped the song",
|
||||
"pauseSong": "<@{author}> ⏸ paused the music.",
|
||||
"resumeSong": "<@{author}> ▶ resumed the music!",
|
||||
"unmutedSong": "<@{author}> 🔊 unmuted the music!",
|
||||
"mutedSong": "<@{author}> 🔇 muted the music!",
|
||||
"decreasedVolume": "<@{author}> 🔉 decreased the volume, the volume is now {volume}%",
|
||||
"increasedVolume": "<@{author}> 🔊 increased the volume, the volume is now {volume}%",
|
||||
"loopSong": "<@{author}> Loop is now {loop}",
|
||||
"stopSong": "<@{author}> ⏹ stopped the music!",
|
||||
"leaveChannel": "Leaving voice channel...",
|
||||
"songNotFound": "Audio Not Found",
|
||||
"songAccessErr": "Video is age restricted, private or unavailable",
|
||||
"errorNoResults": "No results found for {url}",
|
||||
"errorInvalidURL": "Invalid URL, please try a search or youtube url"
|
||||
},
|
||||
"playlist": {
|
||||
"description": "Play a playlist from youtube",
|
||||
"usagesReply": "Usage: {prefix}playlist <YouTube Playlist URL | Playlist Name>",
|
||||
"errorNotChannel": "You need to join a voice channel first!",
|
||||
"errorNotInSameChannel": "You must be in the same channel as {user}",
|
||||
"missingPermissionConnect": "Cannot connect to voice channel, missing permissions",
|
||||
"missingPermissionSpeak": "I cannot speak in this voice channel, make sure I have the proper permissions!",
|
||||
"errorNotFoundPlaylist": "Playlist not found :(",
|
||||
"fetchingPlaylist": "⌛ fetching the playlist...",
|
||||
"playlistCharLimit": "\nPlaylist larger than character limit...",
|
||||
"startedPlaylist": "<@{author}> Started a playlist",
|
||||
"cantJoinChannel": "Could not join the channel: {error}"
|
||||
},
|
||||
"pruning": {
|
||||
"description": "Toggle pruning of bot messages",
|
||||
"errorWritingFile": "There was an error writing to the file.",
|
||||
"result": "Message pruning is {result}"
|
||||
},
|
||||
"queue": {
|
||||
"description": "Show the music queue and now playing.",
|
||||
"missingPermissionMessage": "Missing permission to manage messages or add reactions",
|
||||
"errorNotQueue": "❌ **Nothing playing in this server**",
|
||||
"currentPage": "Current Page - ",
|
||||
"embedTitle": "Song Queue\n",
|
||||
"embedCurrentSong": "**Current Song - [{title}]({url})**\n\n{info}"
|
||||
},
|
||||
"remove": {
|
||||
"description": "Remove song from the queue",
|
||||
"errorNotQueue": "There is no queue.",
|
||||
"usageReply": "Usage: {prefix}remove <Queue Number>",
|
||||
"result": "<@{author}> ❌ removed **{title}** from the queue."
|
||||
},
|
||||
"resume": {
|
||||
"description": "Resume currently playing music",
|
||||
"errorNotQueue": "There is nothing playing.",
|
||||
"resultNotPlaying": "<@{author}> ▶ resumed the music!",
|
||||
"errorPlaying": "The queue is not paused."
|
||||
},
|
||||
"search": {
|
||||
"description": "Search and select videos to play",
|
||||
"usageReply": "Usage: {prefix}{name} <Video Name>",
|
||||
"errorAlreadyCollector": "A message collector is already active in this channel.",
|
||||
"errorNotChannel": "You need to join a voice channel first!",
|
||||
"resultEmbedTitle": "**Reply with the song number you want to play**",
|
||||
"resultEmbedDesc": "Results for: {search}",
|
||||
"optionQuery": "Search query"
|
||||
},
|
||||
"shuffle": {
|
||||
"description": "Shuffle queue",
|
||||
"errorNotQueue": "There is no queue.",
|
||||
"result": "<@{author}> 🔀 shuffled the queue"
|
||||
},
|
||||
"skip": {
|
||||
"description": "Skip the currently playing song",
|
||||
"errorNotQueue": "There is nothing playing that I could skip for you.",
|
||||
"result": "<@{author}> ⏭ skipped the song"
|
||||
},
|
||||
"skipto": {
|
||||
"description": "Skip to the selected queue number",
|
||||
"usageReply": "Usage: {prefix}{name} <Queue Number>",
|
||||
"errorNotQueue": "There is no queue.",
|
||||
"errorNotValid": "The queue is only {length} songs long!",
|
||||
"result": "<@{author}> ⏭ skipped {arg} songs",
|
||||
"args": {
|
||||
"number": "The queue number to skip to"
|
||||
}
|
||||
},
|
||||
"stop": {
|
||||
"description": "Stops the music",
|
||||
"errorNotQueue": "There is nothing playing.",
|
||||
"result": "<@{author}> ⏹ stopped the music!"
|
||||
},
|
||||
"uptime": {
|
||||
"description": "Check the uptime",
|
||||
"result": "Uptime: `{days} day(s),{hours} hours, {minutes} minutes, {seconds} seconds`"
|
||||
},
|
||||
"volume": {
|
||||
"description": "Change volume of currently playing music",
|
||||
"errorNotQueue": "There is nothing playing.",
|
||||
"errorNotChannel": "You need to join a voice channel first!",
|
||||
"currentVolume": "🔊 The current volume is: **{volume}%**",
|
||||
"errorNotNumber": "Please use a number to set volume.",
|
||||
"errorNotValid": "Please use a number between 0 - 100.",
|
||||
"result": "Volume set to: **{arg}%**"
|
||||
},
|
||||
"common": {
|
||||
"on": "**on**",
|
||||
"off": "**off**",
|
||||
"enabled": "**enabled**",
|
||||
"disabled": "**disabled**",
|
||||
"errorNotChannel": "You need to join a voice channel first!",
|
||||
"cooldownMessage": "please wait {time} more second(s) before reusing the `{name}` command.",
|
||||
"errorCommand": "There was an error executing that command."
|
||||
},
|
||||
"Invite me to your server!": "Invite me to your server!",
|
||||
"Invite": "Invite"
|
||||
}
|
|
@ -10,16 +10,20 @@
|
|||
"devDependencies": {
|
||||
"@types/crypto-js": "^4.1.1",
|
||||
"@types/gifencoder": "^2.0.1",
|
||||
"@types/i18n": "^0.13.6",
|
||||
"@types/node": "^20.1.3",
|
||||
"@types/pidusage": "^2.0.2",
|
||||
"@types/string-similarity": "^4.0.0",
|
||||
"nodemon": "^2.0.22",
|
||||
"prettier": "^2.8.8",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^5.0.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@discordjs/core": "^0.6.0",
|
||||
"@discordjs/opus": "^0.9.0",
|
||||
"@discordjs/rest": "^1.7.1",
|
||||
"@discordjs/voice": "^0.16.0",
|
||||
"@esbuild-kit/esm-loader": "^2.5.5",
|
||||
"@types/xml2js": "^0.4.11",
|
||||
"axios": "^1.4.0",
|
||||
|
@ -30,21 +34,34 @@
|
|||
"dotenv": "^16.0.3",
|
||||
"express": "^4.18.2",
|
||||
"fastify": "^4.17.0",
|
||||
"ffmpeg-static": "^4.4.1",
|
||||
"gifencoder": "^2.0.1",
|
||||
"glob": "^10.2.3",
|
||||
"graphql": "^16.6.0",
|
||||
"graphql-request": "^6.0.0",
|
||||
"i18n": "^0.15.1",
|
||||
"napi-nanoid": "^0.2.0",
|
||||
"node-cache": "^5.1.2",
|
||||
"pidusage": "^3.0.2",
|
||||
"play-dl": "^1.9.6",
|
||||
"redis": "^4.6.6",
|
||||
"soundcloud-downloader": "^0.2.3",
|
||||
"string-progressbar": "^1.0.4",
|
||||
"string-similarity": "^4.0.4",
|
||||
"xml2js": "^0.5.0",
|
||||
"youtube-sr": "^4.3.4",
|
||||
"zod": "^3.21.4"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "NODE_ENV=development nodemon --watch 'src/**/*.ts' --exec node --loader @esbuild-kit/esm-loader src/index.ts",
|
||||
"start": "node --loader @esbuild-kit/esm-loader src/index.ts",
|
||||
"build": "tsc"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@discordjs/opus": "^0.9.0",
|
||||
"libsodium-wrappers": "^0.7.11",
|
||||
"opusscript": "^0.1.0",
|
||||
"sodium-native": "^3.4.1",
|
||||
"tweetnacl": "^1.0.3"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
import { Client, Collection, GatewayIntentBits, Partials } from 'discord.js'
|
||||
import {
|
||||
ApplicationCommandDataResolvable,
|
||||
Client,
|
||||
Collection,
|
||||
Snowflake,
|
||||
} from 'discord.js'
|
||||
|
||||
import './server'
|
||||
|
||||
|
@ -6,40 +11,36 @@ import { loadSlashCommand, loadTextCommand } from './loaders/command'
|
|||
import { loadDiscordEvent } from './loaders/event'
|
||||
import type { SlashCommand, TextCommand } from './sturctures/command'
|
||||
import { hgqlInit } from './utils/database'
|
||||
import { MusicQueue } from './utils/musicQueue'
|
||||
import { Command } from './interfaces/Command'
|
||||
|
||||
const client = new Client({
|
||||
intents: [
|
||||
GatewayIntentBits.Guilds,
|
||||
GatewayIntentBits.GuildMembers,
|
||||
GatewayIntentBits.GuildMessages,
|
||||
GatewayIntentBits.DirectMessages,
|
||||
GatewayIntentBits.MessageContent,
|
||||
],
|
||||
allowedMentions: { parse: ['users', 'roles'], repliedUser: false },
|
||||
partials: [
|
||||
Partials.User,
|
||||
Partials.Channel,
|
||||
Partials.Message,
|
||||
Partials.GuildMember,
|
||||
],
|
||||
})
|
||||
export class Bot {
|
||||
public prefix = 'h!'
|
||||
public commands = new Collection<string, Command>()
|
||||
public slashCommands = new Array<ApplicationCommandDataResolvable>()
|
||||
public slashCommandsMap = new Collection<string, Command>()
|
||||
public cooldowns = new Collection<string, Collection<Snowflake, number>>()
|
||||
public queues = new Collection<Snowflake, MusicQueue>()
|
||||
|
||||
client.commands = new Collection()
|
||||
client.commandsCatagories = new Collection()
|
||||
client.aliases = new Collection()
|
||||
client.slashcommands = new Collection()
|
||||
public constructor(public client: Client) {
|
||||
this.client.commands = new Collection()
|
||||
this.client.commandsCatagories = new Collection()
|
||||
this.client.aliases = new Collection()
|
||||
this.client.slashcommands = new Collection()
|
||||
|
||||
await loadDiscordEvent(client)
|
||||
await loadTextCommand(client)
|
||||
loadDiscordEvent(this.client)
|
||||
loadTextCommand(this.client)
|
||||
|
||||
await hgqlInit()
|
||||
hgqlInit()
|
||||
|
||||
await client.login(process.env.TOKEN)
|
||||
|
||||
await loadSlashCommand(client, process.env.CLIENT_ID, process.env.TOKEN)
|
||||
|
||||
export default client
|
||||
this.client.login(process.env.TOKEN)
|
||||
|
||||
this.client.on('ready', () => {
|
||||
console.log(`🟢 Logged in as ${client.user?.tag}!`)
|
||||
loadSlashCommand(client, process.env.CLIENT_ID, process.env.TOKEN)
|
||||
})
|
||||
}
|
||||
}
|
||||
// declare types.
|
||||
declare module 'discord.js' {
|
||||
export interface Client {
|
||||
|
@ -47,5 +48,6 @@ declare module 'discord.js' {
|
|||
commands: Collection<string, TextCommand>
|
||||
slashcommands: Collection<string, SlashCommand>
|
||||
commandsCatagories: Collection<string, string[]>
|
||||
queues: Collection<Snowflake, MusicQueue>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
import { SlashCommandBuilder } from 'discord.js'
|
||||
import { bot } from '../../index'
|
||||
import { i18n } from '../../utils/i18n'
|
||||
import { canModifyQueue } from '../../utils/queue'
|
||||
import { SlashCommand } from '../../sturctures/command'
|
||||
|
||||
export const command: SlashCommand = {
|
||||
slashData: new SlashCommandBuilder()
|
||||
.setName('loop')
|
||||
.setDescription(i18n.__('loop.description')),
|
||||
run: (itd) => {
|
||||
const { interaction }: any = itd
|
||||
const queue = bot.queues.get(interaction.guild!.id)
|
||||
|
||||
const guildMemer = interaction.guild!.members.cache.get(
|
||||
interaction.user.id
|
||||
)
|
||||
|
||||
if (!queue)
|
||||
return interaction
|
||||
.reply({
|
||||
content: i18n.__('loop.errorNotQueue'),
|
||||
ephemeral: true,
|
||||
})
|
||||
.catch(console.error)
|
||||
|
||||
if (!guildMemer || !canModifyQueue(guildMemer))
|
||||
return i18n.__('common.errorNotChannel')
|
||||
|
||||
queue.loop = !queue.loop
|
||||
|
||||
const content = {
|
||||
content: i18n.__mf('loop.result', {
|
||||
loop: queue.loop ? i18n.__('common.on') : i18n.__('common.off'),
|
||||
}),
|
||||
}
|
||||
|
||||
if (interaction.replied)
|
||||
interaction.followUp(content).catch(console.error)
|
||||
else interaction.reply(content).catch(console.error)
|
||||
},
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
import { EmbedBuilder, SlashCommandBuilder } from 'discord.js'
|
||||
import { splitBar } from 'string-progressbar'
|
||||
import { bot } from '../../index'
|
||||
import { i18n } from '../../utils/i18n'
|
||||
import { SlashCommand } from '../../sturctures/command'
|
||||
|
||||
export const command: SlashCommand = {
|
||||
slashData: new SlashCommandBuilder()
|
||||
.setName('nowplaying')
|
||||
.setDescription(i18n.__('nowplaying.description')),
|
||||
run: (itd) => {
|
||||
const { interaction }: any = itd
|
||||
const queue = bot.queues.get(interaction.guild!.id)
|
||||
|
||||
console.log(i18n.__('nowplaying.description'))
|
||||
if (!queue || !queue.songs.length)
|
||||
return interaction
|
||||
.reply({
|
||||
content: i18n.__('nowplaying.errorNotQueue'),
|
||||
ephemeral: true,
|
||||
})
|
||||
.catch(console.error)
|
||||
|
||||
const song = queue.songs[0]
|
||||
const seek = queue.resource.playbackDuration / 1000
|
||||
const left = song.duration - seek
|
||||
|
||||
const nowPlaying = new EmbedBuilder()
|
||||
.setTitle(i18n.__('nowplaying.embedTitle'))
|
||||
.setDescription(`${song.title}\n${song.url}`)
|
||||
.setColor('#F8AA2A')
|
||||
|
||||
if (song.duration > 0) {
|
||||
nowPlaying.addFields({
|
||||
name: '\u200b',
|
||||
value:
|
||||
new Date(seek * 1000).toISOString().substr(11, 8) +
|
||||
'[' +
|
||||
splitBar(
|
||||
song.duration == 0 ? seek : song.duration,
|
||||
seek,
|
||||
20
|
||||
)[0] +
|
||||
']' +
|
||||
(song.duration == 0
|
||||
? ' ◉ LIVE'
|
||||
: new Date(song.duration * 1000)
|
||||
.toISOString()
|
||||
.substr(11, 8)),
|
||||
inline: false,
|
||||
})
|
||||
|
||||
nowPlaying.setFooter({
|
||||
text: i18n.__mf('nowplaying.timeRemaining', {
|
||||
time: new Date(left * 1000).toISOString().substr(11, 8),
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
return interaction.reply({ embeds: [nowPlaying] })
|
||||
},
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
import { SlashCommandBuilder } from 'discord.js'
|
||||
import { bot } from '../../index'
|
||||
import { i18n } from '../../utils/i18n'
|
||||
import { canModifyQueue } from '../../utils/queue'
|
||||
import { SlashCommand } from '../../sturctures/command'
|
||||
|
||||
export const command: SlashCommand = {
|
||||
slashData: new SlashCommandBuilder()
|
||||
.setName('pause')
|
||||
.setDescription(i18n.__('pause.description')),
|
||||
run: (itd) => {
|
||||
const { interaction }: any = itd
|
||||
const guildMemer = interaction.guild!.members.cache.get(
|
||||
interaction.user.id
|
||||
)
|
||||
const queue = bot.queues.get(interaction.guild!.id)
|
||||
|
||||
if (!queue)
|
||||
return interaction
|
||||
.reply({ content: i18n.__('pause.errorNotQueue') })
|
||||
.catch(console.error)
|
||||
|
||||
if (!canModifyQueue(guildMemer!))
|
||||
return i18n.__('common.errorNotChannel')
|
||||
|
||||
if (queue.player.pause()) {
|
||||
const content = {
|
||||
content: i18n.__mf('pause.result', {
|
||||
author: interaction.user.id,
|
||||
}),
|
||||
}
|
||||
|
||||
if (interaction.replied)
|
||||
interaction.followUp(content).catch(console.error)
|
||||
else interaction.reply(content).catch(console.error)
|
||||
|
||||
return true
|
||||
}
|
||||
},
|
||||
}
|
|
@ -0,0 +1,145 @@
|
|||
import {
|
||||
DiscordGatewayAdapterCreator,
|
||||
joinVoiceChannel,
|
||||
} from '@discordjs/voice'
|
||||
import { SlashCommandBuilder, TextChannel } from 'discord.js'
|
||||
import { bot } from '../../index'
|
||||
import { MusicQueue } from '../../utils/musicQueue'
|
||||
import { Song } from '../../utils/song'
|
||||
import { i18n } from '../../utils/i18n'
|
||||
import { playlistPattern } from '../../utils/patterns'
|
||||
import { SlashCommand } from '../../sturctures/command'
|
||||
|
||||
export const command: SlashCommand = {
|
||||
slashData: new SlashCommandBuilder()
|
||||
.setName('play')
|
||||
.setDescription(i18n.__('play.description'))
|
||||
.addStringOption((option) =>
|
||||
option
|
||||
.setName('song')
|
||||
.setDescription('The song you want to play')
|
||||
.setRequired(true)
|
||||
),
|
||||
run: async (itd) => {
|
||||
const { interaction, input }: any = itd
|
||||
let argSongName = interaction.options.getString('song')
|
||||
if (!argSongName) argSongName = input
|
||||
|
||||
const guildMember = interaction.guild!.members.cache.get(
|
||||
interaction.user.id
|
||||
)
|
||||
const { channel } = guildMember!.voice
|
||||
|
||||
if (!channel)
|
||||
return interaction
|
||||
.reply({
|
||||
content: i18n.__('play.errorNotChannel'),
|
||||
ephemeral: true,
|
||||
})
|
||||
.catch(console.error)
|
||||
|
||||
const queue = bot.queues.get(interaction.guild!.id)
|
||||
|
||||
if (queue && channel.id !== queue.connection.joinConfig.channelId)
|
||||
return interaction
|
||||
.reply({
|
||||
content: i18n.__mf('play.errorNotInSameChannel', {
|
||||
user: bot.client.user!.username,
|
||||
}),
|
||||
ephemeral: true,
|
||||
})
|
||||
.catch(console.error)
|
||||
|
||||
if (!argSongName)
|
||||
return interaction
|
||||
.reply({
|
||||
content: i18n.__mf('play.usageReply', {
|
||||
prefix: bot.prefix,
|
||||
}),
|
||||
ephemeral: true,
|
||||
})
|
||||
.catch(console.error)
|
||||
|
||||
const url = argSongName
|
||||
|
||||
if (interaction.replied)
|
||||
await interaction.editReply('⏳ Loading...').catch(console.error)
|
||||
else await interaction.reply('⏳ Loading...')
|
||||
|
||||
// Start the playlist if playlist url was provided
|
||||
if (playlistPattern.test(url)) {
|
||||
await interaction
|
||||
.editReply('🔗 Link is playlist')
|
||||
.catch(console.error)
|
||||
|
||||
return bot.slashCommandsMap.get('playlist')!.execute(interaction)
|
||||
}
|
||||
|
||||
let song
|
||||
|
||||
try {
|
||||
song = await Song.from(url, url)
|
||||
} catch (error: any) {
|
||||
if (error.name == 'NoResults')
|
||||
return interaction
|
||||
.reply({
|
||||
content: i18n.__mf('play.errorNoResults', {
|
||||
url: `<${url}>`,
|
||||
}),
|
||||
ephemeral: true,
|
||||
})
|
||||
.catch(console.error)
|
||||
if (error.name == 'InvalidURL')
|
||||
return interaction
|
||||
.reply({
|
||||
content: i18n.__mf('play.errorInvalidURL', {
|
||||
url: `<${url}>`,
|
||||
}),
|
||||
ephemeral: true,
|
||||
})
|
||||
.catch(console.error)
|
||||
|
||||
console.error(error)
|
||||
if (interaction.replied)
|
||||
return await interaction
|
||||
.editReply({ content: i18n.__('common.errorCommand') })
|
||||
.catch(console.error)
|
||||
else
|
||||
return interaction
|
||||
.reply({
|
||||
content: i18n.__('common.errorCommand'),
|
||||
ephemeral: true,
|
||||
})
|
||||
.catch(console.error)
|
||||
}
|
||||
|
||||
if (queue) {
|
||||
queue.enqueue(song)
|
||||
|
||||
return (interaction.channel as TextChannel)
|
||||
.send({
|
||||
content: i18n.__mf('play.queueAdded', {
|
||||
title: song.title,
|
||||
author: interaction.user.id,
|
||||
}),
|
||||
})
|
||||
.catch(console.error)
|
||||
}
|
||||
|
||||
const newQueue = new MusicQueue({
|
||||
interaction,
|
||||
textChannel: interaction.channel! as TextChannel,
|
||||
connection: joinVoiceChannel({
|
||||
channelId: channel.id,
|
||||
guildId: channel.guild.id,
|
||||
adapterCreator: channel.guild
|
||||
.voiceAdapterCreator as DiscordGatewayAdapterCreator,
|
||||
}),
|
||||
})
|
||||
|
||||
bot.queues.set(interaction.guild!.id, newQueue)
|
||||
|
||||
newQueue.enqueue(song)
|
||||
interaction.deleteReply().catch(console.error)
|
||||
},
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
import {
|
||||
DiscordGatewayAdapterCreator,
|
||||
joinVoiceChannel,
|
||||
} from '@discordjs/voice'
|
||||
import { EmbedBuilder, SlashCommandBuilder, TextChannel } from 'discord.js'
|
||||
import { bot } from '../../index'
|
||||
import { MusicQueue } from '../../utils/musicQueue'
|
||||
import { Playlist } from '../../utils/playlist'
|
||||
import { Song } from '../../utils/song'
|
||||
import { i18n } from '../../utils/i18n'
|
||||
import { SlashCommand } from '../../sturctures/command'
|
||||
|
||||
export const command: SlashCommand = {
|
||||
slashData: new SlashCommandBuilder()
|
||||
.setName('playlist')
|
||||
.setDescription(i18n.__('playlist.description'))
|
||||
.addStringOption((option) =>
|
||||
option
|
||||
.setName('playlist')
|
||||
.setDescription('Playlist name or link')
|
||||
.setRequired(true)
|
||||
),
|
||||
run: async (itd) => {
|
||||
const { interaction }: any = itd
|
||||
const argSongName = interaction.options.getString('playlist')
|
||||
|
||||
const guildMemer = interaction.guild!.members.cache.get(
|
||||
interaction.user.id
|
||||
)
|
||||
const { channel } = guildMemer!.voice
|
||||
|
||||
const queue = bot.queues.get(interaction.guild!.id)
|
||||
|
||||
if (!channel)
|
||||
return interaction
|
||||
.reply({
|
||||
content: i18n.__('playlist.errorNotChannel'),
|
||||
ephemeral: true,
|
||||
})
|
||||
.catch(console.error)
|
||||
|
||||
if (queue && channel.id !== queue.connection.joinConfig.channelId)
|
||||
if (interaction.replied)
|
||||
return interaction
|
||||
.editReply({
|
||||
content: i18n.__mf('play.errorNotInSameChannel', {
|
||||
user: interaction.client.user!.username,
|
||||
}),
|
||||
})
|
||||
.catch(console.error)
|
||||
else
|
||||
return interaction
|
||||
.reply({
|
||||
content: i18n.__mf('play.errorNotInSameChannel', {
|
||||
user: interaction.client.user!.username,
|
||||
}),
|
||||
ephemeral: true,
|
||||
})
|
||||
.catch(console.error)
|
||||
|
||||
let playlist
|
||||
|
||||
try {
|
||||
playlist = await Playlist.from(
|
||||
argSongName!.split(' ')[0],
|
||||
argSongName!
|
||||
)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
|
||||
if (interaction.replied)
|
||||
return interaction
|
||||
.editReply({
|
||||
content: i18n.__('playlist.errorNotFoundPlaylist'),
|
||||
})
|
||||
.catch(console.error)
|
||||
else
|
||||
return interaction
|
||||
.reply({
|
||||
content: i18n.__('playlist.errorNotFoundPlaylist'),
|
||||
ephemeral: true,
|
||||
})
|
||||
.catch(console.error)
|
||||
}
|
||||
|
||||
if (queue) {
|
||||
queue.songs.push(...playlist.videos)
|
||||
} else {
|
||||
const newQueue = new MusicQueue({
|
||||
interaction,
|
||||
textChannel: interaction.channel! as TextChannel,
|
||||
connection: joinVoiceChannel({
|
||||
channelId: channel.id,
|
||||
guildId: channel.guild.id,
|
||||
adapterCreator: channel.guild
|
||||
.voiceAdapterCreator as DiscordGatewayAdapterCreator,
|
||||
}),
|
||||
})
|
||||
|
||||
bot.queues.set(interaction.guild!.id, newQueue)
|
||||
newQueue.songs.push(...playlist.videos)
|
||||
|
||||
newQueue.enqueue(playlist.videos[0])
|
||||
}
|
||||
|
||||
const playlistEmbed = new EmbedBuilder()
|
||||
.setTitle(`${playlist.data.title}`)
|
||||
.setDescription(
|
||||
playlist.videos
|
||||
.map(
|
||||
(song: Song, index: number) =>
|
||||
`${index + 1}. ${song.title}`
|
||||
)
|
||||
.join('\n')
|
||||
)
|
||||
.setURL(playlist.data.url!)
|
||||
.setColor('#F8AA2A')
|
||||
.setTimestamp()
|
||||
|
||||
if (playlistEmbed.data.description!.length >= 2048)
|
||||
playlistEmbed.setDescription(
|
||||
playlistEmbed.data.description!.substr(0, 2007) +
|
||||
i18n.__('playlist.playlistCharLimit')
|
||||
)
|
||||
|
||||
if (interaction.replied)
|
||||
return interaction.editReply({
|
||||
content: i18n.__mf('playlist.startedPlaylist', {
|
||||
author: interaction.user.id,
|
||||
}),
|
||||
embeds: [playlistEmbed],
|
||||
})
|
||||
interaction
|
||||
.reply({
|
||||
content: i18n.__mf('playlist.startedPlaylist', {
|
||||
author: interaction.user.id,
|
||||
}),
|
||||
embeds: [playlistEmbed],
|
||||
})
|
||||
.catch(console.error)
|
||||
},
|
||||
}
|
|
@ -0,0 +1,130 @@
|
|||
import {
|
||||
CommandInteraction,
|
||||
EmbedBuilder,
|
||||
MessageReaction,
|
||||
SlashCommandBuilder,
|
||||
TextChannel,
|
||||
User,
|
||||
} from 'discord.js'
|
||||
import { bot } from '../../index'
|
||||
import { Song } from '../../utils/song'
|
||||
import { i18n } from '../../utils/i18n'
|
||||
import { SlashCommand } from '../../sturctures/command'
|
||||
|
||||
export const command: SlashCommand = {
|
||||
slashData: new SlashCommandBuilder()
|
||||
.setName('queue')
|
||||
.setDescription(i18n.__('queue.description')),
|
||||
run: async (itd) => {
|
||||
const { interaction }: any = itd
|
||||
const queue = bot.queues.get(interaction.guild!.id)
|
||||
if (!queue || !queue.songs.length)
|
||||
return interaction.reply({
|
||||
content: i18n.__('queue.errorNotQueue'),
|
||||
})
|
||||
|
||||
let currentPage = 0
|
||||
const embeds = generateQueueEmbed(interaction, queue.songs)
|
||||
|
||||
await interaction.reply('⏳ Loading queue...')
|
||||
|
||||
if (interaction.replied)
|
||||
await interaction.editReply({
|
||||
content: `**${i18n.__mf('queue.currentPage')} ${
|
||||
currentPage + 1
|
||||
}/${embeds.length}**`,
|
||||
embeds: [embeds[currentPage]],
|
||||
})
|
||||
|
||||
const queueEmbed = await interaction.fetchReply()
|
||||
|
||||
try {
|
||||
await queueEmbed.react('⬅️')
|
||||
await queueEmbed.react('⏹')
|
||||
await queueEmbed.react('➡️')
|
||||
} catch (error: any) {
|
||||
console.error(error)
|
||||
;(interaction.channel as TextChannel)
|
||||
.send(error.message)
|
||||
.catch(console.error)
|
||||
}
|
||||
|
||||
const filter = (reaction: MessageReaction, user: User) =>
|
||||
['⬅️', '⏹', '➡️'].includes(reaction.emoji.name!) &&
|
||||
interaction.user.id === user.id
|
||||
|
||||
const collector = queueEmbed.createReactionCollector({
|
||||
filter,
|
||||
time: 60000,
|
||||
})
|
||||
|
||||
collector.on('collect', async (reaction: any, _user: any) => {
|
||||
try {
|
||||
if (reaction.emoji.name === '➡️') {
|
||||
if (currentPage < embeds.length - 1) {
|
||||
currentPage++
|
||||
queueEmbed.edit({
|
||||
content: i18n.__mf('queue.currentPage', {
|
||||
page: currentPage + 1,
|
||||
length: embeds.length,
|
||||
}),
|
||||
embeds: [embeds[currentPage]],
|
||||
})
|
||||
}
|
||||
} else if (reaction.emoji.name === '⬅️') {
|
||||
if (currentPage !== 0) {
|
||||
--currentPage
|
||||
queueEmbed.edit({
|
||||
content: i18n.__mf('queue.currentPage', {
|
||||
page: currentPage + 1,
|
||||
length: embeds.length,
|
||||
}),
|
||||
embeds: [embeds[currentPage]],
|
||||
})
|
||||
}
|
||||
} else {
|
||||
collector.stop()
|
||||
reaction.message.reactions.removeAll()
|
||||
}
|
||||
await reaction.users.remove(interaction.user.id)
|
||||
} catch (error: any) {
|
||||
console.error(error)
|
||||
return (interaction.channel as TextChannel)
|
||||
.send(error.message)
|
||||
.catch(console.error)
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
function generateQueueEmbed(interaction: CommandInteraction, songs: Song[]) {
|
||||
const embeds = []
|
||||
let k = 10
|
||||
|
||||
for (let i = 0; i < songs.length; i += 10) {
|
||||
const current = songs.slice(i, k)
|
||||
let j = i
|
||||
k += 10
|
||||
|
||||
const info = current
|
||||
.map((track) => `${++j} - [${track.title}](${track.url})`)
|
||||
.join('\n')
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle(i18n.__('queue.embedTitle'))
|
||||
// eslint-disable-next-line
|
||||
.setThumbnail(interaction.guild?.iconURL()!)
|
||||
.setColor('#F8AA2A')
|
||||
.setDescription(
|
||||
i18n.__mf('queue.embedCurrentSong', {
|
||||
title: songs[0].title,
|
||||
url: songs[0].url,
|
||||
info: info,
|
||||
})
|
||||
)
|
||||
.setTimestamp()
|
||||
embeds.push(embed)
|
||||
}
|
||||
|
||||
return embeds
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
import { SlashCommandBuilder } from 'discord.js'
|
||||
import { bot } from '../../index'
|
||||
import { Song } from '../../utils/song'
|
||||
import { i18n } from '../../utils/i18n'
|
||||
import { canModifyQueue } from '../../utils/queue'
|
||||
|
||||
const pattern = /^[0-9]{1,2}(\s*,\s*[0-9]{1,2})*$/
|
||||
|
||||
import { SlashCommand } from '../../sturctures/command'
|
||||
|
||||
export const command: SlashCommand = {
|
||||
slashData: new SlashCommandBuilder()
|
||||
.setName('remove')
|
||||
.setDescription(i18n.__('remove.description'))
|
||||
.addStringOption((option) =>
|
||||
option
|
||||
.setName('slot')
|
||||
.setDescription(i18n.__('remove.description'))
|
||||
.setRequired(true)
|
||||
),
|
||||
run: (itd) => {
|
||||
const { interaction }: any = itd
|
||||
const guildMemer = interaction.guild!.members.cache.get(
|
||||
interaction.user.id
|
||||
)
|
||||
const removeArgs = interaction.options.getString('slot')
|
||||
|
||||
const queue = bot.queues.get(interaction.guild!.id)
|
||||
|
||||
if (!queue)
|
||||
return interaction
|
||||
.reply({
|
||||
content: i18n.__('remove.errorNotQueue'),
|
||||
ephemeral: true,
|
||||
})
|
||||
.catch(console.error)
|
||||
|
||||
if (!canModifyQueue(guildMemer!))
|
||||
return i18n.__('common.errorNotChannel')
|
||||
|
||||
if (!removeArgs)
|
||||
return interaction.reply({
|
||||
content: i18n.__mf('remove.usageReply', { prefix: bot.prefix }),
|
||||
ephemeral: true,
|
||||
})
|
||||
|
||||
const songs = removeArgs.split(',').map((arg: any) => parseInt(arg))
|
||||
|
||||
const removed: Song[] = []
|
||||
|
||||
if (pattern.test(removeArgs)) {
|
||||
// eslint-disable-next-line
|
||||
queue.songs = queue.songs.filter((item, index) => {
|
||||
if (songs.find((songIndex: any) => songIndex - 1 === index))
|
||||
removed.push(item)
|
||||
else return true
|
||||
})
|
||||
|
||||
interaction.reply(
|
||||
i18n.__mf('remove.result', {
|
||||
title: removed.map((song) => song.title).join('\n'),
|
||||
author: interaction.user.id,
|
||||
})
|
||||
)
|
||||
} else if (
|
||||
!isNaN(+removeArgs) &&
|
||||
+removeArgs >= 1 &&
|
||||
+removeArgs <= queue.songs.length
|
||||
) {
|
||||
return interaction.reply(
|
||||
i18n.__mf('remove.result', {
|
||||
title: queue.songs.splice(+removeArgs - 1, 1)[0].title,
|
||||
author: interaction.user.id,
|
||||
})
|
||||
)
|
||||
} else {
|
||||
return interaction.reply({
|
||||
content: i18n.__mf('remove.usageReply', { prefix: bot.prefix }),
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
import { SlashCommandBuilder } from 'discord.js'
|
||||
import { bot } from '../../index'
|
||||
import { i18n } from '../../utils/i18n'
|
||||
import { canModifyQueue } from '../../utils/queue'
|
||||
import { SlashCommand } from '../../sturctures/command'
|
||||
|
||||
export const command: SlashCommand = {
|
||||
slashData: new SlashCommandBuilder()
|
||||
.setName('resume')
|
||||
.setDescription(i18n.__('resume.description')),
|
||||
run: (itd) => {
|
||||
const { interaction }: any = itd
|
||||
const queue = bot.queues.get(interaction.guild!.id)
|
||||
const guildMemer = interaction.guild!.members.cache.get(
|
||||
interaction.user.id
|
||||
)
|
||||
|
||||
if (!queue)
|
||||
return interaction
|
||||
.reply({
|
||||
content: i18n.__('resume.errorNotQueue'),
|
||||
ephemeral: true,
|
||||
})
|
||||
.catch(console.error)
|
||||
|
||||
if (!canModifyQueue(guildMemer!))
|
||||
return i18n.__('common.errorNotChannel')
|
||||
|
||||
if (queue.player.unpause()) {
|
||||
const content = {
|
||||
content: i18n.__mf('resume.resultNotPlaying', {
|
||||
author: interaction.user.id,
|
||||
}),
|
||||
}
|
||||
|
||||
if (interaction.replied)
|
||||
interaction.followUp(content).catch(console.error)
|
||||
else interaction.reply(content).catch(console.error)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const content = { content: i18n.__('resume.errorPlaying') }
|
||||
|
||||
if (interaction.replied)
|
||||
interaction.followUp(content).catch(console.error)
|
||||
else interaction.reply(content).catch(console.error)
|
||||
return false
|
||||
},
|
||||
}
|
|
@ -0,0 +1,114 @@
|
|||
import {
|
||||
ActionRowBuilder,
|
||||
SlashCommandBuilder,
|
||||
StringSelectMenuBuilder,
|
||||
StringSelectMenuInteraction,
|
||||
} from 'discord.js'
|
||||
import youtube, { Video } from 'youtube-sr'
|
||||
import { bot } from '../../index'
|
||||
import { i18n } from '../../utils/i18n'
|
||||
import { SlashCommand } from '../../sturctures/command'
|
||||
|
||||
export const command: SlashCommand = {
|
||||
slashData: new SlashCommandBuilder()
|
||||
.setName('search')
|
||||
.setDescription(i18n.__('search.description'))
|
||||
.addStringOption((option) =>
|
||||
option
|
||||
.setName('query')
|
||||
.setDescription(i18n.__('search.optionQuery'))
|
||||
.setRequired(true)
|
||||
),
|
||||
run: async (itd) => {
|
||||
const { interaction }: any = itd
|
||||
const query = interaction.options.getString('query', true)
|
||||
const member = interaction.guild!.members.cache.get(interaction.user.id)
|
||||
|
||||
if (!member?.voice.channel)
|
||||
return interaction
|
||||
.reply({
|
||||
content: i18n.__('search.errorNotChannel'),
|
||||
ephemeral: true,
|
||||
})
|
||||
.catch(console.error)
|
||||
|
||||
const search = query
|
||||
|
||||
await interaction.reply('⏳ Loading...').catch(console.error)
|
||||
|
||||
let results: Video[] = []
|
||||
|
||||
try {
|
||||
results = await youtube.search(search, { limit: 10, type: 'video' })
|
||||
} catch (error: any) {
|
||||
console.error(error)
|
||||
|
||||
interaction
|
||||
.editReply({ content: i18n.__('common.errorCommand') })
|
||||
.catch(console.error)
|
||||
}
|
||||
|
||||
if (!results) return
|
||||
|
||||
const options = results!.map((video) => {
|
||||
return {
|
||||
label: video.title ?? '',
|
||||
value: video.url,
|
||||
}
|
||||
})
|
||||
|
||||
const row =
|
||||
new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(
|
||||
new StringSelectMenuBuilder()
|
||||
.setCustomId('search-select')
|
||||
.setPlaceholder('Nothing selected')
|
||||
.setMinValues(1)
|
||||
.setMaxValues(10)
|
||||
.addOptions(options)
|
||||
)
|
||||
|
||||
const followUp = await interaction.followUp({
|
||||
content: 'Choose songs to play',
|
||||
components: [row],
|
||||
})
|
||||
|
||||
followUp
|
||||
.awaitMessageComponent({
|
||||
time: 30000,
|
||||
})
|
||||
.then(
|
||||
(selectInteraction: {
|
||||
update: (arg0: {
|
||||
content: string
|
||||
components: never[]
|
||||
}) => void
|
||||
values: any[]
|
||||
}) => {
|
||||
if (
|
||||
!(
|
||||
selectInteraction instanceof
|
||||
StringSelectMenuInteraction
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
selectInteraction.update({
|
||||
content: '⏳ Loading the selected songs...',
|
||||
components: [],
|
||||
})
|
||||
|
||||
bot.slashCommandsMap
|
||||
.get('play')!
|
||||
.execute(interaction, selectInteraction.values[0])
|
||||
.then(() => {
|
||||
selectInteraction.values.slice(1).forEach((url) => {
|
||||
bot.slashCommandsMap
|
||||
.get('play')!
|
||||
.execute(interaction, url)
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
||||
.catch(console.error)
|
||||
},
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
import { SlashCommandBuilder } from 'discord.js'
|
||||
import { bot } from '../../index'
|
||||
import { i18n } from '../../utils/i18n'
|
||||
import { canModifyQueue } from '../../utils/queue'
|
||||
import { SlashCommand } from '../../sturctures/command'
|
||||
|
||||
export const command: SlashCommand = {
|
||||
slashData: new SlashCommandBuilder()
|
||||
.setName('shuffle')
|
||||
.setDescription(i18n.__('shuffle.description')),
|
||||
run: (itd) => {
|
||||
const { interaction }: any = itd
|
||||
const queue = bot.queues.get(interaction.guild!.id)
|
||||
const guildMemer = interaction.guild!.members.cache.get(
|
||||
interaction.user.id
|
||||
)
|
||||
|
||||
if (!queue)
|
||||
return interaction
|
||||
.reply({
|
||||
content: i18n.__('shuffle.errorNotQueue'),
|
||||
ephemeral: true,
|
||||
})
|
||||
.catch(console.error)
|
||||
|
||||
if (!guildMemer || !canModifyQueue(guildMemer))
|
||||
return i18n.__('common.errorNotChannel')
|
||||
|
||||
const songs = queue.songs
|
||||
|
||||
for (let i = songs.length - 1; i > 1; i--) {
|
||||
const j = 1 + Math.floor(Math.random() * i)
|
||||
;[songs[i], songs[j]] = [songs[j], songs[i]]
|
||||
}
|
||||
|
||||
queue.songs = songs
|
||||
|
||||
const content = {
|
||||
content: i18n.__mf('shuffle.result', {
|
||||
author: interaction.user.id,
|
||||
}),
|
||||
}
|
||||
|
||||
if (interaction.replied)
|
||||
interaction.followUp(content).catch(console.error)
|
||||
else interaction.reply(content).catch(console.error)
|
||||
},
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
import { SlashCommandBuilder } from 'discord.js'
|
||||
import { bot } from '../../index'
|
||||
import { i18n } from '../../utils/i18n'
|
||||
import { canModifyQueue } from '../../utils/queue'
|
||||
import { SlashCommand } from '../../sturctures/command'
|
||||
|
||||
export const command: SlashCommand = {
|
||||
slashData: new SlashCommandBuilder()
|
||||
.setName('skip')
|
||||
.setDescription(i18n.__('skip.description')),
|
||||
run: (itd) => {
|
||||
const { interaction }: any = itd
|
||||
const queue = bot.queues.get(interaction.guild!.id)
|
||||
const guildMemer = interaction.guild!.members.cache.get(
|
||||
interaction.user.id
|
||||
)
|
||||
|
||||
if (!queue)
|
||||
return interaction
|
||||
.reply(i18n.__('skip.errorNotQueue'))
|
||||
.catch(console.error)
|
||||
|
||||
if (!canModifyQueue(guildMemer!))
|
||||
return i18n.__('common.errorNotChannel')
|
||||
|
||||
queue.player.stop(true)
|
||||
|
||||
interaction
|
||||
.reply({
|
||||
content: i18n.__mf('skip.result', {
|
||||
author: interaction.user.id,
|
||||
}),
|
||||
})
|
||||
.catch(console.error)
|
||||
},
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
import { SlashCommandBuilder } from 'discord.js'
|
||||
import { bot } from '../../index'
|
||||
import { i18n } from '../../utils/i18n'
|
||||
import { canModifyQueue } from '../../utils/queue'
|
||||
import { SlashCommand } from '../../sturctures/command'
|
||||
|
||||
export const command: SlashCommand = {
|
||||
slashData: new SlashCommandBuilder()
|
||||
.setName('skipto')
|
||||
.setDescription(i18n.__('skipto.description'))
|
||||
.addIntegerOption((option) =>
|
||||
option
|
||||
.setName('number')
|
||||
.setDescription(i18n.__('skipto.args.number'))
|
||||
.setRequired(true)
|
||||
),
|
||||
run: (itd) => {
|
||||
const { interaction }: any = itd
|
||||
const playlistSlotArg = interaction.options.getInteger('number')
|
||||
const guildMemer = interaction.guild!.members.cache.get(
|
||||
interaction.user.id
|
||||
)
|
||||
|
||||
if (!playlistSlotArg || isNaN(playlistSlotArg))
|
||||
return interaction
|
||||
.reply({
|
||||
content: i18n.__mf('skipto.usageReply', {
|
||||
prefix: bot.prefix,
|
||||
name: module.exports.name,
|
||||
}),
|
||||
ephemeral: true,
|
||||
})
|
||||
.catch(console.error)
|
||||
|
||||
const queue = bot.queues.get(interaction.guild!.id)
|
||||
|
||||
if (!queue)
|
||||
return interaction
|
||||
.reply({
|
||||
content: i18n.__('skipto.errorNotQueue'),
|
||||
ephemeral: true,
|
||||
})
|
||||
.catch(console.error)
|
||||
|
||||
if (!canModifyQueue(guildMemer!))
|
||||
return i18n.__('common.errorNotChannel')
|
||||
|
||||
if (playlistSlotArg > queue.songs.length)
|
||||
return interaction
|
||||
.reply({
|
||||
content: i18n.__mf('skipto.errorNotValid', {
|
||||
length: queue.songs.length,
|
||||
}),
|
||||
ephemeral: true,
|
||||
})
|
||||
.catch(console.error)
|
||||
|
||||
if (queue.loop) {
|
||||
for (let i = 0; i < playlistSlotArg - 2; i++) {
|
||||
queue.songs.push(queue.songs.shift()!)
|
||||
}
|
||||
} else {
|
||||
queue.songs = queue.songs.slice(playlistSlotArg - 2)
|
||||
}
|
||||
|
||||
queue.player.stop()
|
||||
|
||||
interaction
|
||||
.reply({
|
||||
content: i18n.__mf('skipto.result', {
|
||||
author: interaction.user.id,
|
||||
arg: playlistSlotArg - 1,
|
||||
}),
|
||||
})
|
||||
.catch(console.error)
|
||||
},
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
import { SlashCommandBuilder } from 'discord.js'
|
||||
import { bot } from '../../index'
|
||||
import { i18n } from '../../utils/i18n'
|
||||
import { canModifyQueue } from '../../utils/queue'
|
||||
import { SlashCommand } from '../../sturctures/command'
|
||||
|
||||
export const command: SlashCommand = {
|
||||
slashData: new SlashCommandBuilder()
|
||||
.setName('stop')
|
||||
.setDescription(i18n.__('stop.description')),
|
||||
run: (itd) => {
|
||||
const { interaction }: any = itd
|
||||
const queue = bot.queues.get(interaction.guild!.id)
|
||||
const guildMemer = interaction.guild!.members.cache.get(
|
||||
interaction.user.id
|
||||
)
|
||||
|
||||
if (!queue)
|
||||
return interaction
|
||||
.reply(i18n.__('stop.errorNotQueue'))
|
||||
.catch(console.error)
|
||||
if (!guildMemer || !canModifyQueue(guildMemer))
|
||||
return i18n.__('common.errorNotChannel')
|
||||
|
||||
queue.stop()
|
||||
|
||||
interaction
|
||||
.reply({
|
||||
content: i18n.__mf('stop.result', {
|
||||
author: interaction.user.id,
|
||||
}),
|
||||
})
|
||||
.catch(console.error)
|
||||
},
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
import { SlashCommandBuilder } from 'discord.js'
|
||||
import { bot } from '../../index'
|
||||
import { i18n } from '../../utils/i18n'
|
||||
import { canModifyQueue } from '../../utils/queue'
|
||||
import { SlashCommand } from '../../sturctures/command'
|
||||
|
||||
export const command: SlashCommand = {
|
||||
slashData: new SlashCommandBuilder()
|
||||
.setName('volume')
|
||||
.setDescription(i18n.__('volume.description'))
|
||||
.addIntegerOption((option) =>
|
||||
option
|
||||
.setName('volume')
|
||||
.setDescription(i18n.__('volume.description'))
|
||||
),
|
||||
run: async (intData) => {
|
||||
const { interaction }: any = intData
|
||||
const queue = bot.queues.get(interaction.guild!.id)
|
||||
const guildMemer = interaction.guild!.members.cache.get(
|
||||
interaction.user.id
|
||||
)
|
||||
const volumeArg = interaction.options.getInteger('volume')
|
||||
|
||||
if (!queue)
|
||||
return interaction
|
||||
.reply({
|
||||
content: i18n.__('volume.errorNotQueue'),
|
||||
ephemeral: true,
|
||||
})
|
||||
.catch(console.error)
|
||||
|
||||
if (!canModifyQueue(guildMemer!))
|
||||
return interaction
|
||||
.reply({
|
||||
content: i18n.__('volume.errorNotChannel'),
|
||||
ephemeral: true,
|
||||
})
|
||||
.catch(console.error)
|
||||
|
||||
if (!volumeArg || volumeArg === queue.volume)
|
||||
return interaction
|
||||
.reply({
|
||||
content: i18n.__mf('volume.currentVolume', {
|
||||
volume: queue.volume,
|
||||
}),
|
||||
})
|
||||
.catch(console.error)
|
||||
|
||||
if (isNaN(volumeArg))
|
||||
return interaction
|
||||
.reply({
|
||||
content: i18n.__('volume.errorNotNumber'),
|
||||
ephemeral: true,
|
||||
})
|
||||
.catch(console.error)
|
||||
|
||||
if (Number(volumeArg) > 100 || Number(volumeArg) < 0)
|
||||
return interaction
|
||||
.reply({
|
||||
content: i18n.__('volume.errorNotValid'),
|
||||
ephemeral: true,
|
||||
})
|
||||
.catch(console.error)
|
||||
|
||||
queue.volume = volumeArg
|
||||
queue.resource.volume?.setVolumeLogarithmic(volumeArg / 100)
|
||||
|
||||
return interaction
|
||||
.reply({ content: i18n.__mf('volume.result', { arg: volumeArg }) })
|
||||
.catch(console.error)
|
||||
},
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
import type { Client } from 'discord.js'
|
||||
import { ActivityType } from 'discord.js'
|
||||
|
||||
import { defaultPrefix } from '../../config/bot.json'
|
||||
import type { DiscordEvent } from '../sturctures/event'
|
||||
|
||||
export const event: DiscordEvent = {
|
||||
|
@ -9,18 +8,10 @@ export const event: DiscordEvent = {
|
|||
name: 'ready',
|
||||
run: (client: Client) => {
|
||||
// Dynamic Status
|
||||
let index = 0
|
||||
|
||||
const status = [`${client.guilds.cache.size} Servers`]
|
||||
|
||||
setInterval(() => {
|
||||
const _status = status[index++ % status.length]
|
||||
const text = `${defaultPrefix}help | ${_status}`
|
||||
client.user?.setActivity(text, {
|
||||
type: ActivityType.Watching,
|
||||
})
|
||||
}, 15 * 1000)
|
||||
|
||||
const text = `to my master's wishes 🧜🏻♀️`
|
||||
client.user?.setActivity(text, {
|
||||
type: ActivityType.Listening,
|
||||
})
|
||||
console.log('🤖 Bot is in ready status!')
|
||||
},
|
||||
}
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import 'dotenv/config'
|
||||
|
||||
import chalk from 'chalk'
|
||||
import { Client, GatewayIntentBits, Partials } from 'discord.js'
|
||||
|
||||
import './validate-env'
|
||||
|
||||
import { Bot } from './bot'
|
||||
import { isDev } from './utils/constants'
|
||||
|
||||
// Check NODE Version
|
||||
|
@ -31,4 +32,23 @@ if (isDev) {
|
|||
process.on('unhandledRejection', console.error)
|
||||
}
|
||||
|
||||
import('./bot')
|
||||
export const bot = new Bot(
|
||||
new Client({
|
||||
intents: [
|
||||
GatewayIntentBits.Guilds,
|
||||
GatewayIntentBits.GuildMembers,
|
||||
GatewayIntentBits.GuildMessages,
|
||||
GatewayIntentBits.DirectMessages,
|
||||
GatewayIntentBits.MessageContent,
|
||||
GatewayIntentBits.GuildVoiceStates,
|
||||
GatewayIntentBits.GuildMessageReactions,
|
||||
],
|
||||
allowedMentions: { parse: ['users', 'roles'], repliedUser: false },
|
||||
partials: [
|
||||
Partials.User,
|
||||
Partials.Channel,
|
||||
Partials.Message,
|
||||
Partials.GuildMember,
|
||||
],
|
||||
})
|
||||
)
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
import { SlashCommandBuilder } from 'discord.js'
|
||||
|
||||
export interface Command {
|
||||
permissions?: string[]
|
||||
cooldown?: number
|
||||
data: SlashCommandBuilder
|
||||
execute(...args: any): any
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
export interface Config {
|
||||
TOKEN: string
|
||||
PREFIX: string
|
||||
MAX_PLAYLIST_SIZE: number
|
||||
PRUNING: boolean
|
||||
STAY_TIME: number
|
||||
DEFAULT_VOLUME: number
|
||||
LOCALE: string
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import { VoiceConnection } from '@discordjs/voice'
|
||||
import { CommandInteraction, TextChannel } from 'discord.js'
|
||||
|
||||
export interface QueueOptions {
|
||||
interaction: CommandInteraction
|
||||
textChannel: TextChannel
|
||||
connection: VoiceConnection
|
||||
}
|
|
@ -1,10 +1,10 @@
|
|||
import fastify from 'fastify'
|
||||
import client from '../bot'
|
||||
import { bot } from '../index'
|
||||
|
||||
const server = fastify()
|
||||
|
||||
server.get('/', () => {
|
||||
return client.user?.username + ' is ready !!'
|
||||
return bot.client.user?.username + ' is ready !!'
|
||||
})
|
||||
|
||||
await server.listen({
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
import i18n from 'i18n'
|
||||
import path, { join } from 'path'
|
||||
import config from '../../config/music.json'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
|
||||
const __dirname = path.dirname(__filename)
|
||||
|
||||
i18n.configure({
|
||||
locales: ['en'],
|
||||
directory: join(__dirname, '../..', 'locales'),
|
||||
defaultLocale: 'en',
|
||||
retryInDefaultLocale: true,
|
||||
objectNotation: true,
|
||||
register: global,
|
||||
|
||||
logWarnFn: function (msg: any) {
|
||||
console.log(msg)
|
||||
},
|
||||
|
||||
logErrorFn: function (msg: any) {
|
||||
console.log(msg)
|
||||
},
|
||||
|
||||
missingKeyFn: function (_locale: any, value: any) {
|
||||
return value
|
||||
},
|
||||
|
||||
mustacheConfig: {
|
||||
tags: ['{{', '}}'],
|
||||
disable: false,
|
||||
},
|
||||
})
|
||||
|
||||
i18n.setLocale(config.LOCALE)
|
||||
|
||||
export { i18n }
|
|
@ -0,0 +1,387 @@
|
|||
import {
|
||||
AudioPlayer,
|
||||
AudioPlayerState,
|
||||
AudioPlayerStatus,
|
||||
AudioResource,
|
||||
createAudioPlayer,
|
||||
entersState,
|
||||
NoSubscriberBehavior,
|
||||
VoiceConnection,
|
||||
VoiceConnectionDisconnectReason,
|
||||
VoiceConnectionState,
|
||||
VoiceConnectionStatus,
|
||||
} from '@discordjs/voice'
|
||||
import { CommandInteraction, Message, TextChannel, User } from 'discord.js'
|
||||
import { promisify } from 'node:util'
|
||||
import { bot } from '../index'
|
||||
import { QueueOptions } from '../interfaces/QueueOptions'
|
||||
import config from '../../config/music.json'
|
||||
import { i18n } from '../utils/i18n'
|
||||
import { canModifyQueue } from './queue'
|
||||
import { Song } from './song'
|
||||
|
||||
const wait = promisify(setTimeout)
|
||||
|
||||
export class MusicQueue {
|
||||
public readonly interaction: CommandInteraction
|
||||
public readonly connection: VoiceConnection
|
||||
public readonly player: AudioPlayer
|
||||
public readonly textChannel: TextChannel
|
||||
public readonly bot = bot
|
||||
|
||||
public resource: AudioResource
|
||||
public songs: Song[] = []
|
||||
public volume = config.DEFAULT_VOLUME || 100
|
||||
public loop = false
|
||||
public muted = false
|
||||
public waitTimeout: NodeJS.Timeout | null
|
||||
private queueLock = false
|
||||
private readyLock = false
|
||||
private stopped = false
|
||||
|
||||
public constructor(options: QueueOptions) {
|
||||
Object.assign(this, options)
|
||||
|
||||
this.player = createAudioPlayer({
|
||||
behaviors: { noSubscriber: NoSubscriberBehavior.Play },
|
||||
})
|
||||
this.connection.subscribe(this.player)
|
||||
|
||||
const networkStateChangeHandler = (
|
||||
_oldNetworkState: any,
|
||||
newNetworkState: any
|
||||
) => {
|
||||
const newUdp = Reflect.get(newNetworkState, 'udp')
|
||||
clearInterval(newUdp?.keepAliveInterval)
|
||||
}
|
||||
|
||||
this.connection.on(
|
||||
'stateChange' as any,
|
||||
async (
|
||||
oldState: VoiceConnectionState,
|
||||
newState: VoiceConnectionState
|
||||
) => {
|
||||
Reflect.get(oldState, 'networking')?.off(
|
||||
'stateChange',
|
||||
networkStateChangeHandler
|
||||
)
|
||||
Reflect.get(newState, 'networking')?.on(
|
||||
'stateChange',
|
||||
networkStateChangeHandler
|
||||
)
|
||||
|
||||
if (newState.status === VoiceConnectionStatus.Disconnected) {
|
||||
if (
|
||||
newState.reason ===
|
||||
VoiceConnectionDisconnectReason.WebSocketClose &&
|
||||
newState.closeCode === 4014
|
||||
) {
|
||||
try {
|
||||
this.stop()
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
this.stop()
|
||||
}
|
||||
} else if (this.connection.rejoinAttempts < 5) {
|
||||
await wait((this.connection.rejoinAttempts + 1) * 5_000)
|
||||
this.connection.rejoin()
|
||||
} else {
|
||||
this.connection.destroy()
|
||||
}
|
||||
} else if (
|
||||
!this.readyLock &&
|
||||
(newState.status === VoiceConnectionStatus.Connecting ||
|
||||
newState.status === VoiceConnectionStatus.Signalling)
|
||||
) {
|
||||
this.readyLock = true
|
||||
try {
|
||||
await entersState(
|
||||
this.connection,
|
||||
VoiceConnectionStatus.Ready,
|
||||
20_000
|
||||
)
|
||||
} catch {
|
||||
if (
|
||||
this.connection.state.status !==
|
||||
VoiceConnectionStatus.Destroyed
|
||||
) {
|
||||
try {
|
||||
this.connection.destroy()
|
||||
} catch {
|
||||
console.log('Failed to destroy connection')
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
this.readyLock = false
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
this.player.on(
|
||||
'stateChange' as any,
|
||||
async (oldState: AudioPlayerState, newState: AudioPlayerState) => {
|
||||
if (
|
||||
oldState.status !== AudioPlayerStatus.Idle &&
|
||||
newState.status === AudioPlayerStatus.Idle
|
||||
) {
|
||||
if (this.loop && this.songs.length) {
|
||||
this.songs.push(this.songs.shift()!)
|
||||
} else {
|
||||
this.songs.shift()
|
||||
if (!this.songs.length) return this.stop()
|
||||
}
|
||||
|
||||
if (this.songs.length || this.resource.audioPlayer)
|
||||
this.processQueue()
|
||||
} else if (
|
||||
oldState.status === AudioPlayerStatus.Buffering &&
|
||||
newState.status === AudioPlayerStatus.Playing
|
||||
) {
|
||||
this.sendPlayingMessage(newState)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
this.player.on('error', (error) => {
|
||||
console.error(error)
|
||||
|
||||
if (this.loop && this.songs.length) {
|
||||
this.songs.push(this.songs.shift()!)
|
||||
} else {
|
||||
this.songs.shift()
|
||||
}
|
||||
|
||||
this.processQueue()
|
||||
})
|
||||
}
|
||||
|
||||
public enqueue(...songs: Song[]) {
|
||||
if (this.waitTimeout !== null) clearTimeout(this.waitTimeout)
|
||||
this.waitTimeout = null
|
||||
this.stopped = false
|
||||
this.songs = this.songs.concat(songs)
|
||||
this.processQueue()
|
||||
}
|
||||
|
||||
public stop() {
|
||||
if (this.stopped) return
|
||||
|
||||
this.stopped = true
|
||||
this.loop = false
|
||||
this.songs = []
|
||||
this.player.stop()
|
||||
|
||||
!config.PRUNING &&
|
||||
this.textChannel
|
||||
.send(i18n.__('play.queueEnded'))
|
||||
.catch(console.error)
|
||||
|
||||
if (this.waitTimeout !== null) return
|
||||
|
||||
this.waitTimeout = setTimeout(() => {
|
||||
if (
|
||||
this.connection.state.status !== VoiceConnectionStatus.Destroyed
|
||||
) {
|
||||
try {
|
||||
this.connection.destroy()
|
||||
} catch {
|
||||
console.log('Failed to destroy the connection.')
|
||||
}
|
||||
}
|
||||
bot.queues.delete(this.interaction.guild!.id)
|
||||
|
||||
!config.PRUNING &&
|
||||
this.textChannel.send(i18n.__('play.leaveChannel'))
|
||||
}, config.STAY_TIME * 1000)
|
||||
}
|
||||
|
||||
public async processQueue(): Promise<void> {
|
||||
if (
|
||||
this.queueLock ||
|
||||
this.player.state.status !== AudioPlayerStatus.Idle
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.songs.length) {
|
||||
return this.stop()
|
||||
}
|
||||
|
||||
this.queueLock = true
|
||||
|
||||
const next = this.songs[0]
|
||||
|
||||
try {
|
||||
const resource = await next.makeResource()
|
||||
|
||||
this.resource = resource!
|
||||
this.player.play(this.resource)
|
||||
this.resource.volume?.setVolumeLogarithmic(this.volume / 100)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
|
||||
return this.processQueue()
|
||||
} finally {
|
||||
this.queueLock = false
|
||||
}
|
||||
}
|
||||
|
||||
private async sendPlayingMessage(newState: any) {
|
||||
const song = (newState.resource as AudioResource<Song>).metadata
|
||||
|
||||
let playingMessage: Message
|
||||
|
||||
try {
|
||||
playingMessage = await this.textChannel.send(
|
||||
(
|
||||
newState.resource as AudioResource<Song>
|
||||
).metadata.startMessage()
|
||||
)
|
||||
|
||||
await playingMessage.react('⏭')
|
||||
await playingMessage.react('⏯')
|
||||
await playingMessage.react('🔇')
|
||||
await playingMessage.react('🔉')
|
||||
await playingMessage.react('🔊')
|
||||
await playingMessage.react('🔁')
|
||||
await playingMessage.react('🔀')
|
||||
await playingMessage.react('⏹')
|
||||
} catch (error: any) {
|
||||
console.error(error)
|
||||
this.textChannel.send(error.message)
|
||||
return
|
||||
}
|
||||
|
||||
const filter = (_reaction: any, user: User) =>
|
||||
user.id !== this.textChannel.client.user!.id
|
||||
|
||||
const collector = playingMessage.createReactionCollector({
|
||||
filter,
|
||||
time: song.duration > 0 ? song.duration * 1000 : 600000,
|
||||
})
|
||||
collector.on('collect', async (reaction, user) => {
|
||||
if (!this.songs) return
|
||||
|
||||
const member = await playingMessage.guild!.members.fetch(user)
|
||||
|
||||
switch (reaction.emoji.name) {
|
||||
case '⏭':
|
||||
reaction.users.remove(user).catch(console.error)
|
||||
await this.bot.slashCommandsMap
|
||||
.get('skip')!
|
||||
.execute(this.interaction)
|
||||
break
|
||||
|
||||
case '⏯':
|
||||
reaction.users.remove(user).catch(console.error)
|
||||
if (this.player.state.status == AudioPlayerStatus.Playing) {
|
||||
await this.bot.slashCommandsMap
|
||||
.get('pause')!
|
||||
.execute(this.interaction)
|
||||
} else {
|
||||
await this.bot.slashCommandsMap
|
||||
.get('resume')!
|
||||
.execute(this.interaction)
|
||||
}
|
||||
break
|
||||
|
||||
case '🔇':
|
||||
reaction.users.remove(user).catch(console.error)
|
||||
if (!canModifyQueue(member))
|
||||
return i18n.__('common.errorNotChannel')
|
||||
this.muted = !this.muted
|
||||
if (this.muted) {
|
||||
this.resource.volume?.setVolumeLogarithmic(0)
|
||||
this.textChannel
|
||||
.send(i18n.__mf('play.mutedSong', { author: user }))
|
||||
.catch(console.error)
|
||||
} else {
|
||||
this.resource.volume?.setVolumeLogarithmic(
|
||||
this.volume / 100
|
||||
)
|
||||
this.textChannel
|
||||
.send(
|
||||
i18n.__mf('play.unmutedSong', { author: user })
|
||||
)
|
||||
.catch(console.error)
|
||||
}
|
||||
break
|
||||
|
||||
case '🔉':
|
||||
reaction.users.remove(user).catch(console.error)
|
||||
if (this.volume == 0) return
|
||||
if (!canModifyQueue(member))
|
||||
return i18n.__('common.errorNotChannel')
|
||||
this.volume = Math.max(this.volume - 10, 0)
|
||||
this.resource.volume?.setVolumeLogarithmic(
|
||||
this.volume / 100
|
||||
)
|
||||
this.textChannel
|
||||
.send(
|
||||
i18n.__mf('play.decreasedVolume', {
|
||||
author: user,
|
||||
volume: this.volume,
|
||||
})
|
||||
)
|
||||
.catch(console.error)
|
||||
break
|
||||
|
||||
case '🔊':
|
||||
reaction.users.remove(user).catch(console.error)
|
||||
if (this.volume == 100) return
|
||||
if (!canModifyQueue(member))
|
||||
return i18n.__('common.errorNotChannel')
|
||||
this.volume = Math.min(this.volume + 10, 100)
|
||||
this.resource.volume?.setVolumeLogarithmic(
|
||||
this.volume / 100
|
||||
)
|
||||
this.textChannel
|
||||
.send(
|
||||
i18n.__mf('play.increasedVolume', {
|
||||
author: user,
|
||||
volume: this.volume,
|
||||
})
|
||||
)
|
||||
.catch(console.error)
|
||||
break
|
||||
|
||||
case '🔁':
|
||||
reaction.users.remove(user).catch(console.error)
|
||||
await this.bot.slashCommandsMap
|
||||
.get('loop')!
|
||||
.execute(this.interaction)
|
||||
break
|
||||
|
||||
case '🔀':
|
||||
reaction.users.remove(user).catch(console.error)
|
||||
await this.bot.slashCommandsMap
|
||||
.get('shuffle')!
|
||||
.execute(this.interaction)
|
||||
break
|
||||
|
||||
case '⏹':
|
||||
reaction.users.remove(user).catch(console.error)
|
||||
await this.bot.slashCommandsMap
|
||||
.get('stop')!
|
||||
.execute(this.interaction)
|
||||
collector.stop()
|
||||
break
|
||||
|
||||
default:
|
||||
reaction.users.remove(user).catch(console.error)
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
collector.on('end', () => {
|
||||
playingMessage.reactions.removeAll().catch(console.error)
|
||||
|
||||
if (config.PRUNING) {
|
||||
setTimeout(() => {
|
||||
playingMessage.delete().catch()
|
||||
}, 3000)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
/* eslint-disable no-useless-escape */
|
||||
export const videoPattern =
|
||||
/^(https?:\/\/)?(www\.)?(m\.|music\.)?(youtube\.com|youtu\.?be)\/.+$/
|
||||
export const playlistPattern = /^.*(list=)([^#\&\?]*).*/
|
||||
export const scRegex = /^https?:\/\/(soundcloud\.com)\/(.*)$/
|
||||
export const mobileScRegex = /^https?:\/\/(soundcloud\.app\.goo\.gl)\/(.*)$/
|
||||
export const isURL =
|
||||
/^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/
|
|
@ -0,0 +1,45 @@
|
|||
import youtube, { Playlist as YoutubePlaylist } from 'youtube-sr'
|
||||
import config from '../../config/music.json'
|
||||
import { Song } from './song'
|
||||
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
const pattern = /^.*(youtu.be\/|list=)([^#\&\?]*).*/i
|
||||
|
||||
export class Playlist {
|
||||
public data: YoutubePlaylist
|
||||
public videos: Song[]
|
||||
|
||||
public constructor(playlist: YoutubePlaylist) {
|
||||
this.data = playlist
|
||||
|
||||
this.videos = this.data.videos
|
||||
.filter(
|
||||
(video) =>
|
||||
video.title != 'Private video' &&
|
||||
video.title != 'Deleted video'
|
||||
)
|
||||
.slice(0, config.MAX_PLAYLIST_SIZE - 1)
|
||||
.map((video) => {
|
||||
return new Song({
|
||||
title: video.title!,
|
||||
url: `https://youtube.com/watch?v=${video.id}`,
|
||||
duration: video.duration / 1000,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
public static async from(url = '', search = '') {
|
||||
const urlValid = pattern.test(url)
|
||||
let playlist
|
||||
|
||||
if (urlValid) {
|
||||
playlist = await youtube.getPlaylist(url)
|
||||
} else {
|
||||
const result = await youtube.searchOne(search, 'playlist')
|
||||
|
||||
playlist = await youtube.getPlaylist(result.url!)
|
||||
}
|
||||
|
||||
return new this(playlist)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
import { GuildMember } from 'discord.js'
|
||||
|
||||
export const canModifyQueue = (member: GuildMember) =>
|
||||
member.voice.channelId === member.guild.members.me!.voice.channelId
|
|
@ -0,0 +1,93 @@
|
|||
import { AudioResource, createAudioResource } from '@discordjs/voice'
|
||||
import youtube from 'youtube-sr'
|
||||
import { videoPattern, isURL } from './patterns'
|
||||
import { i18n } from './i18n'
|
||||
import {
|
||||
SoundCloudStream,
|
||||
stream,
|
||||
video_basic_info,
|
||||
YouTubeStream,
|
||||
} from 'play-dl'
|
||||
|
||||
export interface SongData {
|
||||
url: string
|
||||
title: string
|
||||
duration: number
|
||||
}
|
||||
|
||||
export class Song {
|
||||
public readonly url: string
|
||||
public readonly title: string
|
||||
public readonly duration: number
|
||||
|
||||
public constructor({ url, title, duration }: SongData) {
|
||||
this.url = url
|
||||
this.title = title
|
||||
this.duration = duration
|
||||
}
|
||||
|
||||
public static async from(url = '', search = '') {
|
||||
const isYoutubeUrl = videoPattern.test(url)
|
||||
|
||||
let songInfo
|
||||
|
||||
if (isYoutubeUrl) {
|
||||
songInfo = (await video_basic_info(url)) as any
|
||||
|
||||
return new this({
|
||||
url: songInfo.video_details.url,
|
||||
title: songInfo.video_details.title,
|
||||
duration: parseInt(songInfo.video_details.durationInSec),
|
||||
})
|
||||
} else {
|
||||
const result = await youtube.searchOne(search)
|
||||
|
||||
result ? null : console.log(`No results found for ${search}`) // This is for handling the case where no results are found (spotify links for example)
|
||||
|
||||
if (!result) {
|
||||
const err = new Error(`No search results found for ${search}`)
|
||||
err.name = 'NoResults'
|
||||
if (isURL.test(url)) err.name = 'InvalidURL'
|
||||
|
||||
throw err
|
||||
}
|
||||
|
||||
songInfo = (await video_basic_info(
|
||||
`https://youtube.com/watch?v=${result.id}`
|
||||
)) as any
|
||||
|
||||
return new this({
|
||||
url: songInfo.video_details.url,
|
||||
title: songInfo.video_details.title,
|
||||
duration: parseInt(songInfo.video_details.durationInSec),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public async makeResource(): Promise<AudioResource<Song> | void> {
|
||||
let playStream: YouTubeStream | SoundCloudStream | any = null
|
||||
|
||||
// let type = this.url.includes("youtube.com") ? StreamType.Opus : StreamType.OggOpus;
|
||||
|
||||
const source = this.url.includes('youtube') ? 'youtube' : 'soundcloud'
|
||||
|
||||
if (source === 'youtube') {
|
||||
playStream = await stream(this.url)
|
||||
}
|
||||
|
||||
if (!stream) return
|
||||
|
||||
return createAudioResource(playStream.stream, {
|
||||
metadata: this,
|
||||
inputType: playStream.type,
|
||||
inlineVolume: true,
|
||||
})
|
||||
}
|
||||
|
||||
public startMessage() {
|
||||
return i18n.__mf('play.startedPlaying', {
|
||||
title: this.title,
|
||||
url: this.url,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -18,7 +18,7 @@
|
|||
"noFallthroughCasesInSwitch": true,
|
||||
"noImplicitAny": true,
|
||||
"noImplicitOverride": true,
|
||||
"noImplicitReturns": true,
|
||||
"noImplicitReturns": false,
|
||||
"noImplicitThis": true,
|
||||
"noUncheckedIndexedAccess": false,
|
||||
|
||||
|
@ -26,10 +26,10 @@
|
|||
"noUnusedParameters": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
// "strict": true,
|
||||
"strictFunctionTypes": true,
|
||||
"strictNullChecks": true,
|
||||
"strictPropertyInitialization": true,
|
||||
"strictPropertyInitialization": false,
|
||||
"target": "esnext",
|
||||
"outDir": "./dist"
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue