Merge pull request #1 from Myxelium/pause-resume-loop

Pause resume loop & progress bar with message
This commit was merged in pull request #1.
This commit is contained in:
2023-06-26 02:40:13 +02:00
committed by GitHub
18 changed files with 448 additions and 198 deletions

2
.env Normal file
View File

@@ -0,0 +1,2 @@
token="MTExOTYyOTQwNzk5NzQwNzMwMg.GOuub1.r8FZ_DWaouO17vO2QpgcDI-zTh4iTuUrgNZRbY"
clientId="1119629407997407302"

View File

@@ -1,8 +1,6 @@
const play = require('play-dl'); const play = require('play-dl');
if (play.is_expired()) { async function ReAuth() {
await play.refreshToken()
}
play.getFreeClientID().then((clientID) => { play.getFreeClientID().then((clientID) => {
play.setToken({ play.setToken({
@@ -15,4 +13,6 @@ play.getFreeClientID().then((clientID) => {
}) })
play.authorization(); play.authorization();
}
module.exports.ReAuth = ReAuth;

34
commands/loop.js Normal file
View File

@@ -0,0 +1,34 @@
const musicQueue = require("../musicQueue");
async function enableLooping(interaction) {
await interaction.deferReply();
const guildId = interaction.guild.id;
musicQueue.enableLooping(guildId);
interaction.followUp("Enabled looping for the current queue.");
}
async function unloopCommand(interaction) {
await interaction.deferReply();
const guildId = interaction.guild.id;
musicQueue.disableLooping(guildId);
interaction.followUp("Disabled looping for the current queue.");
}
async function toggleLoopCommand(interaction) {
await interaction.deferReply();
const guildId = interaction.guild.id;
if (musicQueue.looping.has(guildId) && musicQueue.looping.get(guildId)) {
musicQueue.disableLooping(guildId, false);
interaction.followUp("Disabled looping for the current queue.");
} else {
musicQueue.enableLooping(guildId, true);
interaction.followUp("Enabled looping for the current queue.");
}
}
module.exports.toggleLoopCommand = toggleLoopCommand;
module.exports.unloopCommand = unloopCommand;
module.exports.enableLooping = enableLooping;

34
commands/pauseResume.js Normal file
View File

@@ -0,0 +1,34 @@
const { getVoiceConnection } = require("@discordjs/voice");
async function pauseCommand(interaction) {
await interaction.deferReply();
const connection = getVoiceConnection(interaction.guild.id);
if (!connection) {
return interaction.followUp(
"There is no active music player in this server."
);
}
connection.state.subscription.player.pause();
interaction.followUp("Paused the music.");
}
async function unpauseCommand(interaction) {
await interaction.deferReply();
const connection = getVoiceConnection(interaction.guild.id);
if (!connection) {
return interaction.followUp(
"There is no active music player in this server."
);
}
connection.state.subscription.player.unpause();
interaction.followUp("Resumed the music.");
}
module.exports.pauseCommand = pauseCommand;
module.exports.unpauseCommand = unpauseCommand;

32
commands/play.js Normal file
View File

@@ -0,0 +1,32 @@
const { getMusicStream } = require("./../utils/getMusicStream");
const musicQueue = require("../musicQueue");
const { musicPlayer } = require("../utils/musicPlayer");
const { joinVoiceChannel } = require("@discordjs/voice");
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,
});
musicPlayer(interaction.guild.id, connection, interaction);
}
module.exports.playCommand = playCommand;

View File

@@ -1,23 +1,26 @@
const musicQueue = require('../musicQueue'); const musicQueue = require("../musicQueue");
const { getMusicStream } = require('../utils/getMusicStream'); const { getMusicStream } = require("../utils/getMusicStream");
async function queueCommand(interaction) { async function queueCommand(interaction) {
await interaction.deferReply(); await interaction.deferReply();
const query = interaction.options.getString('song'); const query = interaction.options.getString("song");
const voiceChannel = interaction.member.voice.channel; const voiceChannel = interaction.member.voice.channel;
if (!voiceChannel) { if (!voiceChannel) {
return interaction.followUp('You must be in a voice channel to use this command.'); return interaction.followUp(
"You must be in a voice channel to use this command."
);
} }
const song = await getMusicStream(query); const song = await getMusicStream(query);
if (!song) { if (!song) {
return interaction.followUp('Error finding song. Try Again.').then(msg => setTimeout(() => msg.delete(), 10000)); return interaction
.followUp("Error finding song. Try Again.")
.then((msg) => setTimeout(() => msg.delete(), 10000));
} }
musicQueue.addToQueue(interaction.guild.id, song); musicQueue.addToQueue(interaction.guild.id, song);
interaction.followUp(`Added ${song.title} to the queue.`); interaction.followUp(`Added ${song.title} to the queue.`);
} }

30
commands/stop.js Normal file
View File

@@ -0,0 +1,30 @@
const musicQueue = require("../musicQueue");
const { getVoiceConnection } = require("@discordjs/voice");
async function stopCommand(interaction) {
await interaction.deferReply();
const voiceChannel = interaction.member.voice.channel;
const connection = getVoiceConnection(interaction.guild.id);
if (!voiceChannel) {
return interaction.followUp(
"You must be in a voice channel to use this command."
);
}
const guildId = interaction.guild.id;
if (!connection.state.subscription.player) {
return interaction.followUp(
"I am not currently playing music in a voice channel."
);
}
connection.state.subscription.player.stop();
musicQueue.clearQueue(guildId);
interaction.followUp("Stopped the music and cleared the queue.");
}
module.exports.stopCommand = stopCommand;

View File

@@ -1,9 +1,13 @@
const { playCommand } = require('./play.js');
const { Client, GatewayIntentBits } = require('discord.js'); const { Client, GatewayIntentBits } = require('discord.js');
const { queueCommand } = require('./commands/queue');
const { registerCommands } = require('./utils/registerCommands'); const { registerCommands } = require('./utils/registerCommands');
const process = require('dotenv').config(); const { playCommand } = require('./commands/play');
const { queueCommand } = require('./commands/queue');
const { stopCommand } = require('./commands/stop');
const { pauseCommand, unpauseCommand } = require('./commands/pauseResume');
const { toggleLoopCommand } = require('./commands/loop');
const { ReAuth } = require('./ReAuthenticate');
const process = require('dotenv').config();
const clientId = process.parsed.clientId; const clientId = process.parsed.clientId;
const token = process.parsed.token; const token = process.parsed.token;
@@ -14,7 +18,8 @@ const client = new Client({
GatewayIntentBits.MessageContent, GatewayIntentBits.MessageContent,
GatewayIntentBits.GuildVoiceStates, GatewayIntentBits.GuildVoiceStates,
GatewayIntentBits.GuildIntegrations GatewayIntentBits.GuildIntegrations
] }) ]
})
client.on('ready', async () => { client.on('ready', async () => {
console.log(`Logged in as ${client.user.tag}!`); console.log(`Logged in as ${client.user.tag}!`);
@@ -31,9 +36,21 @@ client.on('interactionCreate', async (interaction) => {
await playCommand(interaction); await playCommand(interaction);
} else if (commandName === 'queue') { } else if (commandName === 'queue') {
await queueCommand(interaction); await queueCommand(interaction);
} else if (commandName === 'pause') {
await pauseCommand(interaction);
} else if (commandName === 'resume') {
await unpauseCommand(interaction);
} else if (commandName === 'loop') {
await toggleLoopCommand(interaction);
} else if (commandName === 'stop') {
await stopCommand(interaction);
} }
}); });
client.on('messageCreate', async (message) => {
if(message.content == 'reauth') {
await ReAuth();
}
});
// client.login(process.env.TOKEN);
client.login(token); client.login(token);

