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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poru",
3
- "version": "1.1.2",
3
+ "version": "1.2.3",
4
4
  "description": "A stable and powefull lavalink client with so many features",
5
5
  "main": "index.js",
6
6
  "scripts": {
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
- let guild = data.guild_id
86
- this.voiceServers.set(guild, data);
87
- const server = this.voiceServers.get(guild);
88
- const state = this.voiceStates.get(guild);
89
- if (!server) return false;
90
- const player = this.players.get(guild);
91
- if (!player) return false;
92
-
93
- player.connect({
94
- sessionId: state ? state.session_id : player.voiceUpdateState.sessionId,
95
- event: server,
96
- });
97
-
98
- return true;
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
- if (!['VOICE_STATE_UPDATE', 'VOICE_SERVER_UPDATE'].includes(packet.t)) return;
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
- this.setServersUpdate(packet.d);
138
+ if (packet.t === "VOICE_SERVER_UPDATE") {
139
+ this.setServersUpdate(packet.d);
131
140
  }
132
- if (packet.t === "VOICE_STATE_UPDATE"){
133
- this.setStateUpdate(packet.d);
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())
@@ -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
@@ -1,3 +1,5 @@
1
+ import { EventEmitter } from "ws";
2
+
1
3
  declare module'Poru' {
2
4
 
3
5
  import { EventEmitter } from 'events';