From bf46494f2f0d0a25b4bb04309f351d4d82703e32 Mon Sep 17 00:00:00 2001 From: 1Lucas1apk <76886832+1Lucas1apk@users.noreply.github.com> Date: Thu, 24 Nov 2022 19:35:52 +0000 Subject: [PATCH] Add files via upload --- @Moonlink/MoonlinkManager.js | 367 +++++++++++++++++++++++++++++++++++ @Moonlink/MoonlinkNodes.js | 352 +++++++++++++++++++++++++++++++++ @Moonlink/MoonlinkPlayer.js | 255 ++++++++++++++++++++++++ @Rest/MakeRequest.js | 39 ++++ @Rest/MoonlinkQueue.js | 53 +++++ @Rest/MoonlinkTrack.js | 19 ++ @Rest/MoonlinkUtils.js | 54 ++++++ @Rest/database.json | 1 + 8 files changed, 1140 insertions(+) create mode 100644 @Moonlink/MoonlinkManager.js create mode 100644 @Moonlink/MoonlinkNodes.js create mode 100644 @Moonlink/MoonlinkPlayer.js create mode 100644 @Rest/MakeRequest.js create mode 100644 @Rest/MoonlinkQueue.js create mode 100644 @Rest/MoonlinkTrack.js create mode 100644 @Rest/MoonlinkUtils.js create mode 100644 @Rest/database.json diff --git a/@Moonlink/MoonlinkManager.js b/@Moonlink/MoonlinkManager.js new file mode 100644 index 00000000..e688dda2 --- /dev/null +++ b/@Moonlink/MoonlinkManager.js @@ -0,0 +1,367 @@ +var { EventEmitter } = require('events') +var WebSocket = require('ws') +var utils = require('../@Rest/MoonlinkUtils.js') +var Nodes = require('./MoonlinkNodes.js') +var manager; +class MoonlinkManager extends EventEmitter { + #reconnectAtattempts = 0; + #retryAmount = 5; + #retryTime = 300000; + #TokenSpotify = 'anonymous'; + #request = utils.request; + #on = false + #ws; + #options; + #clientId; + #sPayload; + #nodes; + constructor(lavalinks, options, sPayload) { + + super(); + if (!lavalinks) throw new Error('[ Moonlink.js ]: Options is empty') + if (lavalinks && !Array.isArray(lavalinks)) throw new Error('[ Moonlink.js ]: Option "nodes" must be in an array.') + if (lavalinks.length === 0) throw new Error('[ Moonlink.js ]: Parament of "nodes" must contain an object') + if (typeof options.shard !== "number" && typeof options.shard !== 'undefined') throw new TypeError('[ Moonlink.js ]: Option "shards" must be in number. ') + if (typeof options.clientName !== 'undefined' && typeof options.clientName !== 'string') throw new TypeError('[ Moonlink.js ]: The "clientname" option must be in string.') + if (typeof sPayload !== 'function') throw new TypeError('[ MoonLink ]: The "send" option must be a function') + if(sPayload) utils.esdw(sPayload) + this.#sPayload = sPayload; + this.#nodes = lavalinks; + this.#options = options; + this.nodes; + + this.sendWs; + manager = this + } + init(clientId) { + if (!clientId) throw new TypeError('[ MoonLink ]: "clientId" option is required.') + this.nodes = new Nodes(this, this.#nodes, this.#options, this.#sPayload, clientId) + this.nodes.init() + + + this.sendWs = (json) => { + return this.nodes.sendWs(json) + } + this.#clientId = clientId + } + + updateVoiceState(packet) { + var map = utils.map + if (packet.t == 'VOICE_SERVER_UPDATE') { + let voiceServer = {} + voiceServer[packet.d.guild_id] = { + event: packet.d + } + map.set('voiceServer', voiceServer) + return this.#attemptConnection(packet.d.guild_id) + } + if (packet.t == 'VOICE_STATE_UPDATE') { + if (packet.d.user_id !== this.#clientId) return; + if (packet.d.channel_id) { + let voiceStates = {} + voiceStates[packet.d.guild_id] = packet.d + map.set('voiceStates', voiceStates) + return this.#attemptConnection(packet.d.guild_id) + } + } + } + request(node, endpoint, params) { + return utils.makeRequest(`http://${node.host}${node.port ? `:${node.port}` : ``}/${endpoint}?${params}`, 'GET' ,{ + headers: { + Authorization: node.password + } + }) + + } + async search(options, source) { + return new Promise(async(resolve) => { + if (!options) throw new Error('[ MoonLink.Js ]: the search option has to be in string format or in an array') + if(source && typeof source !== 'string') throw new Error('[ moonlink.js ]: the source option has to be in string format') + if (typeof options !== 'string' && typeof options !== 'object' ) throw new Error('[ MoonLink.Js ]: (search) the search option has to be in string or array format') + if(typeof options.query !== 'undefined' && typeof options.query !== 'string') throw new Error('[ moonlink.js ]: query has to be in string format') + if(typeof options.source !== 'undefined' && typeof options.source !== 'string') throw new Error('[ moonlink.js ]: this option has to be in string format') + let db = utils.db + var spotifyApi = 'https://api.spotify.com/v1/' + let { MoonTrack } = require('../@Rest/MoonlinkTrack.js') + if (typeof options !== 'undefined' && !options.startsWith('https://') && !options.startsWith('http://') && typeof source === 'undefined') { + options = `ytsearch:${options}`; + } + if(typeof options !== 'undefined' && !options.startsWith('https://') && !options.startsWith('http://') && typeof source !== 'undefined') { + options = `${source}:${options}` + } + if(typeof options === 'object' && typeof options.query !== 'undefined' && !options.query.startsWith('https://') && !options.query.startsWith('http://') && typeof options.source === 'undefined') { + options = `ytsearch:${options.query}`; + } + if(typeof options === 'object' && typeof options.query !== 'undefined' && !options.query.startsWith('https://') && !options.query.startsWith('http://') && typeof options.source !== 'undefined') { + options = `${options.source}:${options.query}`; + } + + if(typeof options === 'object' && typeof options.query === 'string' && /(?:https:\/\/open\.spotify\.com\/|spotify:)(?:.+)?(track|playlist|artist|episode|show|album)[/:]([A-Za-z0-9]+)/.test(options.query) || typeof options === 'string' && /(?:https:\/\/open\.spotify\.com\/|spotify:)(?:.+)?(track|playlist|artist|episode|show|album)[/:]([A-Za-z0-9]+)/.test(options)) { + + if(typeof options === 'object' && typeof options.query === 'string') options = options.query + let track = /(?:https:\/\/open\.spotify\.com\/|spotify:)(?:.+)?(track|playlist|artist|episode|show|album)[/:]([A-Za-z0-9]+)/.exec(options) + let url = ''; + switch(track[1]) { + case 'track': + url = spotifyApi + `tracks/${track[2]}` + break; + case 'album': + url = spotifyApi + `albums/${track[2]}` + break; + case 'show': + url = spotifyApi + `shows/${track[2]}` + break; + case 'episodes': + url = spotifyApi + `episodes/${track[2]}` + break; + case 'playlist': + url = spotifyApi + `playlists/${track[2]}?market=ES` + break; + default: + return resolve({ loadType: 'NO_MATCHES', playlistInfo: {}, tracks: [] }) + } + let req = await this.#spotifyRequest(url) + + if(track[1] === 'track') { + if (req.error?.status == 400) return resolve({ loadType: 'NO_MATCHES', playlistInfo: {}, tracks: [] }) + let pes = await this.search(`${req.name} ${req.artists[0].name}`) + if (pes.loadType != 'SEARCH_RESULT') return pes + return resolve({ + loadType: pes.loadType, + playlistInfo: pes.playlistInfo, + tracks: [{...pes.tracks[0], title: req.name, author: req.artists.map(artist => artist.name).join(', '), thumbnail: req.album.images[0].url, length: req.duration_ms, url: req.external_urls.spotify, sourceName: 'Spotify' }] + }) + } + if (track[1] == 'episode') { + + if (req.error?.status == 400) return resolve({ loadType: 'NO_MATCHES', playlistInfo: {}, tracks: [] }) + let pes = await this.search(`${req.name} ${req.publisher}`) + if (pes.loadType != 'SEARCH_RESULT') return resolve(pes) + return resolve({ + loadType: pes.loadType, + playlistInfo: pes.playlistInfo, + tracks: [{...pes.tracks[0], title: req.name, author: null, thumbnail: req.images[0].url, length: req.duration_ms, url: req.external_urls.spotify, sourceName: 'Spotify' }] + }) + } + if (track[1] == 'playlist' || track[1] == 'album') { + + if (req.error?.status == 400) return { loadType: 'NO_MATCHES', playlistInfo: {}, tracks: [] } + if (req.error) return resolve({ loadType: 'LOAD_FAILED', playlistInfo: {}, tracks: [], exception: { message: req.error.message, severity: 'UNKNOWN' } }) + + + let res = { loadType: 'PLAYLIST_LOADED', playlistInfo: { selectedTrack: -1, name: req.name }, tracks: [] } + var i = 0; + req.tracks.items.forEach(async(x, y) => { + let tracks + if (track[1] === 'playlist') tracks = await this.search(`${x.track.name} ${x.track.artists[0].name}`) + else tracks = await this.search(`${x.name} ${x.publisher}`) + + if (tracks.loadType !== 'SEARCH_RESULT') { + + if (y === x.tracks.items.length) return resolve(tracks) + + return; + + } + if (track[1] == 'playlist') tracks = { ...tracks.tracks[0], position: y ,thumbnail: req.images[0].url, title: x.track.name, author: x.track.artists.map(artist => artist.name).join(', '), length: x.track.duration_ms, url: x.track.external_urls.spotify, source: 'Spotify' } + else tracks = { ...tracks.tracks[0], position: i ,thumbnail: req.images[0].url, title: x.name, author: x.artists.map(artist => artist.name).join(', '), length: x.duration_ms, url: x.external_urls.spotify, source: 'Spotify' } + + i++ + res.tracks.push(tracks) + + + if (res.tracks.length === req.tracks.items.length) { + + res.tracks.sort((a, b) => a.position - b.position) + i = 0 + + return resolve(res) + + } + }) + } + if (track[1] == 'show') { + + if (req.error?.status == 400) return { loadType: 'NO_MATCHES', playlistInfo: {}, tracks: [] } + if (req.error) return resolve({ loadType: 'LOAD_FAILED', playlistInfo: {}, tracks: [], exception: { message: req.error.message, severity: 'UNKNOWN' } }) + + + let res = { loadType: 'PLAYLIST_LOADED', playlistInfo: { selectedTrack: -1, name: req.name }, tracks: [] } + var i = 0; + req.tracks.items.forEach(async(x, y) => { + let tracks = await this.search(`${x.name} ${x.publisher}`) + + if (tracks.loadType !== 'SEARCH_RESULT') { + + if (y === x.episodes.items.length) return resolve(tracks) + + return; + + } + if (track[1] == 'playlist') tracks = { ...tracks.tracks[0], position: i ,thumbnail: req.images[0].url, title: x.name, author: req.publisher, length: x.duration_ms, url: x.external_urls.spotify, source: 'Spotify' } + + i++ + res.tracks.push(tracks) + + + if (res.tracks.length === req.episodes.items.length) { + + res.tracks.sort((a, b) => a.position - b.position) + i = 0 + + return resolve(res) + + } + }) + } + } else { + + let params = new URLSearchParams({ identifier: options }) + let res = await this.request(this.nodes.idealNode().node, 'loadtracks', params) + this.emit('debug', '[ MoonLink.Js ]: searching songs') + if (res.loadType === 'LOAD_FAILED' || res.loadType === 'NO_MATCHES') { + return resolve(res) + } else { + const tracks = res.tracks.map(x => new MoonTrack(x)); + if (res.loadType === 'PLAYLIST_LOADED') { + res.playlistInfo.duration = tracks.reduce((acc, cur) => acc + cur.duration, 0); + } + return resolve({ + ...res + , tracks + }) + } + } + }) + } + + + get players() { + let map = utils.map + let { MoonPlayer } = require('../@Moonlink/MoonlinkPlayer.js') + let get = function (guild) { + if (typeof guild !== 'number' && typeof guild !== 'string') { + throw new TypeError('[ MOONLINK ] guild id support only numbers in string!') + } + const { MoonPlayer } = require('../@Moonlink/MoonlinkPlayer.js') + return (new MoonPlayer(map.get('players')[guild], manager)) + } + let create = function (t) { + if(typeof t.guildId !== 'string' && typeof t.guildId !== 'number') { + throw new TypeError('[ MOONLINK ]: guild id support only numbers in string!') + } + if (typeof t.voiceChannel !== 'string' && typeof t.guildId !== 'number') { + throw new TypeError('[ MOONLINK ]: voice channel id support only numbers in string!') + } + if (typeof t.textChannel !== 'string' && typeof t.guildId !== 'number') { + throw new TypeError('[ MOONLINK ]: text channel id support only numbers in string!') + } + + let players = map.get('players') || {} + if(!players[t.guildId]) { + players[t.guildId] = { + guildId: t.guildId + , voiceChannel: String(t.voiceChannel) + , textChannel: String(t.textChannel) + , playing: false + , paused: false + , loop: false + , connected: false + } + map.set('players', players) + + } + let { MoonPlayer } = require('../@Moonlink/MoonlinkPlayer.js') + return (new MoonPlayer(players[t.guildId], manager)) + } + let all = function () { + let players = map.get('players') || null + if (!players) { + return null + } else { + return players + } + } + let has = function(guild) { + let player = map.get('players') || [] + if(typeof guild !== 'string' && isNaN(guild)) { + throw new TypeError(`[ MoonLinkJs ]: ${guild} a number string was expected`) + } + if(player[guild]) player = true + else player = false + return player + } + function edit(info) { + let player = map.get('players') || [] + if(!info) { + throw new TypeError(`[ MoonlinkJs ]: enter a term to edit your player.`) + } + if(!player[info.guildId]) { throw new TypeError(`[ MoonLinkJs ]: cannot edit a player on guild ${info.guildId}.`)} + if (typeof info.guildId !== 'number' && typeof info.guildId !== 'string') { + throw new TypeError('[ MOONLINK ]: guild id support only numbers in string!') + } + if (typeof info.voiceChannel !== 'number' && typeof info.voiceChannel !== 'string') { + throw new TypeError('[ MOONLINK ]: voice channel id support only numbers in string!') + } + if (typeof info.textChannel !== 'number' && typeof info.textChannel !== 'string') { + throw new TypeError('[ MOONLINK ]: text channel id support only numbers in string!') + } + player[info.guildId] = { + guildId: info.guildId + , voiceChannel: info.voiceChannel + , textChannel: info.textChannel + , playing: false + , paused: false + , loop: false + } + map.set('players', player) + return (new MoonPlayer(player[info.guildId], manager)) + + } + return { + get + , create + , all + , has + , edit + + } + } + //---------------------// + + + #attemptConnection(guildId) { + let map = utils.map + let voiceServer = map.get('voiceServer') || {} + let voiceStates = map.get('voiceStates') || {} + let players = map.get('players') || {} + if (!players[guildId]) return false + if (!voiceServer[guildId]) return false + this.nodes.sendWs({ + op: 'voiceUpdate' + , sessionId: voiceStates[guildId].session_id + , guildId: voiceServer[guildId].event.guild_id + , event: voiceServer[guildId].event + }) + return true + + } + + async #spotifyRequest(url) { + let req = await utils.makeRequest(url, 'GET', { headers: { Authorization: ` Bearer ${this.#TokenSpotify}`}}) + if (req.error?.status == 401) { + await utils.makeRequest('https://open.spotify.com/get_access_token', 'GET', { +headers: {} +}).then(async(data) => { + + this.#TokenSpotify = data.accessToken + let r = await this.#spotifyRequest(url) + + req = r + + }) + } + return req + } +} +module.exports = { MoonlinkManager } \ No newline at end of file diff --git a/@Moonlink/MoonlinkNodes.js b/@Moonlink/MoonlinkNodes.js new file mode 100644 index 00000000..90e858ed --- /dev/null +++ b/@Moonlink/MoonlinkNodes.js @@ -0,0 +1,352 @@ +const Nodes = []; +var idealNode = null; +const utils = require('../@Rest/MoonlinkUtils.js') +const WebSocket = require('ws') +class MoonlinkNodes { + #on = false; + constructor(MoonlinkManager, nodes, options, sPayload, clientId) { + + this.ws; + this.manager = MoonlinkManager + this.nodes = nodes + this.options = options + this.sPayload = sPayload + this.clientId = clientId + this.stats = { + players: 0, + playingPlayers: 0, + uptime: 0, + memory: { + free: 0, + used: 0, + allocated: 0, + reservable: 0, + }, + cpu: { + cores: 0, + systemLoad: 0, + lavalinkLoad: 0, + }, + frameStats: { + sent: 0, + nulled: 0, + deficit: 0, + }, + }; + this.retryTime = 300000; + this.reconnectAtattempts = 0; + this.retryAmount = 5; + + } + init() { + this.manager.emit('debug', '[ Moonlink.js ]: connection process started') + this.create(this.nodes, this.clientId) + + } + idealNode() { + if (!idealNode) { + let node = Object.values(Nodes) + .filter((x) => x.ws._readyState === 1) + .sort((b, a) => a.stats.cpu ? (a.stats.cpu.systemLoad / a.stats.cpu.cores) * 100 : 0 - b.stats.cpu ? (b.stats.cpu.systemLoad / b.stats.cpu.cores) * 100 : 0)[0] + if (!node) throw new Error('there are no nodes online!') + if (node) idealNode = node + } + if (idealNode) return idealNode + } + sendWs(json) { + let send = { + error: false + , message: '[ Moonlink.Js ]: MoonLink just sent a request to lavalink' + } +this.idealNode() +this.idealNode().ws.send(JSON.stringify(json), (error) => { + if (error) { + send = { + error: true + , message: error + } + } + }) + this.manager.emit('debug', send.message) + + } + create(options, clientId) { + let db = utils.db + if(!this.#on) { + this.#on = true + db.delete('queue') + } + options.forEach((no) => { + if (typeof no.host !== 'undefined' && typeof no.host !== 'string') throw new TypeError('[ MoonLink ]: Option "host" must be empty string, for localhost leave empty') + if (typeof no.password !== 'undefined' && typeof no.password !== 'string') throw new TypeError("[ MoonLink.Js ]: The password option must be in string, if you don't have a password, leave it empty") + if (no.port && typeof no.port !== 'number' || no.port > 65535 || no.port < 0) throw new TypeError('[ MoonLink ]: The "port" option must be a ') + if(!this.options.clientName) this.options.clientName = `MoonLink/${require('./../package.json').version}` + if(Nodes && Nodes[`${no.host ? no.host : 'localhost'}${no.port ? ':' + no.port : ':443' }`] && Nodes[`${no.host ? no.host : 'localhost'}${no.port ? ':' + no.port : ':443' }`].connected) return; + this.ws = new WebSocket(`ws${no.secure ? 's' : '' }://${no.host ? no.host : 'localhost'}${no.port ? `:${no.port}` : ':443'}`, undefined, { + headers: { + Authorization: no.password ? no.password : '' + , 'Num-Shards': this.options.shards + , 'User-Id': clientId + , 'Client-Name': this.options.clientName + }}) + this.ManagerNodes(no, this.ws) + this.ws.on('open', () => { + this.manager.emit('nodeCreate', no) + this.manager.emit('debug', `${no.host ? no.host : 'localhost'}${no.port ? ':' + no.port : ':443'} is online, and has also been connected`) + }) + this.ws.on('close', (code, reason) => { + this.manager.emit('nodeClose', (no, code, reason)) + this.manager.emit('debug', `${no.host ? no.host : 'localhost'}${no.port ? ':' + no.port : ':443'} has been shut down or restarted, the connection to it has been closed`) + if (code !== 1000 || reason !== "destroy") this.reconnect(no); + }) + this.ws.on('message', async(received) => { + const data = JSON.parse(received) + this.manager.emit('nodeRaw', data) + if (data.op && data.op == 'stats') { + Nodes[`${no.host ? no.host : 'localhost'}${no.port ? ':' + no.port : ':443'}`].stats = data + } + switch (data.op) { + case 'playerUpdate': { + let track = utils.track.current() + let infoUpdate = { ...track, thumbnail: track?.thumbnail, position: data.state.position } + utils.track.editCurrent(infoUpdate) +this.manager.emit('playerUpdate', data) + break + } + case 'event': + switch (data.type) { + case 'TrackStartEvent': { + let map = utils.map + let players = map.get('players') || {} + this.manager.emit('trackStart', players[data.guildId], utils.track.current()) + break + } + case 'TrackEndEvent': { + let db = utils.db + let track = utils.track.current() + let map = utils.map + let players = map.get('players') || {} + let queue = db.get('queue.' + data.guildId) + if (utils.track.skip()) { + + utils.track.skipEdit(false) + return; + } + this.manager.emit('trackEnd', players[data.guildId], track) + if(!players[data.guildId]) return; + if (players[data.guildId].loop === 1) { + if(!utils.track.current()) { + throw new TypeError(`[ MoonLinkJs ]: cannot loop a music with queue is empty!`) } else { + + return this.sendWs({ + op: 'play' + , track: utils.track.current() + .track + , guildId: data.guildId + }) + } + } + if (players[data.guildId].loop > 1) { + if(queue.length < 2) { + throw new TypeError(`[ MoonLinkJs ]: cannot loop queue, no have a second track to loop, try loop a music.`) + } else { +let queue = utils.db.get(`queue.${data.guildId}`) +let trackshifted = queue.shift() +if(!trackshifted) throw new TypeError(`[ MoonlinkJs ]: queue no has track!`) +queue.push(utils.track.current()) +utils.track.editCurrent(trackshifted) +return this.sendWs({ + op: 'play', + guildId: data.guildId, + track: utils.track.current().track, +}); + + } + } + + if (!queue) { + this.manager.emit('debug', '[ MoonLink.Js ]: The queue is empty') + this.manager.emit('queueEnd', '[ Moonlink.js ]: the queue is empty') + utils.track.editCurrent(null) + players[data.guildId] = { + ...players[data.guildId] + , playing: false + } + map.set('players', players) + db.delete(`queue.${data.guildId}`) + } + if (queue[0]) { + let actualtrack = queue.shift() + utils.track.editCurrent(actualtrack) + db.set(`queue.${data.guildId}`, queue) + + this.sendWs({ + op: 'play' + , track: utils.track.current() + .track + , guildId: data.guildId + }) + } else if (typeof queue[0] == 'undefined') { + const players = map.get('players') || {} + players[data.guildId] = { + ...players[data.guildId] + , playing: false + , loop: undefined + + } + map.set('players', players) + db.delete(`queue.${data.guildId}`) + utils.track.editCurrent(null) + } + break; + } + case 'TrackExceptionEvent': { + + +var map = utils.map + delete data.op + delete data.type + db.delete(`queue.${data.guildId}`) + delete map.get('players')[data.guildId] + this.manager.emit('trackException', data) + + this.manager.emit('warn', '[ MoonLink.Js ]: It looks like something when trying to play the track, this is not caused by a MoonLink bug, check your lavalink console or report to lavalink.') + break + } + case 'TrackStuckEvent': { + delete data.op + delete data.type + this.manager.emit('warn', `[ MoonLink.Js ]: it looks like the track got stuck, this is not caused by a MoonLink bug, if continues report to lavalink.`) + let track = utils.track.current() + + let db = utils.db + this.manager.emit('trackEnd', track) + let queue = db.get('queue.' + data.guildId) + let players = map.get('players') || {} + if (players[data.guildId].loop == true) { + + this.sendWs({ + op: 'play' + , track: utils.track.current() + .track + , guildId: data.guildId + }) + } + if (!queue) { + emit.emit('debug', '[ MoonLink.Js ]: The queue is empty') + utils.track.editCurrent(null) + players[data.guildId] = { + ...players[data.guildId] + , playing: false + } + map.set('players', players) + db.delete(`queue.${data.guildId}`) + } + if (queue[0]) { + let actualtrack = queue.shift() + utils.track.editCurrent(actualtrack) + db.set(`queue.${data.guildId}`, queue) + sendWs({ + op: 'play' + , track: utils.track.current() + .track + , guildId: data.guildId + }) + } else if (typeof queue[0] == 'undefined') { + const players = map.get('players') || {} + players[data.guildId] = { + ...players[data.guildId] + , playing: false + } + map.set('players', players) + db.delete(`queue.${data.guildId}`) + utils.track.editCurrent(null) + } + break + } + case 'WebSocketClosedEvent': { + delete data.op + delete data.type + if (data.reason == 'Your session is no longer valid.') { + this.manager.emit('warn', 'your session is no longer valid, check the raw event if everything is ok, also better activate the debug provided by your library.') + this.emit('websocketClosed', data.reason) + } + break + } + default: { + + this.manager.emit('unknowTypeEmitted', `Moon Detected this type: "${data.type || 'no type received :('}" an unknow type that we detect.`) + } + } + break + default: { + + this.manager.emit('unknowOpEmitted', `Moon Detected this op: "${data.op || 'no op received :('}" an unknow op that we detect.`) + } + + } + }) + + + this.ws.on('error', (err) => { + this.manager.emit('nodeError', (no, err)) + this.manager.emit('debug', `[ ${no.host ? no.host : 'localhost'}${no.port ? ':' + no.port : ':443'} ]: Error: ${err} `) + + }) + + }) + } + ManagerNodes(node, ws) { + this.manager.emit('debug', '[ Moonlink.js ]: a node is being configured') + Nodes[`${node.host ? node.host : 'localhost'}${node.port ? ':' + node.port : ':443' }`] = { + ws: this.ws, + stats: this.stats, + connected: true, + calls: 0, + node: node + } + + } + reconnect(node) { + if (this.reconnectAtattempts >= this.retryAmount) { + this.manager.emit('debug', '[ MoonLink.Js ]: unable to reconnect the node, and we have reached the reconnection limit, please check that this node is online, or verify that the information is correct') + let isNode = Nodes[`${node.host ? node.host : 'localhost'}${node.port ? ':' + node.port : ':443' }`] +isNode.ws.close(1000, "destroy") +isNode.ws.removeAllListeners() + delete Nodes[`${node.host ? node.host : 'localhost'}${node.port ? ':' + isNode.port : ':443' }`] + } else { + setTimeout(() => { +Node[`${node.host ? node.host : 'localhost'}${node.port ? ':' + node.port : ':443' }`].ws.removeAllListeners() +Node[`${node.host ? node.host : 'localhost'}${node.port ? ':' + node.port : ':443' }`].connected = false; +this.emit('nodeReconnect', node) +this.create([node], this.clientId) +this.manager.emit('debug', '[ MoonLink.Js ]: Trying to reconnect node, attempted number ' + this.reconnectAtattempts) + this.reconnectAtattempts++ + }, this.retryTime) + } + + } + + get(identify) { + if(identify) return undefined + let node; + if(typeof identify == 'Number') { + node = Object.values(Nodes) + } else { + if(Array.isArray(identify)) { + if(!identify.host || !identify.port) return undefined + if(!Nodes[`${identify.host ? identify.host : 'localhost'}${identify.port ? ':' + identify.port : ':443'}`]) return undefined + node = Nodes[`${identify.host ? identify.host : 'localhost'}${identify.port ? ':' + identify.port : ':443'}`] + } + + } + return node ? node : undefined; + } + + get size() { + if(!Nodes) return 0 + return Nodes.length + } + } + +module.exports = MoonlinkNodes \ No newline at end of file diff --git a/@Moonlink/MoonlinkPlayer.js b/@Moonlink/MoonlinkPlayer.js new file mode 100644 index 00000000..2f38d9c5 --- /dev/null +++ b/@Moonlink/MoonlinkPlayer.js @@ -0,0 +1,255 @@ +"use strict"; +const event = require('events') +const eventos = new event() +let utils = require('../@Rest/MoonlinkUtils.js') +let { MoonQueue } = require('../@Rest/MoonlinkQueue.js') + +var map = utils.map +var player = map.get('players') || {} +var db = utils.db +var sendDs = utils.sendDs(); + +class MoonPlayer { +#sendWs; + #manager; + constructor(infos, manager) { + this.#sendWs = manager.sendWs + this.#manager = manager + this.infos = infos + this.playing = this.infos.playing || null + this.connected = this.infos.connected || null + this.current = utils.track.current() + this.queue = new MoonQueue({ guildId: infos.guildId }) + } + connect(selfMute, selfDeaf) { + sendDs(this.infos.guildId, JSON.stringify({ + op: 4 + , d: { + guild_id: this.infos.guildId + , channel_id: this.infos.voiceChannel + , self_mute: selfMute || null + , self_deaf: selfDeaf || null + } + })) + var players = map.get('players') + player[this.infos.guildId] = { + ...players[this.infos.guildId] + , connected: true +} + map.set('players', player) +} + +disconnect() { + + sendDs(this.infos.guildId, JSON.stringify({ + op: 4 + , d: { + guild_id: this.infos.guildId + , channel_id: null + , self_mute: null + , self_deaf: null + } + + })) + this.destroy() +} +play() { + let queue = db.get(`queue.${this.infos.guildId}`) + if(!queue) throw new TypeError(`[ MoonlinkJs ]: queue is empty, verify docs.`) + else { + let track = queue.shift(); + if(!track) throw new TypeError(`[ MoonlinkJs ]: an internal error has ocorred.`) + if(track) { + utils.track.editCurrent(track) + db.set(`queue.${this.infos.guildId}`, queue) + this.#sendWs({ + op: 'play' + , guildId: this.infos.guildId + , channelId: this.infos.voiceChannel + , track: track.track + , volume: 80 + , noReplace: false + , pause: false + }) + var players = map.get('players') + player[this.infos.guildId] = { + ...players[this.infos.guildId] + , playing: true + } + map.set('players', player) + } +} +} + +pause() { + if(player[this.infos.guildId].pause) throw new TypeError(`[ MoonlinkJs ]: player is already been paused.`) + player[this.infos.guildId] = { + ...player[this.infos.guildId] + , playing: false + , paused: true + } + map.set('players', player) + this.#sendWs({ + op: 'pause' + , guildId: this.infos.guildId + , pause: true + }); +} + +resume() { + if(!player[this.infos.guildId].paused) throw new TypeError(`[ MoonlinkJs ]: player is not paused.`) + player[this.infos.guildId] = { + ...player[this.infos.guildId] + , playing: true + , paused: false + } + map.set('players', player) + this.#sendWs({ + op: 'pause' + , guildId: this.infos.guildId + , pause: false + }); +} + +volume(percent) { + let queue = db.get(`queue.${this.infos.guildId}`) + if(typeof percent !== 'string' && typeof percent !== 'number') throw new TypeError(`[ MoonlinkJs ]: the percentage must be in string and numbers format.`) + if(!queue) throw new TypeError(`[ MoonlinkJs ]: queue is empty.`) + this.#sendWs({ + op: 'volume' + , guildId: this.infos.guildId + , volume: percent + }) +} + +stop() { + let queue = db.get(`queue.${this.infos.guildId}`) + if(!queue[0]) { + this.#sendWs({ + op: 'stop' + , guildId: this.infos.guildId + }); + } else { + delete map.get(`players`)[this.infos.guildId] + this.#sendWs({ + op: 'stop' + , guildId: this.infos.guildId + }); + } + return true +} + +destroy() { + this.disconnect() + this.#sendWs({ + op: 'destroy', + guildId: this.infos.guildId + }); + if(db.get(`queue.${this.infos.guildId}`)) { + +db.set(`queue.${this.infos.guildId}`, null) + } + let players = map.get('players') + players[this.infos.guildId] = null + map.set('players', players) + return true +} + +skip() { + let queue = db.get(`queue.${this.infos.guildId}`), player = map.get('players') || {} + if(!queue) throw new TypeError(`[ MoonlinkJs ]: queue is empty.`) + else { + if(!queue[0]) { + this.destroy(); + return false + } + if(player[this.infos.guildId].loop > 1) { + const trackl = queue.shift(); + queue.push(utils.track.current()) + utils.track.editCurrent(trackl); + utils.track.skipEdit(true); + db.set('queue.' + this.infos.guildId, queue) + this.#sendWs({ + op:"play" + , channelId: this.infos.voiceChannel + , guildId: this.infos.guildId + , track: trackl.track + }) + return true + } + let actualTrack = queue.shift(); + utils.track.editCurrent(actualTrack); + utils.track.skipEdit(true); + db.set(`queue.${this.infos.guildId}`, queue) + this.#sendWs({ + op: 'play' + , channelId: this.infos.voiceChannel + , guildId: this.infos.guildId + , track: actualTrack.track + }) + return true + } + } + +seek(number) { + let queue = db.get(`queue.${this.infos.guildId}`) + if(typeof number !== 'string' && typeof number !== 'number') throw new TypeError(`[ MoonlinkJs ]: seek need a number in mileseconds.`) + if(queue && queue[0]) return; + if(!utils.track.current().isSeekable) throw new TypeError(`[ Moonlink.Js ]: the track "${utils.track.current().track}" is not seekable`) + this.#sendWs({ + op: 'seek' + , guildId: this.infos.guildId + , position: number + }) + return true +} + +loop(number) { + var player = map.get('players') || {} + if(!number) { + player[this.infos.guildId] = { + ...player[this.infos.guildId] + , loop: undefined + } + } + if(typeof number !== 'string' && typeof number !== 'number') throw new TypeError(`[ MoonlinkJs ]: loop accept only numbers in strings.`) + if(number > 2) throw new TypeError(`[ MoonlinkJs ]: the number cannot be above 2.`); + player[this.infos.guildId] = { + ...player[this.infos.guildId] + , loop: number + } + map.set('players', player) +} + +removeSong(position) { + let queue = db.get(`queue.${this.infos.guildId}`) + if(typeof position !== 'string' && typeof position !== 'number') throw new TypeError(`[ MoonlinkJs ]: accepts only numbers string.`); + if(!queue) throw new TypeError(`[ MoonlinkJs ]: queue is empty!`); + if(!queue[position]) throw new TypeError(`[ MoonlinkJs ]: there are no song in the given position.`) + queue.splice(position, 1) + db.set('queue.' + this.infos.guildId, queue) + return true +} + +skipTo(position) { + let queue = db.get(`queue.${this.infos.guildId}`) + if(typeof position !== 'string' && typeof position !== 'number') throw new TypeError(`[ MoonlinkJs ]: skipTo accept only numbers in strings.`) + if(!queue) throw new TypeError(`[ MoonlinkJs ]: queue is empty.`) + else { + let skipedToTrack = queue.splice(position, 1) + utils.track.editCurrent(skipedToTrack); + db.set(`queue.${this.infos.guildId}`, []) + utils.track.skipEdit(true); + this.#sendWs({ + op: 'play' + , guildId: this.infos.guildId + , channelId: this.infos.voiceChannel + , track: utils.track.current().track + }); + } +} + + +} + +module.exports.MoonPlayer = MoonPlayer; \ No newline at end of file diff --git a/@Rest/MakeRequest.js b/@Rest/MakeRequest.js new file mode 100644 index 00000000..24bce410 --- /dev/null +++ b/@Rest/MakeRequest.js @@ -0,0 +1,39 @@ +let http = require('http') +let https = require('https') +module.exports = function(url, method, opts) { + return new Promise((resolve) => { + if(!method) method || 'GET' + let url_ = new URL(url) + opts.headers['User-Agent'] = 'moon' + opts.headers["Content-Type"] = "application/json" + let request; + if(url_.protocol === 'http:') request = http.request + if(url_.protocol === 'https:') request = https.request + const options = { + + port: url_.port ? url_.port : 443, + method, + ...opts +}; + let req = request(url, options, (res) => { + const chunks = []; + res.on('data', async(chunk) => { + + chunks.push(chunk) + }) + res.on('end', async() => { + try { + const data = Buffer.concat(chunks).toString(); + + resolve(JSON.parse(data)) + } catch(err) { + resolve(err) + } + }) + res.on('error', (err) => { + resolve(err) + }) + }) + req.end() + }) + } \ No newline at end of file diff --git a/@Rest/MoonlinkQueue.js b/@Rest/MoonlinkQueue.js new file mode 100644 index 00000000..4e9fc1b5 --- /dev/null +++ b/@Rest/MoonlinkQueue.js @@ -0,0 +1,53 @@ +"use strict"; +let utils = require('../@Rest/MoonlinkUtils.js') +let db = utils.db +const map = new Map(); +class MoonQueue { + constructor(data) { + this.guildId = data.guildId + } + add(track) { + if (!track) throw new Error('[ MoonLink.Js ]: Track object must have a value') + let queue = db.get(`queue.${this.guildId}`) + if (Array.isArray(queue)) { + db.push(`queue.${this.guildId}`, track) + } else if (queue && queue.length > 0 && queue[0]) { + let newQueue = [queue, track] + db.set(`queue.${this.guildId}`, newQueue) + } else { + db.push(`queue.${this.guildId}`, track) + } + } + first() { + let queue = db.get(`queue.${this.guildId}`) + if (!db.get(`queue.${this.guildId}`)) return null + if (Array.isArray(queue)) { + return queue[0] + } else if (queue && queue.length > 0 && queue[0]) return queue + else queue[0] + } + get all() { + if (!db.get('queue.' + this.guildId)) { + return null + } else { + return db.get(`queue.${this.guildId}`) + } + } + clear() { + if(!db.get(`queue.${this.guildId}`)) { + throw new TypeError(`[ MoonlinkJs ]: unable to clear a non-existent queue.`) +} else { + db.delete(`queue.${this.guildId}`) + return true +} + } + get size() { + if(!db.get(`queue.${this.guildId}`)) { + return 0 + } else { + return db.get(`queue.${this.guildId}`).length + } + } + +} +module.exports.MoonQueue = MoonQueue diff --git a/@Rest/MoonlinkTrack.js b/@Rest/MoonlinkTrack.js new file mode 100644 index 00000000..f3f0f4d3 --- /dev/null +++ b/@Rest/MoonlinkTrack.js @@ -0,0 +1,19 @@ +class MoonTrack { + constructor(data, request) { + + this.position = data.info.position + this.title = data.info.title + this.author = data.info.author + this.url = data.info.uri + this.identifier = data.info.identifier + this.duration = data.info.length + this.isSeekable = data.info.isSeekable + this.track = data.track + this.source = data.info.sourceName || undefined + this.requester = undefined + } + get thumbnail() { + if(this.source === 'youtube') return `https://img.youtube.com/vi/${this.identifier}/sddefault.jpg` + } +} +module.exports = { MoonTrack } diff --git a/@Rest/MoonlinkUtils.js b/@Rest/MoonlinkUtils.js new file mode 100644 index 00000000..6839a63a --- /dev/null +++ b/@Rest/MoonlinkUtils.js @@ -0,0 +1,54 @@ +let MemoryNode = [] +let IdealNode = null +let CurrentTrack = null +let sendDiscord = null +let skipMem = null +let database = require('./MoonlinkDatabase.js') + +let makeRequest = require('./MakeRequest.js') +let db = new database() + +let track = { + skip: function () { + return skipMem || false + } + , skipEdit: function (i) { + delete skipMem + skipMem = i + } + , current: function () { + return CurrentTrack + } + , editCurrent: function (track) { + if (typeof track == 'undefined') throw new Error('[ MoonLink.Js ]: An internal error occurred, report to developers') + if (track) CurrentTrack = track + } +} + +function esdw(x) { + sendDiscord = x +} + +function sendDs() { + return sendDiscord +} + + +function request(node, endpoint, params) { + return makeRequest(`http${node.secure ? 's' : ''}://${node.host}${node.port ? `:${node.port}` : ``}/${endpoint}?${params}`, 'GET' ,{ + headers: { + Authorization: node.password + } + }) + +} + +module.exports = { + track + , db + , sendDs + , esdw + , map: new Map() + , request + , makeRequest +} \ No newline at end of file diff --git a/@Rest/database.json b/@Rest/database.json new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/@Rest/database.json @@ -0,0 +1 @@ +{} \ No newline at end of file