View File

@@ -1,24 +1,58 @@
const { getMusicStream } = require('./utils/getMusicStream');
class MusicQueue { class MusicQueue {
constructor() { constructor() {
this.queue = new Map(); this.queue = new Map();
} this.looping = new Map();
getQueue(guildId) {
if (!this.queue.has(guildId)) {
this.queue.set(guildId, []);
}
return this.queue.get(guildId);
} }
addToQueue(guildId, song) { addToQueue(guildId, song) {
const serverQueue = this.getQueue(guildId); if (!this.queue.has(guildId)) {
serverQueue.push(song); this.queue.set(guildId, []);
} }
removeFromQueue(guildId) { this.queue.get(guildId).push(song);
const serverQueue = this.getQueue(guildId); }
async removeFromQueue(guildId) {
if (!this.queue.has(guildId)) {
return;
}
const serverQueue = this.queue.get(guildId);
if (this.looping.has(guildId) && this.looping.get(guildId)) {
const song = serverQueue.shift();
const newSong = await getMusicStream(song.userInput);
serverQueue.push(newSong);
} else {
serverQueue.shift(); serverQueue.shift();
} }
} }
getQueue(guildId) {
if (!this.queue.has(guildId)) {
return [];
}
return this.queue.get(guildId);
}
enableLooping(guildId) {
this.looping.set(guildId, true);
}
disableLooping(guildId) {
this.looping.set(guildId, false);
}
clearQueue(guildId) {
if (!this.queue.has(guildId)) {
return;
}
this.queue.set(guildId, []);
}
}
module.exports = new MusicQueue(); module.exports = new MusicQueue();

