music-very-player/src/player_manager.js

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 }