Compare commits

..

No commits in common. "7682aa7aff5f36d4a49a29943bd054355bb624b0" and "97ef16a4c8f01504204ca2ce3b69e86f73abc8e8" have entirely different histories.

8 changed files with 57 additions and 152 deletions

View File

@ -4,8 +4,6 @@
"commands": {
"p": { "target": "player", "action": "add" },
"play": { "target": "player", "action": "add" },
"q": { "target": "player", "action": "add" },
"quick": { "target": "player", "action": "add" },
"remove": { "target": "player", "action": "remove" },
@ -20,7 +18,6 @@
"next": { "target": "player", "action": "next" },
"previous": { "target": "player", "action": "prev" },
"goto": { "target": "player", "action": "goto" },
"list": { "target": "player", "action": "list" },
@ -35,6 +32,9 @@
"mvpconfig": { "target": "mvp", "action": "config" },
"mvpversion" : { "target": "mvp", "action": "version"},
"q": { "target": "resource", "action": "quick_search" },
"quick": { "target": "resource", "action": "quick_search" },
"s": { "target": "resource", "action": "search" },
"search": { "target": "resource", "action": "search" }
}

View File

@ -37,10 +37,10 @@ class GuildsManager {
if (!this.#guilds[guild_id]) {
const current_config = config[message.guild.id] || config['default']
this.#guilds[guild_id] = {
input: new InputManager(this, guild_id, current_config),
player: new PlayerManager(this, guild_id),
resource: new ResourceManager(this, guild_id),
playlists: await new PlaylistsManager(this, guild_id),
input: new InputManager(this, current_config),
player: new PlayerManager(this),
resource: new ResourceManager(this),
playlists: await new PlaylistsManager(this),
}
}
const guild_action = await this.#guilds[guild_id].input.handle_message(message)

View File