View File

@@ -1,13 +1,14 @@
const playdl = require('play-dl'); const playdl = require("play-dl");
async function getStream(query) { async function getStream(query) {
let songInformation = await playdl.soundcloud(query) // Make sure that url is track url only. For playlist, make some logic. let songInformation = await playdl.soundcloud(query);
let stream = await playdl.stream_from_info(songInformation, { quality: 2 }); let stream = await playdl.stream_from_info(songInformation, { quality: 2 });
return { return {
title: songInformation.name, title: songInformation.name,
stream: stream.stream, stream: stream.stream,
duration: songInformation.durationInSec duration: songInformation.durationInSec * 1000,
} userInput: query,
};
} }
module.exports.getStream = getStream; module.exports.getStream = getStream;

View File

@@ -1,14 +1,13 @@
const playdl = require('play-dl'); const playdl = require('play-dl');
//TODO ADD SPOTIFY SUPPORT
async function getStream(query) { async function getStream(query) {
const trackInfo = await playdl.spotify(query); const trackInfo = await playdl.spotify(query);
let searched = await play.search(`${trackInfo.name}`, { let searched = await play.search(`${trackInfo.name}`, {
limit: 1 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
let stream = await play.stream(searched[0].url)
return stream; return stream;
} }

View File

@@ -1,38 +1,57 @@
const ytsr = require('ytsr'); const ytsr = require("ytsr");
const playdl = require('play-dl'); const playdl = require("play-dl");
async function getStream(query) { async function getStream(query) {
try { try {
const regex = /(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))([^?&\n]+)/; const regex = /(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))([^?&\n]+)/;
const match = query.match(regex); const match = query.match(regex);
let videoId; let videoId;
let usingYtsr = false;
if (match == null) { if (match == null) {
const searchResults = await ytsr(query, { page: 1, type: 'video' }); let result = await playdl.search(query, { limit: 1 });
videoId = searchResults.items[0].id; videoId = result[0].id;
// videoId = null;
console.log(searchResults.items[0].id)
if (videoId == null) { if (videoId == null) {
let result = await playdl.search(query, { limit: 1}) usingYtsr = true;
let videoUrl = result[0].url; const searchResults = await ytsr(query, {
videoId = videoUrl.match(regex)[1]; page: 1,
type: "video",
});
videoId = searchResults.items[0].id;
} }
} else { } else {
videoId = match[1]; videoId = match[1];
} }
const streamResult = await playdl.stream(`https://www.youtube.com/watch?v=${videoId}`, { quality: 2 }); const streamResult = await playdl.stream(
const infoResult = await ytsr(`https://www.youtube.com/watch?v=${videoId}`, { limit: 1}); `https://www.youtube.com/watch?v=${videoId}`,
{ quality: 2 }
);
const infoResult = usingYtsr
? await ytsr(`https://www.youtube.com/watch?v=${videoId}`, {
limit: 1,
})
: await playdl.video_info(
`https://www.youtube.com/watch?v=${videoId}`
);
console.log("\x1b[36m"," Id: ", videoId, "Alternative search:", usingYtsr);
return { return {
title: infoResult.items[0].title ?? 'Unknown', title:
duration: infoResult.items[0].duration ?? 0, (usingYtsr
? infoResult.items[0].title
: infoResult.video_details.title) ??
0,
duration:
(usingYtsr
? infoResult.items[0].duration
: (infoResult.video_details.durationInSec * 1000)) ??
0,
stream: streamResult.stream, stream: streamResult.stream,
type: streamResult.type type: streamResult.type,
userInput: query,
}; };
} catch (error) { } catch (error) {
console.log("\x1b[31m", error);
return null; return null;
} }
} }

View File

@@ -4,7 +4,8 @@
"description": "Discord music bot", "description": "Discord music bot",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1",
"start": "node index.js"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@@ -25,11 +26,8 @@
"discord.js": "^14.11.0", "discord.js": "^14.11.0",
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
"ffmpeg-static": "^4.2.7", "ffmpeg-static": "^4.2.7",
"libsodium-wrappers": "^0.7.11",
"opusscript": "^0.1.0", "opusscript": "^0.1.0",
"play-dl": "^1.9.6", "play-dl": "^1.9.6",
"sodium": "^3.0.2",
"sodium-native": "^4.0.4",
"soundcloud-scraper": "^5.0.3", "soundcloud-scraper": "^5.0.3",
"spotify-web-api-node": "^5.0.2", "spotify-web-api-node": "^5.0.2",
"tweetnacl": "^1.0.3", "tweetnacl": "^1.0.3",
@@ -37,3 +35,4 @@
"ytsr": "^3.8.2" "ytsr": "^3.8.2"
} }
} }

