poru 1.1.2 → 1.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -3
- package/package.json +1 -1
- package/src/Poru.js +45 -33
- package/src/guild/Track.js +1 -2
- package/src/platform/Spotify.js +258 -0
- package/typings/index.d.ts +2 -0
package/README.md
CHANGED
|
@@ -85,9 +85,6 @@ client.once("ready", () => {
|
|
|
85
85
|
console.log(`Logged in as ${client.user.tag}`);
|
|
86
86
|
});
|
|
87
87
|
|
|
88
|
-
// this event used to make connections upto date with lavalink
|
|
89
|
-
client.on("raw",async d => await client.poru.packetUpdate(d));
|
|
90
|
-
|
|
91
88
|
// Finally login at the END of your code
|
|
92
89
|
client.login("your bot token here");
|
|
93
90
|
|
package/package.json
CHANGED
package/src/Poru.js
CHANGED
|
@@ -6,13 +6,13 @@ const Player = require("./Player");
|
|
|
6
6
|
const Node = require("./Node");
|
|
7
7
|
const Response = require("./guild/Response");
|
|
8
8
|
const config = require("./config.json")
|
|
9
|
-
|
|
9
|
+
const Spotify = require("./platform/Spotify")
|
|
10
10
|
class Poru extends EventEmitter {
|
|
11
11
|
constructor(client, nodes, options = {}) {
|
|
12
12
|
super();
|
|
13
13
|
if (!client) throw new Error("[Poru Error] you did't provide a valid client");
|
|
14
14
|
if (!nodes) throw new Error("[Poru Error] you did't provide a lavalink nodes");
|
|
15
|
-
if(!options) throw new Error("[Poru Error] options must be provided!")
|
|
15
|
+
if (!options) throw new Error("[Poru Error] options must be provided!")
|
|
16
16
|
this.client = client;
|
|
17
17
|
this._nodes = nodes;
|
|
18
18
|
this.nodes = new Map();
|
|
@@ -29,7 +29,7 @@ class Poru extends EventEmitter {
|
|
|
29
29
|
|
|
30
30
|
//create a node and connect it with lavalink
|
|
31
31
|
addNode(options) {
|
|
32
|
-
const node = new Node(this, options,this.options);
|
|
32
|
+
const node = new Node(this, options, this.options);
|
|
33
33
|
if (options.name) {
|
|
34
34
|
this.nodes.set(options.name || options.host, node);
|
|
35
35
|
node.connect();
|
|
@@ -41,16 +41,16 @@ class Poru extends EventEmitter {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
//remove node and destroy web socket connection
|
|
44
|
-
removeNode(identifier){
|
|
44
|
+
removeNode(identifier) {
|
|
45
45
|
const node = this.nodes.get(identifier);
|
|
46
46
|
if (!node) return;
|
|
47
47
|
node.destroy();
|
|
48
48
|
this.nodes.delete(identifier)
|
|
49
|
-
|
|
49
|
+
}
|
|
50
50
|
//create connection with discord voice channel
|
|
51
51
|
createConnection(data = {}) {
|
|
52
52
|
const player = this.players.get(data.guild.id || data.guild);
|
|
53
|
-
if (player){
|
|
53
|
+
if (player) {
|
|
54
54
|
return player;
|
|
55
55
|
}
|
|
56
56
|
this.sendData({
|
|
@@ -74,47 +74,56 @@ class Poru extends EventEmitter {
|
|
|
74
74
|
const guild = client.guilds.cache.get(data.d.guild_id);
|
|
75
75
|
if (guild) guild.shard.send(data);
|
|
76
76
|
}
|
|
77
|
-
client.on("raw",async packet =>{
|
|
77
|
+
client.on("raw", async packet => {
|
|
78
78
|
await this.#packetUpdate(packet);
|
|
79
79
|
})
|
|
80
|
+
|
|
80
81
|
this._nodes.forEach((node) => this.addNode(node));
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
if (this.options.spotify.clientID && this.options.spotify.clientSecret) {
|
|
85
|
+
this.spotify = new Spotify(this, {
|
|
86
|
+
clientID: this.options.clientID,
|
|
87
|
+
clientSecret: this.options.clientSecret
|
|
88
|
+
})
|
|
89
|
+
}
|
|
81
90
|
console.log(`Thanks for using Poru`)
|
|
82
91
|
}
|
|
83
92
|
|
|
84
93
|
setServersUpdate(data) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
94
|
+
let guild = data.guild_id
|
|
95
|
+
this.voiceServers.set(guild, data);
|
|
96
|
+
const server = this.voiceServers.get(guild);
|
|
97
|
+
const state = this.voiceStates.get(guild);
|
|
98
|
+
if (!server) return false;
|
|
99
|
+
const player = this.players.get(guild);
|
|
100
|
+
if (!player) return false;
|
|
101
|
+
|
|
102
|
+
player.connect({
|
|
103
|
+
sessionId: state ? state.session_id : player.voiceUpdateState.sessionId,
|
|
104
|
+
event: server,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
100
109
|
|
|
101
110
|
setStateUpdate(data) {
|
|
102
111
|
if (data.user_id !== this.user) return;
|
|
103
112
|
if (data.channel_id) {
|
|
104
113
|
const guild = data.guild_id;
|
|
105
|
-
|
|
114
|
+
|
|
106
115
|
this.voiceStates.set(data.guild_id, data);
|
|
107
116
|
const server = this.voiceServers.get(guild);
|
|
108
117
|
const state = this.voiceStates.get(guild);
|
|
109
118
|
if (!server) return false;
|
|
110
119
|
const player = this.players.get(guild);
|
|
111
120
|
if (!player) return false;
|
|
112
|
-
|
|
121
|
+
|
|
113
122
|
player.connect({
|
|
114
123
|
sessionId: state ? state.session_id : player.voiceUpdateState.sessionId,
|
|
115
124
|
event: server,
|
|
116
125
|
});
|
|
117
|
-
|
|
126
|
+
|
|
118
127
|
return true;
|
|
119
128
|
}
|
|
120
129
|
this.voiceServers.delete(data.guild_id);
|
|
@@ -122,15 +131,15 @@ class Poru extends EventEmitter {
|
|
|
122
131
|
}
|
|
123
132
|
|
|
124
133
|
#packetUpdate(packet) {
|
|
125
|
-
|
|
134
|
+
if (!['VOICE_STATE_UPDATE', 'VOICE_SERVER_UPDATE'].includes(packet.t)) return;
|
|
126
135
|
const player = this.players.get(packet.d.guild_id);
|
|
127
136
|
if (!player) return;
|
|
128
137
|
|
|
129
|
-
if (packet.t === "VOICE_SERVER_UPDATE"){
|
|
130
|
-
|
|
138
|
+
if (packet.t === "VOICE_SERVER_UPDATE") {
|
|
139
|
+
this.setServersUpdate(packet.d);
|
|
131
140
|
}
|
|
132
|
-
|
|
133
|
-
|
|
141
|
+
if (packet.t === "VOICE_STATE_UPDATE") {
|
|
142
|
+
this.setStateUpdate(packet.d);
|
|
134
143
|
}
|
|
135
144
|
}
|
|
136
145
|
|
|
@@ -156,7 +165,7 @@ class Poru extends EventEmitter {
|
|
|
156
165
|
if (!node) throw new Error("[Poru Error] No nodes are avalible");
|
|
157
166
|
|
|
158
167
|
// eslint-disable-next-line new-cap
|
|
159
|
-
const player = new Player(this,node, data);
|
|
168
|
+
const player = new Player(this, node, data);
|
|
160
169
|
this.players.set(guild, player);
|
|
161
170
|
player.connect()
|
|
162
171
|
return player;
|
|
@@ -168,13 +177,16 @@ class Poru extends EventEmitter {
|
|
|
168
177
|
async resolve(track, source) {
|
|
169
178
|
const node = this.leastUsedNodes[0];
|
|
170
179
|
if (!node) throw new Error("No nodes are available.");
|
|
180
|
+
if(this.spotify && this.spotify.check(track)){
|
|
181
|
+
return await this.spotify.resolve(track);
|
|
182
|
+
}
|
|
171
183
|
const regex = /^https?:\/\//;
|
|
172
184
|
if (!regex.test(track)) {
|
|
173
185
|
// eslint-disable-next-line no-param-reassign
|
|
174
186
|
track = `${source || "yt"}search:${track}`;
|
|
175
187
|
}
|
|
176
188
|
const result = await this.#fetch(node, "loadtracks", `identifier=${encodeURIComponent(track)}`);
|
|
177
|
-
|
|
189
|
+
|
|
178
190
|
if (!result) throw new Error("[Poru Error] No tracks found.");
|
|
179
191
|
return new Response(result);
|
|
180
192
|
}
|
|
@@ -192,7 +204,7 @@ class Poru extends EventEmitter {
|
|
|
192
204
|
return fetch(`http${node.secure ? "s" : ""}://${node.host}:${node.port}/${endpoint}?${param}`, {
|
|
193
205
|
headers: {
|
|
194
206
|
Authorization: node.password,
|
|
195
|
-
|
|
207
|
+
|
|
196
208
|
},
|
|
197
209
|
})
|
|
198
210
|
.then((r) => r.json())
|
package/src/guild/Track.js
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
class Track {
|
|
2
2
|
constructor(data) {
|
|
3
3
|
this.track = data.track
|
|
4
|
-
this.info =
|
|
4
|
+
this.info ={
|
|
5
5
|
identifier : data.info.identifier,
|
|
6
6
|
isSeekable : data.info.isSeekable,
|
|
7
7
|
author : data.info.author,
|
|
8
8
|
length : data.info.length,
|
|
9
9
|
isStream : data.info.isStream,
|
|
10
|
-
position : data.info.position,
|
|
11
10
|
sourceName:data.info.sourceName,
|
|
12
11
|
title : data.info.title,
|
|
13
12
|
uri : data.info.uri,
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
const fetch = (...args) => import('node-fetch').then(({
|
|
2
|
+
default: fetch
|
|
3
|
+
}) => fetch(...args));
|
|
4
|
+
class Spotify {
|
|
5
|
+
constructor(manager) {
|
|
6
|
+
this.manager = manager;
|
|
7
|
+
this.baseURL = "https://api.spotify.com/v1"
|
|
8
|
+
this.spotifyPattern = /^(?:https:\/\/open\.spotify\.com\/(?:user\/[A-Za-z0-9]+\/)?|spotify:)(album|playlist|track|artist)(?:[/:])([A-Za-z0-9]+).*$/
|
|
9
|
+
this.clientID = manager.options.spotify.clientID;
|
|
10
|
+
this.clientSecret = manager.options.spotify.clientSecret
|
|
11
|
+
this.authorization = Buffer
|
|
12
|
+
.from(`${this.clientID}:${this.clientSecret}`)
|
|
13
|
+
.toString("base64");
|
|
14
|
+
this.interval = 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
check(url) {
|
|
18
|
+
return this.spotifyPattern.test(url);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async requestToken() {
|
|
22
|
+
if (this.nextRequest) return;
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const data = await fetch("https://accounts.spotify.com/api/token?grant_type=client_credentials",{
|
|
26
|
+
method:"POST",
|
|
27
|
+
headers: {
|
|
28
|
+
Authorization: `Basic ${this.authorization}`,
|
|
29
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const body = await data.json();
|
|
34
|
+
|
|
35
|
+
this.token = `Bearer ${body.access_token}`;
|
|
36
|
+
this.interval = body.expires_in * 1000
|
|
37
|
+
} catch (e) {
|
|
38
|
+
if (e.status === 400) {
|
|
39
|
+
throw new Error("Invalid Spotify client.")
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async renew() {
|
|
45
|
+
if (Date.now() >= this.interval) {
|
|
46
|
+
await this.requestToken();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async requestData(endpoint) {
|
|
51
|
+
await this.renew();
|
|
52
|
+
|
|
53
|
+
const req = await fetch(`${this.baseURL}${/^\//.test(endpoint) ? endpoint : `/${endpoint}`}`, {
|
|
54
|
+
headers: { Authorization: this.token }
|
|
55
|
+
})
|
|
56
|
+
const data = await req.json()
|
|
57
|
+
return data
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
async resolve(url) {
|
|
62
|
+
if (!this.token) await this.requestToken()
|
|
63
|
+
const [, type, id] = await this.spotifyPattern.exec(url) ?? [];
|
|
64
|
+
|
|
65
|
+
switch (type) {
|
|
66
|
+
|
|
67
|
+
case "playlist":
|
|
68
|
+
{
|
|
69
|
+
return this.fetchPlaylist(id)
|
|
70
|
+
}
|
|
71
|
+
case "track":
|
|
72
|
+
{
|
|
73
|
+
return this.fetchTrack(id)
|
|
74
|
+
}
|
|
75
|
+
case "album":
|
|
76
|
+
{
|
|
77
|
+
return this.fetchAlbum(id)
|
|
78
|
+
}
|
|
79
|
+
case "artist":
|
|
80
|
+
{
|
|
81
|
+
return this.fetchArtist(id);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
default: {
|
|
85
|
+
return this.manager.resolve(url)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async fetchPlaylist(id) {
|
|
92
|
+
try {
|
|
93
|
+
const playlist = await this.requestData(`/playlists/${id}`)
|
|
94
|
+
await this.fetchPlaylistTracks(playlist);
|
|
95
|
+
const unresolvedPlaylistTracks = playlist.tracks.items.map(x => this.buildUnresolved(x.track));
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
return this.buildResponse(
|
|
99
|
+
"PLAYLIST_LOADED",
|
|
100
|
+
(await Promise.all(unresolvedPlaylistTracks.map(x => x.then((a) => a.resolve())))).filter(Boolean),
|
|
101
|
+
playlist.name
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
} catch (e) {
|
|
105
|
+
return this.buildResponse(e.status === 404 ? "NO_MATCHES" : "LOAD_FAILED", [], undefined, e.body?.error.message ?? e.message);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async fetchAlbum(id) {
|
|
110
|
+
try{
|
|
111
|
+
const album = await this.requestData(`/albums/${id}`)
|
|
112
|
+
|
|
113
|
+
const unresolvedPlaylistTracks = album.tracks.map(x => this.buildUnresolved(x));
|
|
114
|
+
return this.buildResponse(
|
|
115
|
+
"PLAYLIST_LOADED",
|
|
116
|
+
(await Promise.all(unresolvedPlaylistTracks.map(x => x.then((a) => a.resolve())))).filter(Boolean),
|
|
117
|
+
album.name
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
}catch(e){
|
|
121
|
+
return this.buildResponse(e.body?.error.message === "invalid id" ? "NO_MATCHES" : "LOAD_FAILED", [], undefined, e.body?.error.message ?? e.message);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async fetchArtist(id) {
|
|
126
|
+
try{
|
|
127
|
+
const artist = await this.requestData(`/artists/${id}`)
|
|
128
|
+
|
|
129
|
+
const data = await this.requestData(`/artists/${id}/top-tracks?market=US`)
|
|
130
|
+
const unresolvedPlaylistTracks = data.tracks.map(x => this.buildUnresolved(x));
|
|
131
|
+
|
|
132
|
+
return this.buildResponse(
|
|
133
|
+
"PLAYLIST_LOADED",
|
|
134
|
+
(await Promise.all(unresolvedPlaylistTracks.map(x => x.then((a) => a.resolve())))).filter(Boolean),
|
|
135
|
+
artist.name
|
|
136
|
+
);
|
|
137
|
+
}catch(e){
|
|
138
|
+
return this.buildResponse(e.body?.error.message === "invalid id" ? "NO_MATCHES" : "LOAD_FAILED", [], undefined, e.body?.error.message ?? e.message);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async fetchTrack(id) {
|
|
144
|
+
try{
|
|
145
|
+
const data = await this.requestData(`/tracks/${id}`)
|
|
146
|
+
const unresolvedTrack = this.buildUnresolved(data);
|
|
147
|
+
|
|
148
|
+
return this.buildResponse(
|
|
149
|
+
"TRACK_LOADED",
|
|
150
|
+
[await unresolvedTrack.then((a) => a.resolve())]
|
|
151
|
+
);
|
|
152
|
+
}catch(e){
|
|
153
|
+
return this.buildResponse(e.body?.error.message === "invalid id" ? "NO_MATCHES" : "LOAD_FAILED", [], undefined, e.body?.error.message ?? e.message);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async fetchByWords(query) {
|
|
158
|
+
try{
|
|
159
|
+
const data = await this.requestData(`/search/?q="${query}"&type=artist,album,track`)
|
|
160
|
+
|
|
161
|
+
const unresolvedTrack = this.buildUnresolved(data.tracks.items[0]);
|
|
162
|
+
|
|
163
|
+
return this.buildResponse(
|
|
164
|
+
"TRACK_LOADED",
|
|
165
|
+
[await unresolvedTrack.then((a) => a.resolve())]
|
|
166
|
+
);
|
|
167
|
+
}catch(e){
|
|
168
|
+
return this.buildResponse(e.body?.error.message === "invalid id" ? "NO_MATCHES" : "LOAD_FAILED", [], undefined, e.body?.error.message ?? e.message);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async fetchPlaylistTracks(spotifyPlaylist) {
|
|
173
|
+
let nextPage = spotifyPlaylist.tracks.next;
|
|
174
|
+
let pageLoaded = 1;
|
|
175
|
+
while (nextPage) {
|
|
176
|
+
if (!nextPage) break;
|
|
177
|
+
const req = await fetch(nextPage, {
|
|
178
|
+
headers: { Authorization: this.token }
|
|
179
|
+
})
|
|
180
|
+
const body = await req.json()
|
|
181
|
+
if (body.error) break;
|
|
182
|
+
spotifyPlaylist.tracks.items.push(...body.items);
|
|
183
|
+
|
|
184
|
+
nextPage = body.next;
|
|
185
|
+
pageLoaded++;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
async buildUnresolved(track) {
|
|
192
|
+
if (!track) throw new ReferenceError("The Spotify track object was not provided");
|
|
193
|
+
// if (!track.artists) throw new ReferenceError("The track artists array was not provided");
|
|
194
|
+
if (!track.name) throw new ReferenceError("The track name was not provided");
|
|
195
|
+
if (!Array.isArray(track.artists)) throw new TypeError(`The track artists must be an array, received type ${typeof track.artists}`);
|
|
196
|
+
if (typeof track.name !== "string") throw new TypeError(`The track name must be a string, received type ${typeof track.name}`);
|
|
197
|
+
|
|
198
|
+
const _this = this;
|
|
199
|
+
return {
|
|
200
|
+
track: "",
|
|
201
|
+
info: {
|
|
202
|
+
sourceName: 'spotify',
|
|
203
|
+
identifier: track.id,
|
|
204
|
+
isSeekable: true,
|
|
205
|
+
author: track.artists[0] ? track.artists[0].name : 'Unknown',
|
|
206
|
+
length: track.duration_ms,
|
|
207
|
+
isStream: false,
|
|
208
|
+
title: track.name,
|
|
209
|
+
uri: `https://open.spotify.com/track/${track.id}`,
|
|
210
|
+
image: track.album?.images[0]?.url,
|
|
211
|
+
},
|
|
212
|
+
resolve() {
|
|
213
|
+
return _this.buildTrack(this)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async fetchMetaData(track) {
|
|
222
|
+
|
|
223
|
+
const fetch = await this.manager.resolve(`${track.info.title} ${track.info.author}`)
|
|
224
|
+
return fetch.tracks[0];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async buildTrack(unresolvedTrack) {
|
|
228
|
+
const lavaTrack = await this.fetchMetaData(unresolvedTrack);
|
|
229
|
+
if(lavaTrack){
|
|
230
|
+
unresolvedTrack.track = lavaTrack.track;
|
|
231
|
+
unresolvedTrack.info.identifier = lavaTrack.info.identifier
|
|
232
|
+
return unresolvedTrack
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
compareValue(value) {
|
|
238
|
+
return typeof value !== 'undefined' ? value !== null : typeof value !== 'undefined';
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
buildResponse(loadType, tracks, playlistName, exceptionMsg) {
|
|
243
|
+
|
|
244
|
+
return Object.assign({
|
|
245
|
+
loadType,
|
|
246
|
+
tracks,
|
|
247
|
+
playlistInfo: playlistName ? { name: playlistName } : {}
|
|
248
|
+
}, exceptionMsg ? { exception: { message: exceptionMsg, severity: "COMMON" } } : {});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
module.exports = Spotify
|