Create Discord Music Bot
Can play and queue, can play music in multiple servers at once
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
node_modules
|
||||||
18
ReAuthenticate.js
Normal file
18
ReAuthenticate.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
const play = require('play-dl');
|
||||||
|
|
||||||
|
if (play.is_expired()) {
|
||||||
|
await play.refreshToken()
|
||||||
|
}
|
||||||
|
|
||||||
|
play.getFreeClientID().then((clientID) => {
|
||||||
|
play.setToken({
|
||||||
|
soundcloud : {
|
||||||
|
client_id : clientID
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('Soudncloud Client ID: ' + clientID);
|
||||||
|
})
|
||||||
|
|
||||||
|
play.authorization();
|
||||||
|
|
||||||
24
commands/queue.js
Normal file
24
commands/queue.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
const musicQueue = require('../musicQueue');
|
||||||
|
const { getMusicStream } = require('../utils/getMusicStream');
|
||||||
|
|
||||||
|
async function queueCommand(interaction) {
|
||||||
|
await interaction.deferReply();
|
||||||
|
|
||||||
|
const query = interaction.options.getString('song');
|
||||||
|
const voiceChannel = interaction.member.voice.channel;
|
||||||
|
|
||||||
|
if (!voiceChannel) {
|
||||||
|
return interaction.followUp('You must be in a voice channel to use this command.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const song = await getMusicStream(query);
|
||||||
|
if (!song) {
|
||||||
|
return interaction.followUp('Error finding song. Try Again.').then(msg => setTimeout(() => msg.delete(), 10000));
|
||||||
|
}
|
||||||
|
|
||||||
|
musicQueue.addToQueue(interaction.guild.id, song);
|
||||||
|
|
||||||
|
interaction.followUp(`Added ${song.title} to the queue.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.queueCommand = queueCommand;
|
||||||
39
index.js
Normal file
39
index.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
const { playCommand } = require('./play.js');
|
||||||
|
const { Client, GatewayIntentBits } = require('discord.js');
|
||||||
|
const { queueCommand } = require('./commands/queue');
|
||||||
|
const { registerCommands } = require('./utils/registerCommands');
|
||||||
|
const process = require('dotenv').config();
|
||||||
|
|
||||||
|
const clientId = process.parsed.clientId;
|
||||||
|
const token = process.parsed.token;
|
||||||
|
|
||||||
|
const client = new Client({
|
||||||
|
intents: [
|
||||||
|
GatewayIntentBits.Guilds,
|
||||||
|
GatewayIntentBits.GuildMessages,
|
||||||
|
GatewayIntentBits.MessageContent,
|
||||||
|
GatewayIntentBits.GuildVoiceStates,
|
||||||
|
GatewayIntentBits.GuildIntegrations
|
||||||
|
] })
|
||||||
|
|
||||||
|
client.on('ready', async () => {
|
||||||
|
console.log(`Logged in as ${client.user.tag}!`);
|
||||||
|
|
||||||
|
await registerCommands(clientId, token);
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('interactionCreate', async (interaction) => {
|
||||||
|
if (!interaction.isCommand()) return;
|
||||||
|
|
||||||
|
const { commandName } = interaction;
|
||||||
|
|
||||||
|
if (commandName === 'play') {
|
||||||
|
await playCommand(interaction);
|
||||||
|
} else if (commandName === 'queue') {
|
||||||
|
await queueCommand(interaction);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// client.login(process.env.TOKEN);
|
||||||
|
client.login(token);
|
||||||
24
musicQueue.js
Normal file
24
musicQueue.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
class MusicQueue {
|
||||||
|
constructor() {
|
||||||
|
this.queue = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
getQueue(guildId) {
|
||||||
|
if (!this.queue.has(guildId)) {
|
||||||
|
this.queue.set(guildId, []);
|
||||||
|
}
|
||||||
|
return this.queue.get(guildId);
|
||||||
|
}
|
||||||
|
|
||||||
|
addToQueue(guildId, song) {
|
||||||
|
const serverQueue = this.getQueue(guildId);
|
||||||
|
serverQueue.push(song);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeFromQueue(guildId) {
|
||||||
|
const serverQueue = this.getQueue(guildId);
|
||||||
|
serverQueue.shift();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new MusicQueue();
|
||||||
13
music_sources/soundcloud.js
Normal file
13
music_sources/soundcloud.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
const playdl = require('play-dl');
|
||||||
|
|
||||||
|
async function getStream(query) {
|
||||||
|
let songInformation = await playdl.soundcloud(query) // Make sure that url is track url only. For playlist, make some logic.
|
||||||
|
let stream = await playdl.stream_from_info(songInformation, { quality: 2 });
|
||||||
|
return {
|
||||||
|
title: songInformation.name,
|
||||||
|
stream: stream.stream,
|
||||||
|
duration: songInformation.durationInSec
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.getStream = getStream;
|
||||||
15
music_sources/spotify.js
Normal file
15
music_sources/spotify.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
const playdl = require('play-dl');
|
||||||
|
|
||||||
|
async function getStream(query) {
|
||||||
|
const trackInfo = await playdl.spotify(query);
|
||||||
|
|
||||||
|
let searched = await play.search(`${trackInfo.name}`, {
|
||||||
|
limit: 1
|
||||||
|
}) // This will search the found track on youtube.
|
||||||
|
|
||||||
|
let stream = await play.stream(searched[0].url) // This will create stream from the above search
|
||||||
|
|
||||||
|
return stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.getStream = getStream;
|
||||||
40
music_sources/youtube.js
Normal file
40
music_sources/youtube.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
const ytsr = require('ytsr');
|
||||||
|
const playdl = require('play-dl');
|
||||||
|
|
||||||
|
async function getStream(query) {
|
||||||
|
try {
|
||||||
|
const regex = /(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))([^?&\n]+)/;
|
||||||
|
const match = query.match(regex);
|
||||||
|
let videoId;
|
||||||
|
|
||||||
|
if(match == null) {
|
||||||
|
const searchResults = await ytsr(query, { page: 1, type: 'video' });
|
||||||
|
videoId = searchResults.items[0].id;
|
||||||
|
// videoId = null;
|
||||||
|
|
||||||
|
console.log(searchResults.items[0].id)
|
||||||
|
|
||||||
|
if (videoId == null) {
|
||||||
|
let result = await playdl.search(query, { limit: 1})
|
||||||
|
let videoUrl = result[0].url;
|
||||||
|
videoId = videoUrl.match(regex)[1];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
videoId = match[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
const streamResult = await playdl.stream(`https://www.youtube.com/watch?v=${videoId}`, { quality: 2 });
|
||||||
|
const infoResult = await ytsr(`https://www.youtube.com/watch?v=${videoId}`, { limit: 1});
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: infoResult.items[0].title ?? 'Unknown',
|
||||||
|
duration: infoResult.items[0].duration ?? 0,
|
||||||
|
stream: streamResult.stream,
|
||||||
|
type: streamResult.type
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.getStream = getStream;
|
||||||
2795
package-lock.json
generated
Normal file
2795
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
39
package.json
Normal file
39
package.json
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"name": "lunaris",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Discord music bot",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "lunaris"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"music",
|
||||||
|
"bot",
|
||||||
|
"discord",
|
||||||
|
"lunaris"
|
||||||
|
],
|
||||||
|
"author": "Azaaxin",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@discordjs/builders": "^1.6.3",
|
||||||
|
"@discordjs/opus": "^0.9.0",
|
||||||
|
"@discordjs/voice": "^0.16.0",
|
||||||
|
"discord.js": "^14.11.0",
|
||||||
|
"dotenv": "^16.3.1",
|
||||||
|
"ffmpeg-static": "^4.2.7",
|
||||||
|
"libsodium-wrappers": "^0.7.11",
|
||||||
|
"opusscript": "^0.1.0",
|
||||||
|
"play-dl": "^1.9.6",
|
||||||
|
"sodium": "^3.0.2",
|
||||||
|
"sodium-native": "^4.0.4",
|
||||||
|
"soundcloud-scraper": "^5.0.3",
|
||||||
|
"spotify-web-api-node": "^5.0.2",
|
||||||
|
"tweetnacl": "^1.0.3",
|
||||||
|
"ytdl-core": "^4.11.4",
|
||||||
|
"ytsr": "^3.8.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
86
play.js
Normal file
86
play.js
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
const {
|
||||||
|
joinVoiceChannel,
|
||||||
|
createAudioResource,
|
||||||
|
createAudioPlayer,
|
||||||
|
NoSubscriberBehavior,
|
||||||
|
AudioPlayerStatus,
|
||||||
|
} = require('@discordjs/voice');
|
||||||
|
const musicQueue = require('./musicQueue');
|
||||||
|
const { getMusicStream } = require('./utils/getMusicStream');
|
||||||
|
|
||||||
|
async function playCommand(interaction) {
|
||||||
|
await interaction.deferReply();
|
||||||
|
|
||||||
|
const query = interaction.options.getString('input');
|
||||||
|
const voiceChannel = interaction.member.voice.channel;
|
||||||
|
|
||||||
|
if (!voiceChannel) {
|
||||||
|
return interaction.followUp('You must be in a voice channel to use this command.');
|
||||||
|
}
|
||||||
|
const song = await getMusicStream(query);
|
||||||
|
|
||||||
|
musicQueue.addToQueue(interaction.guild.id, song);
|
||||||
|
|
||||||
|
const connection = joinVoiceChannel({
|
||||||
|
channelId: voiceChannel.id,
|
||||||
|
guildId: interaction.guild.id,
|
||||||
|
adapterCreator: interaction.guild.voiceAdapterCreator,
|
||||||
|
selfDeaf: false,
|
||||||
|
selfMute: false
|
||||||
|
});
|
||||||
|
|
||||||
|
playSong(interaction.guild.id, connection);
|
||||||
|
|
||||||
|
interaction.followUp(`Added ${song.title} to the queue.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function playSong(guildId, connection) {
|
||||||
|
const serverQueue = musicQueue.getQueue(guildId);
|
||||||
|
|
||||||
|
if (serverQueue.length === 0) {
|
||||||
|
connection.destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const song = serverQueue[0];
|
||||||
|
|
||||||
|
let resource = createAudioResource(song.stream, {
|
||||||
|
inputType: song.type
|
||||||
|
})
|
||||||
|
|
||||||
|
let player = createAudioPlayer({
|
||||||
|
behaviors: {
|
||||||
|
noSubscriber: NoSubscriberBehavior.Play
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
player.play(resource)
|
||||||
|
|
||||||
|
connection.subscribe(player)
|
||||||
|
|
||||||
|
player.on(AudioPlayerStatus.Idle, () => {
|
||||||
|
console.log('Song ended:', song.title);
|
||||||
|
musicQueue.removeFromQueue(guildId);
|
||||||
|
playSong(guildId, connection);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: USE THIS AGAIN
|
||||||
|
function convertToMilliseconds(songLenth) {
|
||||||
|
try {
|
||||||
|
let time = songLenth.split(':')
|
||||||
|
let milliseconds = 0;
|
||||||
|
if(time.length == 3) {
|
||||||
|
milliseconds = (parseInt(time[0]) * 60 * 60 * 1000) + (parseInt(time[1]) * 60 * 1000) + (parseInt(time[2]) * 1000)
|
||||||
|
} else if(time.length == 2) {
|
||||||
|
milliseconds = (parseInt(time[0]) * 60 * 1000) + (parseInt(time[1]) * 1000)
|
||||||
|
} else if(time.length == 1) {
|
||||||
|
milliseconds = (parseInt(time[0]) * 1000)
|
||||||
|
}
|
||||||
|
return milliseconds
|
||||||
|
} catch (error) {
|
||||||
|
return 10000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.playCommand = playCommand;
|
||||||
42
utils/getMusicStream.js
Normal file
42
utils/getMusicStream.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
const spotify = require('../music_sources/spotify');
|
||||||
|
const soundcloud = require('../music_sources/soundcloud');
|
||||||
|
const youtube = require('../music_sources/youtube');
|
||||||
|
|
||||||
|
const { StreamType } = require('@discordjs/voice');
|
||||||
|
|
||||||
|
async function getMusicStream(query) {
|
||||||
|
let stream;
|
||||||
|
let songTitle;
|
||||||
|
let songDuration;
|
||||||
|
let type = StreamType.Opus;
|
||||||
|
|
||||||
|
if (query.includes('spotify.com')) {
|
||||||
|
stream = await spotify.getStream(query);
|
||||||
|
songTitle = stream.title;
|
||||||
|
songDuration = stream.duration;
|
||||||
|
stream = stream.stream;
|
||||||
|
|
||||||
|
} else if (query.includes('soundcloud.com')) {
|
||||||
|
stream = await soundcloud.getStream(query);
|
||||||
|
songTitle = stream.title;
|
||||||
|
songDuration = stream.duration;
|
||||||
|
stream = stream.stream;
|
||||||
|
type = StreamType.OggOpus;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
stream = await youtube.getStream(query);
|
||||||
|
songTitle = stream.title ?? 'Unknown';
|
||||||
|
songDuration = stream.duration ?? 'Unknown';
|
||||||
|
stream = stream.stream;
|
||||||
|
type = StreamType.Opus;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: songTitle,
|
||||||
|
duration: songDuration,
|
||||||
|
stream: stream,
|
||||||
|
type: type
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.getMusicStream = getMusicStream;
|
||||||
41
utils/registerCommands.js
Normal file
41
utils/registerCommands.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
const { SlashCommandBuilder } = require('@discordjs/builders');
|
||||||
|
const { REST } = require('@discordjs/rest');
|
||||||
|
const { Routes } = require('discord-api-types/v9');
|
||||||
|
|
||||||
|
async function registerCommands(clientId, token) {
|
||||||
|
const commands = [
|
||||||
|
new SlashCommandBuilder()
|
||||||
|
.setName('play')
|
||||||
|
.setDescription('Plays songs!')
|
||||||
|
.addStringOption(option =>
|
||||||
|
option.setName('input')
|
||||||
|
.setDescription('Play song from YouTube, Spotify, SoundCloud, etc.')
|
||||||
|
.setRequired(true)
|
||||||
|
),
|
||||||
|
new SlashCommandBuilder()
|
||||||
|
.setName('queue')
|
||||||
|
.setDescription('Adds a song to the queue!')
|
||||||
|
.addStringOption(option =>
|
||||||
|
option.setName('song')
|
||||||
|
.setDescription('Add song from YouTube, Spotify, SoundCloud, etc. to the queue')
|
||||||
|
.setRequired(true)
|
||||||
|
)
|
||||||
|
];
|
||||||
|
|
||||||
|
const rest = new REST({ version: '9' }).setToken(token);
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('Started refreshing application (/) commands.');
|
||||||
|
|
||||||
|
await rest.put(
|
||||||
|
Routes.applicationCommands(clientId),
|
||||||
|
{ body: commands },
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('Successfully reloaded application (/) commands.');
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.registerCommands = registerCommands;
|
||||||
Reference in New Issue
Block a user