const { joinVoiceChannel, getVoiceConnection, VoiceConnectionStatus, createAudioPlayer, AudioPlayerStatus, StreamType, createAudioResource } = require('@discordjs/voice'); const {List} = require('./List') const {LogType} = require("loguix"); const songCheck = require('./SongCheck') const ffmpeg = require('fluent-ffmpeg') const fs = require('fs') const { PassThrough } = require('stream'); const plog = new LogType("Player") const clog = new LogType("Signal") const media = require('./Method/Media'); const youtube = require('./Method/Youtube'); const soundcloud = require('./Method/Soundcloud'); const AllPlayers = new Map() class Player { connection; connected = false; player; guildId; channelId; queue; currentResource; loop = false; constructor(guildId) { if(this.guildId === null) { clog.error("Impossible de créer un Player, car guildId est null") return } if(AllPlayers.has(guildId)) { return AllPlayers.get(guildId) } this.connection = null this.player = null this.guildId = guildId this.queue = new List(guildId) AllPlayers.set(guildId, this) } async join(channel) { if(getVoiceConnection(channel.guild.id)) { clog.log(`GUILD : ${this.guildId} - Une connexion existe déjà pour ce serveur`) return } this.joinChannel(channel) this.player = createAudioPlayer() this.generatePlayerEvents() } isConnected() { return this.connected } joinChannel(channel) { this.channelId = channel.id this.connection = joinVoiceChannel({ channelId: channel.id, guildId: channel.guild.id, adapterCreator: channel.guild.voiceAdapterCreator, selfDeaf: false, selfMute: false }); 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() } }); this.connected = true process.emit("PLAYERS_UPDATE") } generatePlayerEvents() { const Activity = require('../discord/Activity'); this.player.on('error', error => { plog.error(`GUILD : ${this.guildId} - Une erreur est survenue dans le player`); plog.error(error); console.error(error); process.emit("PLAYERS_UPDATE") }); this.player.on(AudioPlayerStatus.Idle, () => { if(this.checkConnection()) return // Si la musique est en boucle, on relance la musique if(this.loop) { this.play(this.queue.current) return } // Si la musique n'est pas en boucle, on passe à la musique suivante Activity.idleActivity() this.queue.setCurrent(null) if(this.queue.next.length > 0) { this.play(this.queue.nextSong()) } process.emit("PLAYERS_UPDATE") }); this.player.on(AudioPlayerStatus.Playing, () => { if(this.checkConnection()) return 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) process.emit("PLAYERS_UPDATE") }); } checkConnection() { if(this.connection === null) { clog.error(`GUILD : ${this.guildId} - La connection n'est pas définie`) return true } if(this.player === null) { plog.error(`GUILD : ${this.guildId} - Le player n'est pas défini`) return true } } getState() { const playerStatus = this.player?.state?.status ?? false; const connectionStatus = this.connection?.state?.status ?? false; const state = { current: this.queue.current, next: this.queue.next, previous: this.queue.previous, loop: this.loop, shuffle: this.queue.shuffle, paused: playerStatus === AudioPlayerStatus.Paused, playing: playerStatus === AudioPlayerStatus.Playing, duration: this.getDuration(), playerState: playerStatus, connectionState: connectionStatus, channelId: this.channelId, guildId: this.guildId, } return state } async setLoop() { if(this.checkConnection()) return this.loop = !this.loop if(this.loop) { plog.log(`GUILD : ${this.guildId} - La musique est en boucle`) } else { plog.log(`GUILD : ${this.guildId} - La musique n'est plus en boucle`) } process.emit("PLAYERS_UPDATE") } async setShuffle() { if(this.checkConnection()) return this.queue.shuffle = !this.queue.shuffle if(this.queue.shuffle) { plog.log(`GUILD : ${this.guildId} - La musique est en mode aléatoire`) } else { plog.log(`GUILD : ${this.guildId} - La musique n'est plus en mode aléatoire`) } process.emit("PLAYERS_UPDATE") } async play(song) { if(!songCheck.checkSong(song)) return if(this.checkConnection()) return if(this.queue.current != null) { this.player.stop() } this.queue.setCurrent(song) this.stream = await this.getStream(song) if(this.stream === null) { plog.error(`GUILD : ${this.guildId} - Impossible de lire la musique : ${song.title} avec le type : ${song.type}`) return } this.playStream(this.stream) plog.log(`GUILD : ${this.guildId} - Lecture de la musique : ${song.title} - Type : ${song.type}`) } async getStream(song) { let stream = null if(song.type == "attachment") { stream = await media.getStream(song) } if(song.type == 'youtube') { stream = await youtube.getStream(song) } if(song.type == "soundcloud") { stream = await soundcloud.getStream(song) } return stream } async add(song) { 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 readPlaylist(playlist, now) { if(this.player?.state?.status == AudioPlayerStatus.Idle && this.queue.current === null && this.queue.next.length === 0) { this.play(playlist.songs[0]) this.queue.addNextPlaylist(playlist, true) return } if(now) this.play(playlist.songs[0]) this.queue.addNextPlaylist(playlist, now) plog.log(`GUILD : ${this.guildId} - La playlist a été ajoutée à la liste de lecture : ${playlist.title}`) } async pause() { 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`) process.emit("PLAYERS_UPDATE") return false } else { this.player.pause() plog.log(`GUILD : ${this.guildId} - La musique a été mise en pause`) process.emit("PLAYERS_UPDATE") return true } } async leave() { const Activity = require('../discord/Activity'); 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() this.player = null this.connection = null this.channelId = null this.connected = false 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) process.emit("PLAYERS_UPDATE") } async setDuration(duration) { //FIXME: SET DURATION FONCTIONNE TRES LENTEMENT if (this.checkConnection()) return; if (this.queue.current == null) return; if (this.currentResource == null) return; const maxDuration = this.queue.current.duration; if (duration > maxDuration) { plog.error(`GUILD : ${this.guildId} - La durée demandée dépasse la durée maximale de la musique.`); return; } this.stream = await this.getStream(this.queue.current); if (this.stream === null) { plog.error(`GUILD : ${this.guildId} - Impossible de lire la musique : ${this.queue.current.title} avec le type : ${this.queue.current.type}`); return; } // Si stream est un lien, ouvrir le stream à partir du lien if(typeof this.stream === "string") { this.stream = fs.createReadStream(this.stream) } const passThroughStream = new PassThrough(); ffmpeg(this.stream) .setStartTime(duration) // Démarrer à la position demandée (en secondes) .outputOptions('-f', 'mp3') // Specify output format if needed .on('error', (err) => { plog.error(`GUILD : ${this.guildId} - Une erreur est survenue avec ffmpeg : ${err.message}`); }) .pipe(passThroughStream, { end: true }); this.stream = passThroughStream; this.playStream(this.stream); // Jouer le nouveau flux this.currentResource.playbackDuration = duration * 1000; // Mettre à jour la durée de lecture du resource plog.log(`GUILD : ${this.guildId} - Lecture déplacée à ${duration}s.`); } playStream(stream) { if(this.checkConnection()) return if(this.player !== null) this.player.stop(); this.player = createAudioPlayer() this.generatePlayerEvents() const resource = createAudioResource(stream, { inputType: StreamType.Arbitrary }); this.setCurrentResource(resource) this.player.play(resource); this.connection.subscribe(this.player); process.emit("PLAYERS_UPDATE") } getDuration() { // Return the duration of player if(this.checkConnection()) return if(this.queue.current == null) return if(this.currentResource == null) return return this.currentResource.playbackDuration / 1000 } setCurrentResource(value) { this.currentResource = value; } changeChannel(channel) { if(this.checkConnection()) return if(this.connection === null) return if(this.connection.channelId === channel.id) return this.connection.destroy() this.joinChannel(channel) // Si la musique est en cours de lecture, on la relance avec le bon timecode if(this.player) { this.connection.subscribe(this.player); } process.emit("PLAYERS_UPDATE") } 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) process.emit("PLAYERS_UPDATE") 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) process.emit("PLAYERS_UPDATE") return songPrevious } } /** * * @param {string} guildId * @returns {Player} player */ function getPlayer(guildId) { if(AllPlayers.has(guildId)) { return AllPlayers.get(guildId) } else { return new Player(guildId) } } function getAllPlayers() { const players = new Array() AllPlayers.forEach((player) => { players.push(player) }) } function isPlayer(guildId) { return AllPlayers.has(guildId) } module.exports = {Player, AllPlayers, getPlayer, isPlayer, getAllPlayers} /* You can access created connections elsewhere in your code without having to track the connections yourself. It is best practice to not track the voice connections yourself as you may forget to clean them up once they are destroyed, leading to memory leaks. const connection = getVoiceConnection(myVoiceChannel.guild.id); */