86
play.js
View File

@@ -1,86 +0,0 @@
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;

View File

@@ -9,6 +9,7 @@ async function getMusicStream(query) {
let songTitle; let songTitle;
let songDuration; let songDuration;
let type = StreamType.Opus; let type = StreamType.Opus;
let userInput = query;
if (query.includes('spotify.com')) { if (query.includes('spotify.com')) {
stream = await spotify.getStream(query); stream = await spotify.getStream(query);
@@ -24,16 +25,17 @@ async function getMusicStream(query) {
type = StreamType.OggOpus; type = StreamType.OggOpus;
} else { } else {
stream = await youtube.getStream(query); stream = await youtube.getStream(query)
songTitle = stream.title ?? 'Unknown'; songTitle = stream?.title ?? 'Unknown';
songDuration = stream.duration ?? 'Unknown'; songDuration = stream?.duration ?? 'Unknown';
stream = stream.stream; stream = stream?.stream;
type = StreamType.Opus; type = StreamType.Opus;
} }
return { return {
title: songTitle, title: songTitle,
duration: songDuration, duration: songDuration,
userInput: userInput,
stream: stream, stream: stream,
type: type type: type
}; };

74
utils/musicPlayer.js Normal file
View File

@@ -0,0 +1,74 @@
const {
createAudioResource,
createAudioPlayer,
NoSubscriberBehavior,
AudioPlayerStatus,
} = require('@discordjs/voice');
const { EmbedBuilder } = require('discord.js');
const musicQueue = require('../musicQueue');
const { progressBar } = require('../utils/progress');
async function musicPlayer(guildId, connection, interaction) {
const serverQueue = musicQueue.getQueue(guildId);
if (serverQueue.length === 0) {
connection.destroy();
return;
}
const song = serverQueue[0];
if (song.stream == null) {
musicQueue.removeFromQueue(guildId);
musicPlayer(guildId, connection);
return;
}
let resource = createAudioResource(song.stream, {
inputType: song.type
})
let player = createAudioPlayer({
behaviors: {
noSubscriber: NoSubscriberBehavior.Play
}
})
player.play(resource)
connection.subscribe(player)
progressBar(0, 0, true)
if(interaction.commandName == "play") {
interaction.followUp(`~🎵~`).then(message => {
const embed = new EmbedBuilder()
.setColor("#E0B0FF")
.setTitle("Now playing: " + song.title)
.setDescription(progressBar(song.duration, 10).progressBarString);
message.edit({ embeds: [embed] });
inter = setInterval(() => {
const { progressBarString, isDone } = progressBar(song.duration, 10);
if (isDone) {
clearInterval(inter);
message.delete();
}
embed.setDescription(progressBarString);
message.edit({ embeds: [embed] });
}, 2000)
});
}
player.on(AudioPlayerStatus.Idle, async () => {
console.log('Song ended:', song.title);
await musicQueue.removeFromQueue(guildId)
musicPlayer(guildId, connection, interaction);
});
return player;
}
module.exports.musicPlayer = musicPlayer;

38
utils/progress.js Normal file
View File

@@ -0,0 +1,38 @@
let startTime;
let current = 0;
let percentage;
const progressBar = (totalInMilliseconds, size, reset = false) => {
if (reset) {
startTime = Date.now();
current = 0;
}
if (!startTime) {
startTime = Date.now();
}
current = Date.now() - startTime;
const totalInSeconds = totalInMilliseconds / 1000;
percentage = Math.min((current / 1000) / totalInSeconds, 1);
const progress = Math.round((size * percentage));
const emptyProgress = size - progress;
const progressText = '▇'.repeat(progress);
const emptyProgressText = '—'.repeat(emptyProgress);
const percentageText = Math.round(percentage * 100) + '%';
let elapsedTimeText = new Date(current).toISOString().slice(11, -5);
let totalTimeText = new Date(totalInMilliseconds).toISOString().slice(11, -5);
if (totalTimeText.startsWith('00:')) {
elapsedTimeText = elapsedTimeText.slice(3);
totalTimeText = totalTimeText.slice(3);
}
const progressBarString = elapsedTimeText + ' [' + progressText + emptyProgressText + ']' + percentageText + ' ' + totalTimeText; // Creating and returning the bar
return { progressBarString, isDone: percentage === 1 };
}
module.exports.progressBar = progressBar;

View File

@@ -1,38 +1,56 @@
const { SlashCommandBuilder } = require('@discordjs/builders'); const { SlashCommandBuilder } = require("@discordjs/builders");
const { REST } = require('@discordjs/rest'); const { REST } = require("@discordjs/rest");
const { Routes } = require('discord-api-types/v9'); const { Routes } = require("discord-api-types/v9");
async function registerCommands(clientId, token) { async function registerCommands(clientId, token) {
const commands = [ const commands = [
new SlashCommandBuilder() new SlashCommandBuilder()
.setName('play') .setName("play")
.setDescription('Plays songs!') .setDescription("Plays songs!")
.addStringOption(option => .addStringOption((option) =>
option.setName('input') option
.setDescription('Play song from YouTube, Spotify, SoundCloud, etc.') .setName("input")
.setDescription("Play song from YouTube, Spotify, SoundCloud, etc.")
.setRequired(true) .setRequired(true)
), ),
new SlashCommandBuilder() new SlashCommandBuilder()
.setName('queue') .setName("queue")
.setDescription('Adds a song to the queue!') .setDescription("Adds a song to the queue!")
.addStringOption(option => .addStringOption((option) =>
option.setName('song') option
.setDescription('Add song from YouTube, Spotify, SoundCloud, etc. to the queue') .setName("song")
.setRequired(true) .setDescription(
"Add song from YouTube, Spotify, SoundCloud, etc. to the queue"
) )
.setRequired(true)
),
new SlashCommandBuilder()
.setName("pause")
.setDescription("Pauses the current song!"),
new SlashCommandBuilder()
.setName("resume")
.setDescription("Resumes the current song!"),
new SlashCommandBuilder()
.setName("loop")
.setDescription("Loops the current song! (toggle)"),
new SlashCommandBuilder()
.setName("stop")
.setDescription("Stops the current song!"),
]; ];
const rest = new REST({ version: '9' }).setToken(token); const rest = new REST({
version: "9",
})
.setToken(token);
try { try {
console.log('Started refreshing application (/) commands.'); console.log("\x1b[35m", "Started refreshing application (/) commands.");
await rest.put( await rest.put(Routes.applicationCommands(clientId), {
Routes.applicationCommands(clientId), body: commands,
{ body: commands }, });
);
console.log('Successfully reloaded application (/) commands.'); console.log("\x1b[35m", "Successfully reloaded application (/) commands.");
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} }