diff --git a/.gitignore b/.gitignore index 8506ed8..238b8dd 100644 --- a/.gitignore +++ b/.gitignore @@ -141,4 +141,6 @@ docs/_book # TODO: where does this rule come from? test/ -data/ \ No newline at end of file +data/ + +__DEBUG.js \ No newline at end of file diff --git a/backend/src/discord/Activity.js b/backend/src/discord/Activity.js index 5f063ed..bac8bed 100644 --- a/backend/src/discord/Activity.js +++ b/backend/src/discord/Activity.js @@ -13,10 +13,10 @@ function setMusicActivity(songName, artistName, imageUrl) { const client = bot.getClient() client.user.setActivity(`${songName} - ${artistName}`,{ type: ActivityType.Listening, - /*assets: { + assets: { largeImage: imageUrl, largeText: songName - }*/ + } }); dlog.log(`Activité mise à jour : ${songName} - ${artistName}`) } diff --git a/backend/src/discord/Bot.js b/backend/src/discord/Bot.js index 17c2b36..ba4a1bc 100644 --- a/backend/src/discord/Bot.js +++ b/backend/src/discord/Bot.js @@ -20,7 +20,7 @@ function getClient() { function init() { - + client.once('ready', () => { dlog.log("Connexion au Bot Discord réussi ! Connecté en tant que : " + client.user.tag) diff --git a/backend/src/discord/Commands/Leave.js b/backend/src/discord/Commands/Leave.js new file mode 100644 index 0000000..1713cb7 --- /dev/null +++ b/backend/src/discord/Commands/Leave.js @@ -0,0 +1,28 @@ +const {Command} = require("../Command") +const {Embed, EmbedError} = require("../Embed") +const {Player, AllPlayers} = require("../../player/Player") + +const command = new Command("leave", "Quitter le salon vocal", (client, interaction) => { + + if(!interaction.member.voice.channel) return new EmbedError("Vous devez rejoindre un salon vocal pour arrêter le bot !").send(interaction) + const channel = interaction.member.voice.channel + var embed = new Embed() + if(AllPlayers.has(channel.guildId)) { + const player = AllPlayers.get(channel.guildId) + player.leave() + + + embed.setColor(200, 20, 20) + embed.setTitle('**Déconnexion**') + embed.setDescription('Déconnexion du salon vocal') + embed.setThumbnail("https://www.iconsdb.com/icons/download/white/phone-51-64.png") + + } else { + + embed = new EmbedError("Le bot n'est pas connecté à ce salon vocal") + } + embed.send(interaction) + +}) + +module.exports = {command} diff --git a/backend/src/discord/Commands/Media.js b/backend/src/discord/Commands/Media.js index 723f245..cde51aa 100644 --- a/backend/src/discord/Commands/Media.js +++ b/backend/src/discord/Commands/Media.js @@ -5,7 +5,7 @@ const { Song } = require('../../player/Song'); const command = new Command("media", "Lire un média MP3/WAV dans un salon vocal", async (client, interaction) => { - if(!interaction.member.voice.channel) return interaction.reply({content:"Vous devez rejoindre un salon vocal pour lire un(e) titre / playlist !", ephemeral: true}) + if(!interaction.member.voice.channel) return new EmbedError("Vous devez rejoindre un salon vocal pour jouer un média !").send(interaction, true) const media = interaction.options.get("media") const now = interaction.options.getBoolean("now") || false @@ -34,9 +34,9 @@ const command = new Command("media", "Lire un média MP3/WAV dans un salon vocal } embed.setDescription('**Titre : **' + song.title) - embed.addField('**Durée : **'+ song.readduration, "") - embed.addField("**Artiste : **" + song.author, "") - embed.addField('**Demandé par **' + interaction.member.user.username,"") + embed.addField('**Durée : **', song.readduration) + embed.addField("**Artiste : **",song.author) + embed.addField('**Demandé par **' + interaction.member.user.username, "") embed.setThumbnail(song.thumbnail) diff --git a/backend/src/discord/Commands/Pause.js b/backend/src/discord/Commands/Pause.js index 6bba65f..dac6a82 100644 --- a/backend/src/discord/Commands/Pause.js +++ b/backend/src/discord/Commands/Pause.js @@ -2,22 +2,42 @@ const {Command} = require("../Command") const {Embed, EmbedError} = require("../Embed") const { Player } = require("../../player/Player") -const command = new Command("pause", "Mettre en pause la musique en cours", (client, interaction) => { +const command = new Command("pause", "Mettre en pause / Reprendre la musique en cours", (client, interaction) => { if(!interaction.member.voice.channel) return new EmbedError("Vous devez rejoindre un salon vocal pour mettre en pause la musique !").send(interaction) const channel = interaction.member.voice.channel const player = new Player(channel.guildId) - player.pause() + const result = player.pause() + + + var embed = new Embed() + embed.setColor(0x03ff2d) + + result.then((pause) => { + + if(pause == "no_music") { + embed = new EmbedError("Il n'y a pas de musique en cours de lecture") + + } else if(pause) { + embed.setTitle('Musique en pause') + embed.setDescription("La musique a été mise en pause") + embed.setThumbnail("https://www.iconsdb.com/icons/download/white/pause-64.png") + + + } else { + embed.setTitle('Musique reprise') + embed.setDescription("La musique a été reprise") + embed.setThumbnail("https://www.iconsdb.com/icons/download/white/play-64.png") + } + + embed.send(interaction) + }) // Réponse en embed - const embed = new Embed() - embed.setColor(0x00ff66) - embed.setTitle('Musique en pause') - embed.setDescription("La musique a été mise en pause") - embed.send(interaction) + }) diff --git a/backend/src/discord/Commands/Previous.js b/backend/src/discord/Commands/Previous.js new file mode 100644 index 0000000..bd1a787 --- /dev/null +++ b/backend/src/discord/Commands/Previous.js @@ -0,0 +1,52 @@ +const {Command} = require("../Command") +const {Embed, EmbedError} = require("../Embed") +const { Player, AllPlayers } = require("../../player/Player") + +const command = new Command("previous", "Passe à la musique précédente", (client, interaction) => { + + + if(!interaction.member.voice.channel) return new EmbedError("Vous devez rejoindre un salon vocal pour passer à la musique suivante !").send(interaction) + + const channel = interaction.member.voice.channel + + if(AllPlayers.has(channel.guildId)) { + + const player = new Player(channel.guildId) + const result = player.previous() + + + var embed = new Embed() + embed.setColor(0x15e6ed) + + result.then((song) => { + + if(song == "no_music") { + embed = new EmbedError("Il n'y a pas de musique précédemment jouée") + + } else if(song) { + + // Result is a song + + + + embed.setTitle('**Musique précédente !**') + embed.setDescription('**Titre : **' + song.title) + embed.addField('**Durée : **'+ song.readduration, "") + embed.addField("**Artiste : **" + song.author, "") + embed.setThumbnail(song.thumbnail) + + + } + + embed.send(interaction) + }) + + } else { + return new EmbedError("Le bot n'est pas connecté").send(interaction) + } + + + +}) + +module.exports = {command} \ No newline at end of file diff --git a/backend/src/discord/Commands/Queue.js b/backend/src/discord/Commands/Queue.js new file mode 100644 index 0000000..f67a937 --- /dev/null +++ b/backend/src/discord/Commands/Queue.js @@ -0,0 +1,39 @@ +const {Command} = require("../Command") +const {Embed, EmbedError} = require("../Embed") +const { Player, AllPlayers } = require("../../player/Player") + +const command = new Command("liste", "Affiche la file d'attente", (client, interaction) => { + + const channel = interaction.member.voice.channel + + if(AllPlayers.has(channel.guildId)) { + + const player = new Player(channel.guildId) + const queue = player.queue.getNext() + + var embed = new Embed() + embed.setColor(0x15e6ed) + + if(queue.length == 0) { + embed = new EmbedError("Il n'y a pas de musique en file d'attente") + + } else if(queue.length > 0) { + + // Result is a song + embed.setColor(0x15e6ed) + embed.setThumbnail("https://www.iconsdb.com/icons/download/white/list-2-64.png") + embed.setTitle('**File d\'attente :**') + queue.forEach((song, index) => { + embed.addField(`**${index+1} - ${song.title}**`, `**Durée : **${song.readduration}\n**Artiste : **${song.author}`) + }) + + } + + embed.send(interaction) + + } else { + return new EmbedError("Le bot n'est pas connecté").send(interaction) + } + }) + +module.exports = {command} \ No newline at end of file diff --git a/backend/src/discord/Commands/Skip.js b/backend/src/discord/Commands/Skip.js new file mode 100644 index 0000000..637074f --- /dev/null +++ b/backend/src/discord/Commands/Skip.js @@ -0,0 +1,49 @@ +const {Command} = require("../Command") +const {Embed, EmbedError} = require("../Embed") +const { Player, AllPlayers } = require("../../player/Player") + +const command = new Command("skip", "Passe à la musique suivante", (client, interaction) => { + + if(!interaction.member.voice.channel) return new EmbedError("Vous devez rejoindre un salon vocal pour passer à la musique suivante !").send(interaction) + + const channel = interaction.member.voice.channel + + if(AllPlayers.has(channel.guildId)) { + + const player = new Player(channel.guildId) + const result = player.skip() + + + var embed = new Embed() + embed.setColor(0x15e6ed) + result.then((song) => { + + if(song == "no_music") { + embed = new EmbedError("Il n'y a pas de musique en file d'attente") + + } else if(song) { + + // Result is a song + embed.setColor(0x15e6ed) + + embed.setTitle('**Musique suivante !**') + embed.setDescription('**Titre : **' + song.title) + embed.addField('**Durée : **'+ song.readduration, "") + embed.addField("**Artiste : **" + song.author, "") + embed.setThumbnail(song.thumbnail) + + + } + + embed.send(interaction) + }) + + } else { + return new EmbedError("Le bot n'est pas connecté").send(interaction) + } + + + +}) + +module.exports = {command} \ No newline at end of file diff --git a/backend/src/discord/Commands/State.js b/backend/src/discord/Commands/State.js new file mode 100644 index 0000000..b17ecaa --- /dev/null +++ b/backend/src/discord/Commands/State.js @@ -0,0 +1,34 @@ +const {Command} = require("../Command") +const {Embed, EmbedError} = require("../Embed") +const {Player} = require("../../player/Player") + +const command = new Command("state", "Affiche la musique en cours", (client, interaction) => { + + const channel = interaction.member.voice.channel + const player = new Player(channel.guildId) + const song = player.queue.getCurrent() + + var embed = new Embed() + embed.setColor(0x15e6ed) + + if(!song) { + embed = new EmbedError("Il n'y a pas de musique en cours de lecture") + + } else if(song) { + + // Result is a song + embed.setColor(0x15e6ed) + + embed.setTitle('**Musique en cours :**') + embed.setDescription('**Titre : **' + song.title) + embed.addField('**Durée : **', song.readduration) + embed.addField("**Artiste : **", song.author) + embed.setThumbnail(song.thumbnail) + + + } + + embed.send(interaction) +}) + +module.exports = {command} diff --git a/backend/src/discord/Embed.js b/backend/src/discord/Embed.js index f76f096..7e6e37b 100644 --- a/backend/src/discord/Embed.js +++ b/backend/src/discord/Embed.js @@ -81,8 +81,12 @@ class Embed { return this.embed } - send(interaction) { - interaction.reply({embeds: [this.build()]}) + send(interaction, ephemeral) { + if(ephemeral) { + interaction.reply({embeds: [this.build()], ephemeral: true}) + } else { + interaction.reply({embeds: [this.build()]}) + } } } diff --git a/backend/src/media/MediaInformation.js b/backend/src/media/MediaInformation.js new file mode 100644 index 0000000..fe0f03b --- /dev/null +++ b/backend/src/media/MediaInformation.js @@ -0,0 +1,32 @@ +const ffprobe = require('ffprobe'); +const ffprobeStatic = require('ffprobe-static'); +const { getReadableDuration } = require('../utils/TimeConverter'); +const clog = require("loguix").getInstance("Song") + + +async function getMediaInformation(instance, media, provider) { + try { + const info = await ffprobe(media.attachment.url, { path: ffprobeStatic.path }); + if (info.streams?.[0]?.duration_ts) { + instance.duration = info.streams[0].duration; + instance.readduration = getReadableDuration(instance.duration) + } + + // Vérification pour éviter une erreur si `streams[0]` ou `tags` n'existe pas + instance.thumbnail = info.streams?.[0]?.tags?.thumbnail ?? + "https://radomisol.fr/wp-content/uploads/2016/08/cropped-note-radomisol-musique.png"; + + // Obtenir le titre (sinon utiliser le nom du fichier) + instance.title = info.streams?.[0]?.tags?.title ?? media.attachment.name; + + // Obtenir l'auteur (s'il existe) + instance.author = info.streams?.[0]?.tags?.artist ?? instance.author; + + } catch (err) { + clog.error("Impossible de récupérer les informations de la musique : " + media.attachment.name) + clog.error(err) + return null; + } +} + +module.exports = {getMediaInformation} \ No newline at end of file diff --git a/backend/src/player/Finder.js b/backend/src/player/Finder.js deleted file mode 100644 index e69de29..0000000 diff --git a/backend/src/player/List.js b/backend/src/player/List.js index 71de905..c1616af 100644 --- a/backend/src/player/List.js +++ b/backend/src/player/List.js @@ -3,6 +3,7 @@ const { __glob } = require('../utils/GlobalVars') const PreviousDB = new Database("previous", __glob.PREVIOUSFILE, {}) const {LogType} = require("loguix") const clog = new LogType("List") +const { Song } = require('./Song') const AllLists = new Map() // Map @@ -43,6 +44,9 @@ class List { } nextSong() { + if(this.current != null) { + this.addPreviousSong(this.current) + } var song = null; if(!this.shuffle) { song = this.next[0] @@ -53,9 +57,11 @@ class List { this.next.splice(randomIndex, 1) } + this.setCurrent(song) return song } + clearNext() { this.next = new Array(); } @@ -64,6 +70,10 @@ class List { this.next.push(song) } + firstNext(song) { + this.next.unshift(song) + } + removeNextByIndex(index) { this.next.splice(index, 1) } @@ -75,15 +85,33 @@ class List { } getPrevious() { - return PreviousDB.data[this.guildId]; + const previousList = new Array() + + for(const song of PreviousDB.data[this.guildId]) { + previousList.push(new Song(song)) + } + return previousList; + } getPreviousSong() { + if(PreviousDB.data[this.guildId].length > 0) { + return new Song(PreviousDB.data[this.guildId][0]) + } else { + return null; + } + } + + previousSong() { + if(this.current != null) { + this.firstNext(this.current) + } if(PreviousDB.data[this.guildId].length > 0) { const song = PreviousDB.data[this.guildId][0] + // Remove the song from the previous list PreviousDB.data[this.guildId].splice(0, 1) savePrevious() - return song + return new Song(song) } else { return null; } @@ -116,6 +144,8 @@ class List { this.clearNext(); this.current = null this.shuffle = false; + AllLists.delete(this.guildId) + } setShuffle(value) { diff --git a/backend/src/player/Method/Media.js b/backend/src/player/Method/Media.js index 0185ffe..427d477 100644 --- a/backend/src/player/Method/Media.js +++ b/backend/src/player/Method/Media.js @@ -1,19 +1,28 @@ -const {createAudioResource, VoiceConnectionStatus} = require('@discordjs/voice'); +const {createAudioResource, VoiceConnectionStatus, createAudioPlayer} = require('@discordjs/voice'); const {LogType} = require('loguix') const clog = new LogType("Media") const plog = require("loguix").getInstance("Player") async function play(instance, song) { - //const resource = await song.getResource() - //Test with a local file - const resource = createAudioResource("C:\\Users\\picot\\Downloads\\Confrontation_Replique_Raphix.mp3") - console.log(resource) - // Wait until connection is ready - instance.connection.once(VoiceConnectionStatus.Ready, async () => { - instance.player.play(resource) - }) - plog.log(`GUILD : ${instance.guildId} - Lecture de la musique : ${song.title}`) + + try { + + instance.player = createAudioPlayer() + instance.generatePlayerEvents() + const player = instance.player + const resource = await song.getResource() // Remplace par ton fichier audio + + player.play(resource); + instance.connection.subscribe(player); + clog.log(`GUILD : ${instance.guildId} - Lecture de la musique (Media): ${song.title} - Filename : ${song.filename}`) + + } catch(e) { + clog.error("Erreur lors de la lecture de la musique : " + song.title) + clog.error(e) + } + + } module.exports = {play} \ No newline at end of file diff --git a/backend/src/player/Player.js b/backend/src/player/Player.js index ad2e41d..94681e7 100644 --- a/backend/src/player/Player.js +++ b/backend/src/player/Player.js @@ -6,6 +6,7 @@ const plog = new LogType("Player") const clog = new LogType("Signal") const media = require('./Method/Media'); +const Activity = require('../discord/Activity'); const AllPlayers = new Map() @@ -39,33 +40,48 @@ class Player { channelId: channel.id, guildId: channel.guild.id, adapterCreator: channel.guild.voiceAdapterCreator, + selfDeaf: false, + selfMute: false }); this.player = createAudioPlayer() - - this.connection.subscribe(this.player) - + this.generatePlayerEvents() + this.connection.on('stateChange', (oldState, newState) => { clog.log(`GUILD : ${this.guildId} - [STATE] OLD : "${oldState.status}" NEW : "${newState.status}"`); + + // Si la connection est fermée, on détruit le player + + if(newState.status === VoiceConnectionStatus.Disconnected) { + this.leave() + } }); + } + + generatePlayerEvents() { + this.player.on('error', error => { plog.error(`GUILD : ${this.guildId} - Une erreur est survenue dans le player`); plog.error(error); }); - - this.player.on(AudioPlayerStatus.Idle, () => { + + this.player.on(AudioPlayerStatus.Idle, () => { + Activity.idleActivity() + this.queue.setCurrent(null) if(this.queue.next.length > 0) { - //TODO : Play next song - } + this.play(this.queue.nextSong()) + } }); this.player.on(AudioPlayerStatus.Playing, () => { plog.log(`GUILD : ${this.guildId} - Le player est en train de jouer le contenu suivant : ${this.queue.current.title}`); + Activity.setMusicActivity(this.queue.current.title, this.queue.current.author, this.queue.current.thumbnail) + }); - } + checkConnection() { if(this.connection === null) { clog.error(`GUILD : ${this.guildId} - La connection n'est pas définie`) @@ -78,43 +94,81 @@ class Player { } async play(song) { + if(this.checkConnection()) return + if(this.queue.current != null) { + this.player.stop() + } + this.queue.setCurrent(song) + if(song.type = "attachment") { media.play(this, song) } } async add(song) { - if(this.player.state.status = AudioPlayerStatus.Idle && this.queue.current === null && this.queue.next.length === 0) { + if(this.player.state.status == AudioPlayerStatus.Idle && this.queue.current === null && this.queue.next.length === 0) { this.play(song) return } this.queue.addNextSong(song) + plog.log(`GUILD : ${this.guildId} - La musique a été ajoutée à la liste de lecture : ${song.title}`) } async pause() { - if(this.player.state.status = AudioPlayerStatus.Paused) { + if(this.checkConnection()) return "no_music" + if(this.player.state.status == AudioPlayerStatus.Paused) { this.player.unpause() + plog.log(`GUILD : ${this.guildId} - La musique a été reprise`) + return false } else { this.player.pause() + plog.log(`GUILD : ${this.guildId} - La musique a été mise en pause`) + return true } } async leave() { + if(this.checkConnection()) return + if(this.queue.current != null) { + this.queue.addPreviousSong(this.queue.current) + } // Détruit la connection et le player et l'enlève de la liste des this.connection.destroy() this.player.stop() + Activity.idleActivity() + this.queue.destroy() AllPlayers.delete(this.guildId) clog.log("Connection détruite avec le guildId : " + this.guildId) plog.log("Player détruit avec le guildId : " + this.guildId) } + async skip() { + + if(this.checkConnection()) return "no_music" + if(this.queue.next.length === 0) { + return "no_music" + } + const songSkip = this.queue.nextSong() + this.play(songSkip) + return songSkip + } - + async previous() { + + if(this.checkConnection()) return "no_music" + if(this.queue.getPrevious().length === 0) { + return "no_music" + } + + const songPrevious = this.queue.previousSong() + this.play(songPrevious) + return songPrevious + } } -module.exports = {Player} +module.exports = {Player, AllPlayers} /* diff --git a/backend/src/player/Song.js b/backend/src/player/Song.js index 1a5dd8d..6f8f656 100644 --- a/backend/src/player/Song.js +++ b/backend/src/player/Song.js @@ -1,29 +1,43 @@ const {LogType} = require('loguix') const { createAudioResource, StreamType } = require('@discordjs/voice'); -const ffprobe = require('ffprobe'); -const ffprobeStatic = require('ffprobe-static'); -const { getReadableDuration } = require('../utils/TimeConverter'); + const clog = new LogType("Song") +const MediaInformation = require('../media/MediaInformation') class Song { title = "Aucun titre"; + filename = "Aucun fichier"; author = "Auteur inconnu" url; - thumbnail; + thumbnail = "https://radomisol.fr/wp-content/uploads/2016/08/cropped-note-radomisol-musique.png" ; duration; readduration; type; + constructor(properties) { + if(properties) { + this.title = properties.title ?? this.title + this.filename = properties.filename ?? this.filename + this.author = properties.author ?? this.author + this.url = properties.url ?? this.url + this.thumbnail = properties.thumbnail ?? this.thumbnail + this.duration = properties.duration ?? this.duration + this.readduration = properties.readduration ?? this.readduration + this.type = properties.type ?? this.type + } + } + async processMedia(media, provider) { if(provider) this.author = provider // Check if media is a file or a link if(media.attachment) { this.url = media.attachment.url + this.filename = media.attachment.name this.type = "attachment" // In face, duration is null, get the metadata of the file to get the duration - await getMediaInformation(this, media) + await MediaInformation.getMediaInformation(this, media) } else { clog.error("Impossible de traiter le média") @@ -47,31 +61,3 @@ class Song { } module.exports = {Song} - -async function getMediaInformation(instance, media, provider) { - try { - const info = await ffprobe(media.attachment.url, { path: ffprobeStatic.path }); - if (info.streams?.[0]?.duration_ts) { - instance.duration = info.streams[0].duration; - instance.readduration = getReadableDuration(instance.duration) - } - - // Vérification pour éviter une erreur si `streams[0]` ou `tags` n'existe pas - instance.thumbnail = info.streams?.[0]?.tags?.thumbnail ?? - "https://radomisol.fr/wp-content/uploads/2016/08/cropped-note-radomisol-musique.png"; - - // Obtenir le titre (sinon utiliser le nom du fichier) - instance.title = info.streams?.[0]?.tags?.title ?? media.attachment.name; - - // Obtenir l'auteur (s'il existe) - instance.author = info.streams?.[0]?.tags?.artist ?? instance.author; - - - - - } catch (err) { - clog.error("Impossible de récupérer les informations de la musique : " + this.name) - clog.error(err) - return null; - } -} \ No newline at end of file