poru 1.1.2 → 1.1.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poru",
3
- "version": "1.1.2",
3
+ "version": "1.1.6",
4
4
  "description": "A stable and powefull lavalink client with so many features",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/src/Player.js CHANGED
@@ -56,6 +56,9 @@ class Player extends EventEmitter {
56
56
  return nulll;
57
57
  }
58
58
  this.currentTrack = this.queue.shift();
59
+ if(!this.currentTrack.track){
60
+ this.currentTrack = await this.currentTrack.resolve(this.manager);
61
+ }
59
62
  this.playing = true;
60
63
  this.timestamp = Date.now();
61
64
  this.node.send({
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 && 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())
@@ -141,7 +141,6 @@ class Filters {
141
141
  }
142
142
 
143
143
  updateFilters(){
144
- console.log(this.node.send)
145
144
  const { volume, equalizer, karaoke, timescale, tremolo, vibrato, rotation, distortion, channelMix, lowPass } = this;
146
145
  this.node.send({
147
146
  op:"filters",
@@ -165,4 +164,4 @@ class Filters {
165
164
 
166
165
 
167
166
 
168
- module.exports = Filters;
167
+ module.exports = Filters;
@@ -1,20 +1,61 @@
1
+ const escapeRegExp = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1
2
  class Track {
2
- constructor(data) {
3
- this.track = data.track
4
- this.info = {
5
- identifier : data.info.identifier,
6
- isSeekable : data.info.isSeekable,
7
- author : data.info.author,
8
- length : data.info.length,
9
- isStream : data.info.isStream,
10
- position : data.info.position,
11
- sourceName:data.info.sourceName,
12
- title : data.info.title,
13
- uri : data.info.uri,
14
- image : `https://i.ytimg.com/vi/${data.info.identifier}/maxresdefault.jpg` || null
15
-
3
+ constructor(data) {
4
+ this.track = data.track
5
+ this.info = {
6
+ identifier: data.info.identifier,
7
+ isSeekable: data.info.isSeekable,
8
+ author: data.info.author,
9
+ length: data.info.length,
10
+ isStream: data.info.isStream,
11
+ sourceName: data.info.sourceName,
12
+ title: data.info.title,
13
+ uri: data.info.uri,
14
+ image: `https://i.ytimg.com/vi/${data.info.identifier}/maxresdefault.jpg` || null
15
+
16
+ }
17
+ }
18
+
19
+ async resolve(manager) {
20
+
21
+ const query = [this.info.author, this.info.title].filter((x) => !!x).join(' - ');
22
+
23
+
24
+ const result = await manager.resolve(query);
25
+ if (!result || !result.tracks.length) return;
26
+
27
+ if (this.info.author) {
28
+ const author = [this.info.author, `${this.info.author} - Topic`];
29
+ const officialAudio = result.tracks.find(
30
+ (track) =>
31
+ author.some((name) => new RegExp(`^${escapeRegExp(name)}$`, 'i').test(track.info.author)) ||
32
+ new RegExp(`^${escapeRegExp(this.info.title)}$`, 'i').test(track.info.title),
33
+ );
34
+ if (officialAudio) {
35
+ this.info.identifier = officialAudio.info.identifier
36
+ this.image =`https://i.ytimg.com/vi/${this.info.identifier}/maxresdefault.jpg`
37
+ this.track = officialAudio.track;
38
+ return this
16
39
  }
40
+ }
41
+ if (this.info.length) {
42
+ const sameDuration = result.tracks.find(
43
+ (track) =>
44
+ track.info.length >= (this.info.length ? this.length : 0) - 2000 &&
45
+ track.info.length <= (this.info.length ? this.length : 0) + 2000,
46
+ );
47
+ if (sameDuration) {
48
+ this.info.identifier = sameDuration.info.identifier
49
+ this.image =`https://i.ytimg.com/vi/${this.info.identifier}/maxresdefault.jpg`
50
+ this.track = sameDuration.track;
51
+ return this
17
52
  }
53
+ }
54
+ this.info.identifier = result.tracks[0].info.identifier
55
+ this.image =`https://i.ytimg.com/vi/${this.info.identifier}/maxresdefault.jpg`
56
+ this.track = result.tracks[0].track;
57
+ return this
58
+ }
18
59
  }
19
60
 
20
61
  module.exports = Track;
@@ -0,0 +1,253 @@
1
+ const fetch = (...args) => import('node-fetch').then(({
2
+ default: fetch
3
+ }) => fetch(...args));
4
+ const Track = require("../guild/Track")
5
+ class Spotify {
6
+ constructor(manager) {
7
+ this.manager = manager;
8
+ this.baseURL = "https://api.spotify.com/v1"
9
+ this.spotifyPattern = /^(?:https:\/\/open\.spotify\.com\/(?:user\/[A-Za-z0-9]+\/)?|spotify:)(album|playlist|track|artist)(?:[/:])([A-Za-z0-9]+).*$/
10
+ this.clientID = manager.options.spotify.clientID;
11
+ this.clientSecret = manager.options.spotify.clientSecret
12
+ this.authorization = Buffer
13
+ .from(`${this.clientID}:${this.clientSecret}`)
14
+ .toString("base64");
15
+ this.interval = 0;
16
+ }
17
+
18
+ check(url) {
19
+ return this.spotifyPattern.test(url);
20
+ }
21
+
22
+ async requestToken() {
23
+ if (this.nextRequest) return;
24
+
25
+ try {
26
+ const data = await fetch("https://accounts.spotify.com/api/token?grant_type=client_credentials", {
27
+ method: "POST",
28
+ headers: {
29
+ Authorization: `Basic ${this.authorization}`,
30
+ 'Content-Type': 'application/x-www-form-urlencoded',
31
+ }
32
+ })
33
+
34
+ const body = await data.json();
35
+
36
+ this.token = `Bearer ${body.access_token}`;
37
+ this.interval = body.expires_in * 1000
38
+ } catch (e) {
39
+ if (e.status === 400) {
40
+ throw new Error("Invalid Spotify client.")
41
+ }
42
+ }
43
+ }
44
+
45
+ async renew() {
46
+ if (Date.now() >= this.interval) {
47
+ await this.requestToken();
48
+ }
49
+ }
50
+
51
+ async requestData(endpoint) {
52
+ await this.renew();
53
+
54
+ const req = await fetch(`${this.baseURL}${/^\//.test(endpoint) ? endpoint : `/${endpoint}`}`, {
55
+ headers: { Authorization: this.token }
56
+ })
57
+ const data = await req.json()
58
+ return data
59
+ }
60
+
61
+
62
+ async resolve(url) {
63
+ if (!this.token) await this.requestToken()
64
+ const [, type, id] = await this.spotifyPattern.exec(url) ?? [];
65
+
66
+ switch (type) {
67
+
68
+ case "playlist":
69
+ {
70
+ return this.fetchPlaylist(id)
71
+ }
72
+ case "track":
73
+ {
74
+ return this.fetchTrack(id)
75
+ }
76
+ case "album":
77
+ {
78
+ return this.fetchAlbum(id)
79
+ }
80
+ case "artist":
81
+ {
82
+ return this.fetchArtist(id);
83
+ }
84
+
85
+ default: {
86
+ return this.manager.resolve(url)
87
+ }
88
+ }
89
+
90
+ }
91
+
92
+ async fetchPlaylist(id) {
93
+ try {
94
+ const playlist = await this.requestData(`/playlists/${id}`)
95
+ await this.fetchPlaylistTracks(playlist);
96
+ const unresolvedPlaylistTracks = await Promise.all(playlist.tracks.items.map(x => this.buildUnresolved(x.track)))
97
+
98
+
99
+ return this.buildResponse(
100
+ "PLAYLIST_LOADED",
101
+ unresolvedPlaylistTracks,
102
+ playlist.name
103
+ );
104
+
105
+ } catch (e) {
106
+ return this.buildResponse(e.status === 404 ? "NO_MATCHES" : "LOAD_FAILED", [], undefined, e.body?.error.message ?? e.message);
107
+ }
108
+ }
109
+
110
+ async fetchAlbum(id) {
111
+ try {
112
+ const album = await this.requestData(`/albums/${id}`)
113
+
114
+ const unresolvedPlaylistTracks = await Promise.all(album.tracks.items.map(x => this.buildUnresolved(x)));
115
+ return this.buildResponse(
116
+ "PLAYLIST_LOADED",
117
+ unresolvedPlaylistTracks,
118
+ album.name
119
+ );
120
+
121
+ } catch (e) {
122
+ return this.buildResponse(e.body?.error.message === "invalid id" ? "NO_MATCHES" : "LOAD_FAILED", [], undefined, e.body?.error.message ?? e.message);
123
+ }
124
+ }
125
+
126
+ async fetchArtist(id) {
127
+ try {
128
+ const artist = await this.requestData(`/artists/${id}`)
129
+
130
+ const data = await this.requestData(`/artists/${id}/top-tracks?market=US`)
131
+ const unresolvedPlaylistTracks = await Promise.all(data.tracks.map(x => this.buildUnresolved(x)));
132
+
133
+ return this.buildResponse(
134
+ "PLAYLIST_LOADED",
135
+ unresolvedPlaylistTracks,
136
+ artist.name
137
+ );
138
+ } catch (e) {
139
+ return this.buildResponse(e.body?.error.message === "invalid id" ? "NO_MATCHES" : "LOAD_FAILED", [], undefined, e.body?.error.message ?? e.message);
140
+ }
141
+
142
+ }
143
+
144
+ async fetchTrack(id) {
145
+ try {
146
+ const data = await this.requestData(`/tracks/${id}`)
147
+ const unresolvedTrack = await this.buildUnresolved(data);
148
+ return this.buildResponse(
149
+ "TRACK_LOADED",
150
+ [unresolvedTrack]
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 fetch(query) {
158
+ try {
159
+
160
+ if (this.check(query)) return this.resolve(query)
161
+
162
+ const data = await this.requestData(`/search/?q="${query}"&type=artist,album,track`)
163
+
164
+ const unresolvedTrack = await this.buildUnresolved(data.tracks.items[0]);
165
+
166
+ return this.buildResponse(
167
+ "TRACK_LOADED",
168
+ [unresolvedTrack]
169
+ );
170
+ } catch (e) {
171
+ return this.buildResponse(e.body?.error.message === "invalid id" ? "NO_MATCHES" : "LOAD_FAILED", [], undefined, e.body?.error.message ?? e.message);
172
+ }
173
+ }
174
+
175
+ async fetchPlaylistTracks(spotifyPlaylist) {
176
+ let nextPage = spotifyPlaylist.tracks.next;
177
+ let pageLoaded = 1;
178
+ while (nextPage) {
179
+ if (!nextPage) break;
180
+ const req = await fetch(nextPage, {
181
+ headers: { Authorization: this.token }
182
+ })
183
+ const body = await req.json()
184
+ if (body.error) break;
185
+ spotifyPlaylist.tracks.items.push(...body.items);
186
+
187
+ nextPage = body.next;
188
+ pageLoaded++;
189
+ }
190
+ }
191
+
192
+
193
+
194
+ async buildUnresolved(track) {
195
+ if (!track) throw new ReferenceError("The Spotify track object was not provided");
196
+
197
+ return new Track({
198
+ track: "",
199
+ info: {
200
+ sourceName: 'spotify',
201
+ identifier: track.id,
202
+ isSeekable: true,
203
+ author: track.artists[0] ? track.artists[0].name : 'Unknown',
204
+ length: track.duration_ms,
205
+ isStream: false,
206
+ title: track.name,
207
+ uri: `https://open.spotify.com/track/${track.id}`,
208
+ image: track.album?.images[0]?.url,
209
+ },
210
+ })
211
+
212
+
213
+
214
+ }
215
+
216
+ async fetchMetaData(track) {
217
+
218
+ const fetch = await this.manager.resolve(`${track.info.title} ${track.info.author}`)
219
+ return fetch.tracks[0];
220
+ }
221
+
222
+ async buildTrack(unresolvedTrack) {
223
+ const lavaTrack = await this.fetchMetaData(unresolvedTrack);
224
+ if (lavaTrack) {
225
+ unresolvedTrack.track = lavaTrack.track;
226
+ unresolvedTrack.info.identifier = lavaTrack.info.identifier
227
+ return unresolvedTrack
228
+ }
229
+ }
230
+
231
+
232
+ compareValue(value) {
233
+ return typeof value !== 'undefined' ? value !== null : typeof value !== 'undefined';
234
+ }
235
+
236
+
237
+ buildResponse(loadType, tracks, playlistName, exceptionMsg) {
238
+
239
+ return Object.assign({
240
+ loadType,
241
+ tracks,
242
+ playlistInfo: playlistName ? { name: playlistName } : {}
243
+ }, exceptionMsg ? { exception: { message: exceptionMsg, severity: "COMMON" } } : {});
244
+ }
245
+
246
+
247
+ }
248
+
249
+
250
+
251
+
252
+
253
+ 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';