244 lines
6.5 KiB
JavaScript
244 lines
6.5 KiB
JavaScript
const ytdl = require('ytdl-core')
|
|
|
|
const { Manager } = require('./manager')
|
|
const { Playlist } = require('./playlist');
|
|
const LOOP_VALUES = {
|
|
full: 2,
|
|
all: 2,
|
|
every: 2,
|
|
playlist: 2,
|
|
2: 2,
|
|
track: 1,
|
|
one: 1,
|
|
1: 1,
|
|
none: 0,
|
|
zero: 0,
|
|
0: 0,
|
|
}
|
|
|
|
const LOOP_NAMES = {
|
|
0: 'None',
|
|
1: 'Current track',
|
|
2: 'Full playlist',
|
|
}
|
|
|
|
/**
|
|
* @class
|
|
* @extends Manager
|
|
*/
|
|
class PlayerManager extends Manager {
|
|
#voice
|
|
#dispatcher
|
|
#playlist
|
|
#loop
|
|
#current_stream
|
|
|
|
get_playlist() { return this.#playlist }
|
|
set_playlist(playlist) {
|
|
this.#playlist = playlist
|
|
return this
|
|
}
|
|
|
|
constructor(gmanager, guild_id, config) {
|
|
super(...arguments)
|
|
this.#voice = null
|
|
this.#dispatcher = null
|
|
this.#playlist = new Playlist()
|
|
this.#current_stream = null
|
|
this.#loop = config.default_loop
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
* @description ensure that the current stream is removed
|
|
*/
|
|
#destroyStream() {
|
|
if (this.#current_stream) {
|
|
this.#current_stream.destroy()
|
|
this.#current_stream = null
|
|
}
|
|
}
|
|
|
|
// https://www.youtube.com/watch?v=Yom8nNqmxvQ
|
|
/**
|
|
* Stream the given track from YouTube
|
|
* @param {Track}
|
|
*/
|
|
async stream(track) {
|
|
if (!track) return this
|
|
track.update_play_count()
|
|
const stream = ytdl(track.get_url(), { filter: 'audioonly' })
|
|
this.#dispatcher = this.#voice.play(stream)
|
|
this.#destroyStream()
|
|
this.#current_stream = stream
|
|
this.#dispatcher.on('finish', () => {
|
|
if (this.#loop === 1) {
|
|
this.stream(track)
|
|
} else {
|
|
this.next()
|
|
}
|
|
})
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* @returns true if this is connected to a voice chan
|
|
*/
|
|
is_connected() {
|
|
return Boolean(this.#voice)
|
|
}
|
|
|
|
/**
|
|
* Connects to a voice channel and ensure basic configuration of self (deaf etc.)
|
|
*
|
|
* @param {Discord.VoiceChannel} channel to be joined
|
|
*/
|
|
async connect(channel) {
|
|
this.#voice = await channel.join()
|
|
this.#voice.voice.setSelfDeaf(true)
|
|
}
|
|
|
|
async leave({ message, params }) {
|
|
if (this.#voice) {
|
|
this.#voice.channel.leave()
|
|
this.#destroyStream()
|
|
this.#playlist = new Playlist()
|
|
}
|
|
}
|
|
|
|
async clear({ message, params }) {
|
|
if (this.get_playlist().is_touched()) {
|
|
const confirm_msg = await message.channel.send(":warning: Confirm you want to reset without saving or !register.")
|
|
confirm_msg.react("💾")
|
|
confirm_msg.react("⏏️")
|
|
confirm_msg.awaitReactions(async (r, u) => {
|
|
if (r.emoji.name === "💾" && u.id !== confirm_msg.author.id) {
|
|
const current_playlist = this.get_manager('playlists')
|
|
if (await current_playlist.register({ message, params: [] })) {
|
|
this.#clear_confirmed({ message, params })
|
|
}
|
|
}
|
|
if (r.emoji.name === "⏏️" && u.id !== confirm_msg.author.id) {
|
|
this.#clear_confirmed({ message, params })
|
|
}
|
|
}, { max: 1 })
|
|
}
|
|
}
|
|
|
|
async #clear_confirmed({ message, params }) {
|
|
this.#destroyStream()
|
|
this.#playlist = new Playlist()
|
|
}
|
|
|
|
async shuffle({ message, params }) {
|
|
this.#playlist.shuffle()
|
|
message.channel.send('Shuffled ! :twisted_rightwards_arrows:')
|
|
}
|
|
|
|
/**
|
|
* Change the loop flag to give it 3 possible values: 0 1 2.
|
|
* 0 means no looping
|
|
* 1 means loop the current track
|
|
* 2 means loop the whole track
|
|
*/
|
|
async toggle_loop({ message, params }) {
|
|
if (params[0] === undefined) {
|
|
this.#loop = (this.#loop + 1) % 3
|
|
message.channel.send(`:white_check_mark: Looping set to "${this.get_loop_name()}"`)
|
|
} else if (LOOP_VALUES[params[0]] !== undefined) {
|
|
this.#loop = LOOP_VALUES[params[0]]
|
|
message.channel.send(`:white_check_mark: Looping set to "${this.get_loop_name()}"`)
|
|
} else {
|
|
message.channel.send(`:warning: No such loop as "${params[0]}"`)
|
|
}
|
|
return this
|
|
}
|
|
|
|
get_loop_name() {
|
|
return LOOP_NAMES[this.#loop]
|
|
}
|
|
|
|
async add({ message, params }) {
|
|
if (!message.member?.voice?.channel) {
|
|
message.channel.send('Not connected to voice')
|
|
// cannot connect
|
|
} else {
|
|
await this.connect(message.member.voice.channel)
|
|
if (params[0]) {
|
|
const track = /^https?:\/\//.test(params[0])
|
|
? await this.get_manager('resource').get_metadata(params[0])
|
|
: (await this.get_manager('resource').get_search_results(params))[0]
|
|
if (!track) {
|
|
message.channel.send(':warning: Something went wrong. That\'s not supposed to happen...')
|
|
} else {
|
|
message.react('👌')
|
|
this.#playlist.push(track)
|
|
}
|
|
}
|
|
if (!this.#current_stream) {
|
|
const track = this.#playlist.get_current_track()
|
|
this.stream(track)
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* params[0] is the id of the track to unqueue
|
|
* TODO ensure consistency with index/id displayed...
|
|
*/
|
|
async remove({ message, params }) {
|
|
const id_to_remove = Number(params[0])
|
|
const index_to_remove = id_to_remove - 1;
|
|
if (!isNaN(id_to_remove)) {
|
|
const track = this.#playlist.get_track_by_index(index_to_remove)
|
|
if (track && this.#playlist.delete(index_to_remove)) {
|
|
message.channel.send(`:white_check_mark: The track ${id_to_remove} \`${track.get_full_title()}\` has been removed`)
|
|
} else {
|
|
message.channel.send(`:warning: The track ${id_to_remove} has not been removed or found`)
|
|
}
|
|
}
|
|
}
|
|
|
|
async next() {
|
|
const track = this.#playlist.next({ loop: this.#loop }).get_current_track();
|
|
if (track) {
|
|
await this.stream(track)
|
|
} else {
|
|
this.#destroyStream()
|
|
}
|
|
}
|
|
|
|
async prev() {
|
|
const track = this.#playlist.previous({ loop: this.#loop }).get_current_track();
|
|
if (track) {
|
|
await this.stream(track)
|
|
} else {
|
|
this.#destroyStream()
|
|
}
|
|
}
|
|
|
|
async goto({ message, params }) {
|
|
if (this.#playlist.goto(parseInt(params[0]))) {
|
|
this.stream(this.#playlist.get_current_track())
|
|
message.react('👌')
|
|
} else {
|
|
message.channel.send(`:warning: Could not find track at index \`${params}\`.`)
|
|
}
|
|
}
|
|
|
|
async list({ message, params }) {
|
|
if (this.#playlist.is_empty()) {
|
|
message.channel.send("Empty playlist")
|
|
} else {
|
|
// TODO: check 2000 characters limit
|
|
const string_message = `Current playlist: (${this.#playlist.get_name()}, looping: ${this.get_loop_name()})\n` +
|
|
"```json\n" +
|
|
this.#playlist.map_tracks((t, id, _, current) =>(current === t ? '(*) ' : '') + `${id + 1} ${t}`).join("\n") +
|
|
"```\n";
|
|
message.channel.send(string_message.slice(0, 2000))
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = { PlayerManager }
|