415 lines
13 KiB
JavaScript
415 lines
13 KiB
JavaScript
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);
|
|
|
|
*/ |