@ -1,4 +1,3 @@
const { Manager } = require('./manager')
const { Playlist } = require('./playlist')
const { GuildAction } = require('./guild_action')
@ -24,15 +23,15 @@ const ONE_SECOND = 1000
* are met.
*
* @class
* @extends Manager
*/
class InputManager extends Manager {
class InputManager {
#config
#triggers
#gmanager
constructor(gmanager, guild_id, config) {
super(...arguments)
constructor(gmanager, config) {
this.#config = config
this.#gmanager = gmanager
this.#triggers = []
}

View File

@ -1,28 +0,0 @@
/**
* An abstract class that is meant to be extended by actual managers.<br />
* Managers are classes that have actions, which are used by the {@link GuildsManager} according to the configuration file.
*/
class Manager {
#gmanager
#guild_id
/**
* @param {GuildsManager} gmanager An instance of GuildsManager
* @param {string} guild_id The id of the Discord guild this managers is working on
*/
constructor(gmanager, guild_id) {
this.#gmanager = gmanager
this.#guild_id = guild_id
}
/**
* Retrieves another manager related to the same Discord guild
* @param {"input"|"player"|"playlist"|"resource"} name The name of the manager to retrieve
* @returns {InputManager|PlayerManager|PlaylistsManager|ResourceManager} The associated manager instance
*/
get_manager(name) {
return this.#gmanager.get_manager(this.#guild_id, name)
}
}
module.exports = { Manager }

View File

@ -1,6 +1,5 @@
const ytdl = require('ytdl-core')
const { Manager } = require('./manager')
const { Playlist } = require('./playlist');
const LOOP_VALUES = {
full: 2,
@ -24,14 +23,14 @@ const LOOP_NAMES = {
/**
* @class
* @extends Manager
*/
class PlayerManager extends Manager {
class PlayerManager {
#voice
#dispatcher
#playlist
#gmanager
#is_playing
#loop
#current_stream
get_playlist() { return this.#playlist }
set_playlist(playlist) {
@ -39,26 +38,15 @@ class PlayerManager extends Manager {
return this
}
constructor() {
super(...arguments)
constructor(gmanager) {
this.#gmanager = gmanager
this.#voice = null
this.#dispatcher = null
this.#playlist = new Playlist()
this.#current_stream = null
this.#is_playing = false
this.#loop = 2
}
/**
* @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
@ -67,10 +55,9 @@ class PlayerManager extends Manager {
async stream(track) {
if (!track) return this
track.update_play_count()
this.#is_playing = true
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)
@ -78,21 +65,11 @@ class PlayerManager extends Manager {
this.next()
}
})
return this
return this;
}
/**
* @returns true if this is connected to a voice chan
*/
is_connected() {
return Boolean(this.#voice)
}
is_connected(channel) {}
/**
* 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)
@ -101,7 +78,7 @@ class PlayerManager extends Manager {
async leave({ message, params }) {
if (this.#voice) {
this.#voice.channel.leave()
this.#destroyStream()
this.#is_playing = false
this.#playlist = new Playlist()
}
}
@ -113,7 +90,7 @@ class PlayerManager extends Manager {
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')
const current_playlist = this.#gmanager.get_manager(message, 'playlists')
if (await current_playlist.register({ message, params: [] })) {
this.#clear_confirmed({ message, params })
}
@ -126,7 +103,7 @@ class PlayerManager extends Manager {
}
async #clear_confirmed({ message, params }) {
this.#destroyStream()
this.#is_playing = false
this.#playlist = new Playlist()
}
@ -165,17 +142,14 @@ class PlayerManager extends Manager {
} 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...')
if (!/^http/.test(params[0])) {
message.channel.send(':warning: !play only supports url now, please use !quick for quick search')
} else {
message.react('👌')
const track = await this.#gmanager.get_manager(message.guild.id, 'resource').get_metadata(params[0])
this.#playlist.push(track)
}
}
if (!this.#current_stream) {
if (!this.#is_playing) {
const track = this.#playlist.get_current_track()
this.stream(track)
}
@ -192,7 +166,7 @@ class PlayerManager extends Manager {
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`)
message.channel.send(`:white_check_mark: The track ${id_to_remove} \`${track.full_title}\` has been removed`)
} else {
message.channel.send(`:warning: The track ${id_to_remove} has not been removed or found`)
}
@ -202,27 +176,18 @@ class PlayerManager extends Manager {
async next() {
const track = this.#playlist.next({ loop: this.#loop }).get_current_track();
if (track) {
await this.stream(track)
this.stream(track)
} else {
this.#destroyStream()
this.#is_playing = false;
}
}
async prev() {
const track = this.#playlist.previous({ loop: this.#loop }).get_current_track();
if (track) {
await this.stream(track)
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}\`.`)
this.#is_playing = false;
}
}

View File

@ -84,25 +84,10 @@ class Playlist {
return this
}
goto(idx) {
if (this.#tracks[idx - 1] == null) {
return false
}
this.#current_index = idx - 1
return true
}
/**
* @returns {Track|null} The track that is currenly played, or <code>null</code> if it doesn't exist
*/
get_current_track() {
return this.#tracks[this.#current_index] ?? null
}
/**
* @param {number} track_index The index of the track to be retrieved
* @returns {Track|null} The track that is looked for, or <code>null</code> if it doesn't exist
*/
get_track_by_index(track_index) {
return this.#tracks[track_index] ?? null
}
@ -126,12 +111,7 @@ class Playlist {
* @returns {boolean} <code>true</code> if the element was successfully removed, <code>false</code> otherwise
*/
delete(track_index) {
if (track_index >= this.#tracks.length) {
return false
}
if (track_index < this.#current_index) {
this.#current_index -= 1
}
if (track_index >= this.#tracks.length) return false
delete this.#tracks[track_index]
this.#tracks = this.#tracks.filter(Boolean)
this.#touched = true

View File

@ -1,26 +1,23 @@
const fs = require('fs').promises
const ytdl = require('ytdl-core')
const { Manager } = require('./manager')
const { Playlist } = require('./playlist');
const { youtube_instance } = require('../src/youtube');
/**
* @extends Manager
*/
class PlaylistsManager extends Manager {
class PlaylistsManager {
#playlists
#gmanager
#filepath
constructor(gmanager, guild_id, { filepath = "playlists_manager.db", data = null } = {}) {
constructor(gmanager, { filepath = "playlists_manager.db", data = null } = {}) {
return (async () => {
super(...arguments)
const playlists = data || await PlaylistsManager.readFile(filepath)
Object.entries(playlists).forEach(([title, tracks]) => {
playlists[title] = new Playlist({ tracks, name: title })
})
this.#playlists = playlists
this.#filepath = filepath
this.#gmanager = gmanager
return this
})()
@ -40,7 +37,7 @@ class PlaylistsManager extends Manager {
}
async register({ message, params }) {
const player = this.get_manager('player')
const player = this.#gmanager.get_manager(message, 'player')
if (params[0] || player.get_playlist().is_named()) {
const playlist_title = params[0] || player.get_playlist().get_name()
this.#playlists[playlist_title] = player.get_playlist().clone()
@ -55,7 +52,7 @@ class PlaylistsManager extends Manager {
}
async load({ message, params }) {
const player = this.get_manager('player')
const player = this.#gmanager.get_manager(message, 'player')
const playlist_title = params[0] // TODO handle long names with spaces...
const named_playlist = { title: playlist_title, playlist: this.#playlists[playlist_title] }
// const playlist_id = params[0]

View File

@ -1,38 +1,26 @@
const { Manager } = require('./manager')
const { GuildAction } = require('./guild_action')
const { Track } = require('./track')
const { youtube_instance } = require("./youtube")
/**
* The {@link Manager} that manage resources. Its main goal is to search resources and get their metadata.<br />
* Note that it only works for YouTube videos for now.
* @extends Manager
* @class
*/
class ResourceManager extends Manager {
class ResourceManager {
#gmanager
#awaiting_answers
constructor() {
super(...arguments)
constructor(gmanager) {
this.#gmanager = gmanager
this.#awaiting_answers = []
}
/**
* Performs a research, depending on the search terms it's been asked for
* @param {string|string[]} query The search terms
* @returns {Track[]} An array of objects representing the search results.
*/
async get_search_results(query) {
if (query instanceof Array)
query = query.join(' ')
return await youtube_instance.search_track(query)
}
/**
* Do a research and send the search results back to the Discord channel it's been asked for
* @param {GuildAction}
* @return {Track[]} An array of objects representing the search results.
*/
async search({ message, params }) {
const tracks = await this.get_search_results(params)
const tracks = await youtube_instance.search_track(params.join(' '))
if (tracks.length === 0) {
message.channel.send(':warning: Something went wrong. Please try again')
} else {
@ -40,7 +28,7 @@ class ResourceManager extends Manager {
.slice(0, 10)
.map((track, i) => (i + 1) + ' ' + track)
.join('\n')
const input_manager = this.get_manager('input')
const input_manager = this.#gmanager.get_manager(message.guild.id, 'input')
// TODO: 2000 chars limit
// TODO: find a better syntaxic coloration than js
const own_message = await message.channel.send('```js\n' + text_res + '```')
@ -62,12 +50,9 @@ class ResourceManager extends Manager {
})
input_manager.add_trigger(trigger)
}
return tracks
}
/**
* Choose a track given its id in a search result. Note that this only works right after a call to the {@link ResourceManager#search} action.
* @param {GuildAction}
*/
async search_confirm({ message, params }) {
// TODO: update this so that it can be more relient on the Trigger received
const id = parseInt(message.content)
@ -81,13 +66,20 @@ class ResourceManager extends Manager {
} else {
// TODO: calling the add method is a bit ugly, we can probably do better...
const action = new GuildAction({ message, params: [answer.search_results[id - 1].get_url()] })
this.get_manager('player').add(action)
this.#gmanager.get_manager(message.guild.id, 'player').add(action)
message.react('👌')
}
}
async quick_search({ message, params }) {
const tracks = await youtube_instance.search_track(params.join(' '))
const action = new GuildAction({ message, params: [tracks[0].get_url()] })
this.#gmanager.get_manager(message.guild.id, 'player').add(action)
}
/**
* Given the url of a song, get its metadata.
* Given the url of a song, get is metadata. <br />
* Note that it only works for YouTube videos for now.
* @param {string} url
* @returns {Track}
*/