poru 2.0.2 → 3.0.2

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.
@@ -0,0 +1,13 @@
1
+ # These are supported funding model platforms
2
+
3
+ github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4
+ patreon: # Replace with a single Patreon username
5
+ open_collective: # Replace with a single Open Collective username
6
+ ko_fi: parasdev
7
+ tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8
+ community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9
+ liberapay: # Replace with a single Liberapay username
10
+ issuehunt: # Replace with a single IssueHunt username
11
+ otechie: # Replace with a single Otechie username
12
+ lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
13
+ custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
package/README.md CHANGED
@@ -50,71 +50,84 @@ To use you need a configured [Lavalink](https://github.com/Frederikam/Lavalink)
50
50
  ## Example usage basic bot
51
51
 
52
52
  ```javascript
53
- // main file
54
- // Require both libraries
55
- const { Client } = require('discord.js');
53
+ const { Client, GatewayIntentBits } = require('discord.js');
56
54
  const { Poru } = require('poru');
57
-
58
- // Initiate both main classes
59
- const client = new Client();
60
-
61
- // Define some options for the node
62
55
  const nodes = [
63
56
  {
64
- host: 'localhost',
65
- password: 'youshallnotpass',
66
- port: 2333,
67
- secure: false,
68
- },
69
- ];
70
-
71
- // Define if you want to integrate spotify
72
- const spotifyOptions = {
73
- clientID: 'Your Client ID', // You'll find this on https://developers.spotify.com/dashboard/
74
- clientSecret: 'Your Client Secret', // You'll find this on https://developers.spotify.com/dashboard/
75
- playlistLimit: 10, // The amount of pages to load when a playlist is searched with each page having 50 tracks.
76
- albumLimit: 5, // The amount of pages to load when a album is searched with each page having 50 tracks.
77
- artistLimit: 5, // The amount of pages to load when a artist is searched with each page having 50 tracks.
78
- searchMarket: 'IN', // The market from where the query should be searched from. Mainly this should contain your country.
79
- };
80
-
81
- // Assign Manager to the client variable
82
- client.poru = new Poru(client, nodes);
83
-
84
- // Emitted whenever a node connects
85
- client.poru.on('nodeConnect', node => {
86
- console.log(`Node "${node.name}" connected.`);
57
+ id: "main_node",
58
+ hostname: "localhost",
59
+ port: 8080,
60
+ password: "iloveyou3000"
61
+ }
62
+ ]
63
+ const PoruOptions = {
64
+ reconnectTime: 0,
65
+ resumeKey: 'MyPlayers',
66
+ resumeTimeout: 60,
67
+ defaultPlatform: "ytsearch"
68
+ }
69
+ const client = new Client({
70
+ intents: [
71
+ GatewayIntentBits.Guilds,
72
+ GatewayIntentBits.GuildMessages,
73
+ GatewayIntentBits.GuildVoiceStates,
74
+ GatewayIntentBits.MessageContent
75
+ ]
87
76
  });
77
+ client.poru = new Poru(client, nodes, PoruOptions)
88
78
 
89
- // Emitted whenever a node encountered an error
90
- client.poru.on('nodeError', (node, error) => {
91
- console.log(`Node "${node.name}" encountered an error`);
79
+ client.poru.on('trackStart', (player, track) => {
80
+ const channel = client.channels.cache.get(player.textChannel);
81
+ return channel.send(`Now playing \`${track.title}\``);
92
82
  });
93
83
 
94
- // Listen for when the client becomes ready
95
- client.once('ready', () => {
84
+ client.on('ready', () => {
85
+ console.log('Ready!');
96
86
  client.poru.init(client);
97
- console.log(`Logged in as ${client.user.tag}`);
98
87
  });
99
88
 
100
- // this event used to make connections upto date with lavalink
101
- client.on('raw', async d => await client.poru.packetUpdate(d));
102
-
103
- // Finally login at the END of your code
104
- client.login('your bot token here');
105
- ```
106
89
 
107
- ```javascript
108
- // creating player
109
- const player = await client.poru.createConnection({
110
- guild: message.guild.id,
111
- voiceChannel: message.member.voice.channel.id,
112
- textChannel: message.channel,
113
- selfDeaf: true,
114
- selfMute: false,
90
+ client.on('interactionCreate', async (interaction) => {
91
+ if (!interaction.isChatInputCommand()) return;
92
+ if (!interaction.member.voice.channel) return interaction.reply({ content: `Please connect with voice channel `, ephemeral: true });
93
+
94
+ const track = interaction.options.getString('track');
95
+
96
+ const res = await client.poru.resolve(track);
97
+
98
+ if (res.loadType === "LOAD_FAILED") {
99
+ return interaction.reply('Failed to load track.');
100
+ } else if (res.loadType === "NO_MATCHES") {
101
+ return interaction.reply('No source found!');
102
+ }
103
+
104
+ //create connection with discord voice channnel
105
+ const player = client.poru.createConnection({
106
+ guildId: interaction.guild.id,
107
+ voiceChannel: interaction.member.voice.channelId,
108
+ textChannel: interaction.channel.id,
109
+ selfDeaf: true
110
+ });
111
+
112
+
113
+ if (res.loadType === 'PLAYLIST_LOADED') {
114
+ for (const track of res.tracks) {
115
+ track.info.requester = interaction.user;
116
+ player.queue.add(track);
117
+ }
118
+
119
+ interaction.reply(`${res.playlistInfo.name} has been loaded with ${res.tracks.length}`);
120
+ } else {
121
+ const track = res.tracks[0];
122
+ track.info.requester = interaction.user;
123
+ player.queue.add(track);
124
+ interacton.reply(`Queued Track \n \`${track.title}\``);
125
+ }
126
+
127
+ if (!player.isPlaying && player.isConnected) player.play();
115
128
  });
116
- // Getting tracks
117
- const resolve = await client.poru.resolve('Ignite', 'yt');
129
+
130
+ client.login('TOKEN');
118
131
  ```
119
132
 
120
133
  ## Need Help?
package/index.js CHANGED
@@ -1,20 +1,16 @@
1
+
1
2
  module.exports = {
2
3
  Node: require("./src/Node"),
3
4
  Player: require("./src/Player"),
4
- Poru: require("./src/Poru")
5
+ Poru: require("./src/Poru"),
6
+ Spotify : require("./src/platform/Spotify"),
7
+ Deezer : require("./src/platform/Deezer"),
8
+ AppleMusic : require("./src/platform/AppleMusic"),
9
+ Queue : require("./src/guild/Queue"),
10
+ Filter : require("./src/guild/Filter"),
11
+ Track: require("./src/guild/Track"),
12
+ PoruTrack : require("./src/guild/PoruTrack")
13
+
14
+
15
+
5
16
  }
6
- process.on('unhandledRejection', error => {
7
- if (error.code === '10008' || error.code === '10062') return;
8
- console.log(error.stack);
9
- });
10
- process.on('uncaughtException', (err, origin) => {
11
- console.log(err, origin);
12
- });
13
-
14
- process.on('uncaughtExceptionMonitor', (err, origin) => {
15
- console.log(err, origin);
16
- });
17
-
18
- process.on('multipleResolves', (type, promise, reason) => {
19
- console.log(type, promise, reason);
20
- });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poru",
3
- "version": "2.0.2",
3
+ "version": "3.0.2",
4
4
  "description": "A stable and powefull lavalink client with so many features",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -17,9 +17,12 @@
17
17
  },
18
18
  "homepage": "https://github.com/parasop/poru#readme",
19
19
  "dependencies": {
20
- "node-fetch": "^3.2.6",
20
+ "undici": "^5.7.0",
21
21
  "ws": "^8.8.0"
22
22
  },
23
+ "engines": {
24
+ "node": ">=16.9.0"
25
+ },
23
26
  "keywords": [
24
27
  "music",
25
28
  "lavalink",
package/src/Player.js CHANGED
@@ -2,7 +2,7 @@ const { EventEmitter } = require("events");
2
2
  const Queue = require("./guild/Queue");
3
3
  const Filters = require("./guild/Filter")
4
4
  class Player extends EventEmitter {
5
- constructor(manager,node, options) {
5
+ constructor(manager, node, options) {
6
6
  super();
7
7
 
8
8
  this.manager = manager;
@@ -11,15 +11,19 @@ class Player extends EventEmitter {
11
11
 
12
12
  this.node = node;
13
13
 
14
+ this.options = options;
15
+
14
16
  this.filters = new Filters(this, this.node)
15
17
 
16
- this.guild = options.guild.id || options.guild;
18
+ this.guildId = options.guildId;
17
19
 
18
20
  this.voiceChannel = options.voiceChannel.id || options.voiceChannel;
19
21
 
20
22
  this.textChannel = options.textChannel || null;
21
23
 
22
- this.isConnectd = false;
24
+ this.shardId = options.shardId || 1;
25
+
26
+ this.isConnected = false;
23
27
 
24
28
  this.isPlaying = false;
25
29
 
@@ -37,7 +41,7 @@ class Player extends EventEmitter {
37
41
 
38
42
  this.currentTrack = {};
39
43
 
40
- this.previousTrack = {};
44
+ this.previousTrack = null;
41
45
 
42
46
  this.voiceUpdateState = null;
43
47
 
@@ -45,31 +49,31 @@ class Player extends EventEmitter {
45
49
 
46
50
  this.on("event", (data) => (this.lavalinkEvent(data).bind(this))());
47
51
  this.on("playerUpdate", (packet) => {
48
- this.isConnectd = packet.state.connected,
52
+ this.isConnected = packet.state.connected,
49
53
  this.position = packet.state.position
50
54
  this.manager.emit("playerUpdate", this, packet);
51
55
  });
52
56
  }
53
57
 
54
- async play(options={}) {
58
+ async play(options = {}) {
55
59
 
56
60
  if (!this.queue.length) {
57
61
  return null;
58
62
  }
59
-
60
- this.currentTrack = this.queue.shift()
61
63
 
62
- if(!this.currentTrack.track){
63
- this.currentTrack = await this.currentTrack.resolve(this.manager);
64
+ this.currentTrack = this.queue.shift()
65
+
66
+ if (!this.currentTrack.track) {
67
+ this.currentTrack = await this.currentTrack.resolve(this.manager);
64
68
  }
65
69
 
66
70
  this.isPlaying = true;
67
71
  this.node.send({
68
72
  op: "play",
69
- guildId: this.guild,
73
+ guildId: this.guildId,
70
74
  track: this.currentTrack.track,
71
- noReplace:options.noReplace || true,
72
- });
75
+ noReplace: options.noReplace || true,
76
+ });
73
77
  this.position = 0;
74
78
  return this;
75
79
  }
@@ -78,11 +82,11 @@ class Player extends EventEmitter {
78
82
  stop() {
79
83
 
80
84
  this.position = 0;
81
- this.isConnectd = false
85
+ this.isConnected = false
82
86
  this.isPlaying = false;
83
87
  this.node.send({
84
88
  op: "stop",
85
- guildId: this.guild
89
+ guildId: this.guildId
86
90
  });
87
91
  return this;
88
92
  }
@@ -92,7 +96,7 @@ class Player extends EventEmitter {
92
96
 
93
97
  this.node.send({
94
98
  op: "pause",
95
- guildId: this.guild,
99
+ guildId: this.guildId,
96
100
  pause,
97
101
  });
98
102
  this.isPlaying = !pause;
@@ -106,25 +110,25 @@ class Player extends EventEmitter {
106
110
  this.position = position;
107
111
  this.node.send({
108
112
  op: "seek",
109
- guildId: this.guild,
113
+ guildId: this.guildId,
110
114
  position,
111
115
  });
112
116
  return this;
113
117
  }
114
118
 
115
- async setVolume(volume) {
119
+ setVolume(volume) {
116
120
  if (Number.isNaN(volume)) throw new RangeError("Volume level must be a number.");
117
- this.volume = volume;
121
+ this.volume = volume;
118
122
  this.node.send({
119
123
  op: "volume",
120
- guildId: this.guild,
124
+ guildId: this.guildId,
121
125
  volume: this.volume,
122
126
  });
123
127
  return this;
124
128
 
125
129
  }
126
130
 
127
- async TrackRepeat() {
131
+ TrackRepeat() {
128
132
  this.loop = 1;
129
133
  this.trackRepeat = true;
130
134
  this.queueRepeat = false;
@@ -132,70 +136,74 @@ class Player extends EventEmitter {
132
136
  }
133
137
 
134
138
 
135
- async QueueRepeat() {
139
+ QueueRepeat() {
136
140
  this.loop = 2;
137
141
  this.queueRepeat = true;
138
142
  this.trackRepeat = false;
139
143
  return this;
140
144
  }
141
145
 
142
- async DisableRepeat() {
146
+ DisableRepeat() {
143
147
  this.loop = 0;
144
148
  this.trackRepeat = false;
145
149
  this.queueRepeat = false;
146
- return this;
150
+ return this;
147
151
  }
148
152
 
149
- async setTextChannel(channel) {
153
+ setTextChannel(channel) {
150
154
  if (typeof channel !== "string") throw new RangeError("Channel must be a string.");
151
155
  this.textChannel = channel;
152
156
  return this;
153
157
  }
154
158
 
155
- async setVoiceChannel(channel) {
159
+ setVoiceChannel(channel) {
156
160
  if (typeof channel !== "string") throw new RangeError("Channel must be a string.");
157
161
  this.voiceChannel = channel;
158
162
  return this;
159
163
  }
160
164
 
161
- async connect(data) {
165
+ connect(options) {
166
+
167
+ let { guildId, voiceChannel, deaf, mute } = options;
168
+ this.send({ guild_id: guildId, channel_id: voiceChannel, self_deaf: deaf || true, self_mute: mute || false }, true);
169
+ this.isConnected = true;
170
+
171
+ }
172
+
173
+ updateSession(data) {
162
174
  if (data) {
163
175
  this.voiceUpdateState = data;
164
176
  this.node.send({
165
177
  op: "voiceUpdate",
166
- guildId: this.guild,
178
+ guildId: this.guildId,
167
179
  ...data,
168
180
  });
169
181
  }
170
182
  return this;
183
+
171
184
  }
172
185
 
173
- async reconnect() {
186
+ reconnect() {
174
187
  if (this.voiceChannel === null) return null;
175
- this.node.send({
176
- op: 4,
177
- d: {
178
- guild_id: this.guild,
188
+ this.send({
189
+ guild_id: this.guildId,
179
190
  channel_id: this.voiceChannel,
180
191
  self_mute: false,
181
192
  self_deaf: false,
182
- },
183
- });
193
+ })
194
+
184
195
  return this;
185
196
  }
186
197
 
187
- async disconnect() {
198
+ disconnect() {
188
199
  if (this.voiceChannel === null) return null;
189
200
  this.pause(true);
190
- this.isConnectd = false;
191
- this.manager.sendData({
192
- op: 4,
193
- d: {
194
- guild_id: this.guild,
195
- channel_id: null,
196
- self_mute: false,
197
- self_deaf: false,
198
- },
201
+ this.isConnected = false;
202
+ this.send({
203
+ guild_id: this.guildId,
204
+ channel_id: null,
205
+ self_mute: false,
206
+ self_deaf: false,
199
207
  });
200
208
  this.voiceChannel = null;
201
209
  return this;
@@ -205,26 +213,26 @@ class Player extends EventEmitter {
205
213
  this.disconnect();
206
214
  this.node.send({
207
215
  op: "destroy",
208
- guildId: this.guild,
216
+ guildId: this.guildId,
209
217
  });
210
218
  this.manager.emit("playerDestroy", this);
211
- this.manager.players.delete(this.guild);
219
+ this.manager.players.delete(this.guildId);
212
220
  }
213
221
 
214
- restart(){
222
+ restart() {
215
223
  this.filters.updateFilters();
216
- if(this.currentTrack){
224
+ if (this.currentTrack) {
217
225
 
218
226
  this.isPlaying = true;
219
227
  this.node.send({
220
228
  op: "play",
221
229
  startTime: this.position,
222
- noReplace:true,
223
- guildId: this.guild,
230
+ noReplace: true,
231
+ guildId: this.guildId,
224
232
  track: this.currentTrack.track,
225
- puase: this.isPaused
226
- });
227
-
233
+ pause: this.isPaused
234
+ });
235
+
228
236
 
229
237
  }
230
238
  }
@@ -236,7 +244,7 @@ class Player extends EventEmitter {
236
244
  if (!this.previousTrack) return this.stop();
237
245
  let data = `https://www.youtube.com/watch?v=${this.previousTrack.info.identifier}&list=RD${this.previousTrack.info.identifier}`;
238
246
 
239
- let response = await this.manager.resolve(data);
247
+ let response = await this.manager.resolve(data,this.manager.options.defaultPlatform || "ytsearch");
240
248
 
241
249
  if (!response || !response.tracks || ["LOAD_FAILED", "NO_MATCHES"].includes(response.loadType)) return this.stop();
242
250
 
@@ -255,12 +263,17 @@ class Player extends EventEmitter {
255
263
 
256
264
  }
257
265
 
266
+ send(data) {
267
+
268
+ this.manager.sendData({ op: 4, d: data });
269
+ }
270
+
258
271
 
259
272
  lavalinkEvent(data) {
260
273
  const events = {
261
274
  TrackStartEvent() {
262
275
  this.isPlaying = true;
263
- this.paused = false;
276
+ this.isPaused = false;
264
277
  this.manager.emit("trackStart", this, this.currentTrack, data);
265
278
  },
266
279
  // eslint-disable-next-line consistent-return
@@ -282,36 +295,34 @@ class Player extends EventEmitter {
282
295
  }
283
296
 
284
297
  if (this.queue.length === 0) {
285
- return this.manager.emit("queueEnd", this, this.track, data);
298
+ this.manager.emit("queueEnd",this, this.track, data);
299
+ return this.destroy();
300
+
286
301
  } else if (this.queue.length > 0) {
287
302
  this.manager.emit("trackEnd", this, this.currentTrack, data)
288
303
  return this.play();
289
304
  }
290
- this.manager.emit("queueEnd", this, this.track, data);
305
+ this.manager.emit("queueEnd", this, this.currentTrack, data);
306
+ this.destroy();
291
307
 
292
308
  },
293
309
  TrackStuckEvent() {
294
- this.queue.shift();
295
- this.manager.emit("trackError", this, this.track, data);
310
+ this.manager.emit("trackError", this,this.currentTrack, data);
311
+ this.stop();
312
+
296
313
  },
297
314
  TrackExceptionEvent() {
298
- this.queue.shift();
299
- /**
300
- * Fire up when there's an error while playing the track
301
- * @event trackError
302
- */
303
315
  this.manager.emit("trackError", this, this.track, data);
316
+ this.stop();
304
317
  },
305
318
  WebSocketClosedEvent() {
306
319
  if ([4015, 4009].includes(data.code)) {
307
- this.manager.sendData({
308
- op: 4,
309
- d: {
310
- guild_id: data.guildId,
311
- channel_id: this.voiceChannel.id || this.voiceChannel,
312
- self_mute: this.options.selfMute || false,
313
- self_deaf: this.options.selfDeaf || false,
314
- },
320
+ this.send({
321
+ guild_id: data.guildId,
322
+ channel_id: this.voiceChannel.id || this.voiceChannel,
323
+ self_mute: this.options.mute || false,
324
+ self_deaf: this.options.deaf || false,
325
+
315
326
  });
316
327
  }
317
328
  this.manager.emit("socketClosed", this, data);
package/src/Poru.js CHANGED
@@ -1,7 +1,5 @@
1
1
  const { EventEmitter } = require("events");
2
- const fetch = (...args) => import('node-fetch').then(({
3
- default: fetch
4
- }) => fetch(...args));
2
+ const { fetch } = require("undici")
5
3
  const config = require("./config")
6
4
  const Player = require("./Player");
7
5
  const Node = require("./Node");
@@ -12,23 +10,49 @@ const Deezer = require("./platform/Deezer")
12
10
  class Poru extends EventEmitter {
13
11
  constructor(client, nodes, options = {}) {
14
12
  super();
15
- if (!client) throw new Error("[Poru Error] you did't provide a valid client");
16
- if (!nodes) throw new Error("[Poru Error] you did't provide a lavalink nodes");
17
- if (!options) throw new Error("[Poru Error] options must be provided!")
13
+ if (!client) throw new Error("[Poru Error] You didn't provide a valid client");
14
+ if (!nodes) throw new Error("[Poru Error] You didn't provide a lavalink node");
15
+
18
16
  this.client = client;
19
17
  this._nodes = nodes;
20
18
  this.nodes = new Map();
21
19
  this.players = new Map();
22
20
  this.voiceStates = new Map();
23
21
  this.voiceServers = new Map();
22
+ this.isReady = false;
24
23
  this.user = null;
25
24
  this.options = options
26
25
  this.shards = options.shards || 1;
27
26
  this.sendData = null;
28
27
  this.version = config.version
28
+ this.spotify = new Spotify(this, this.options);
29
+ this.apple = new AppleMusic(this, this.options)
30
+ this.apple.requestToken();
31
+ this.deezer = new Deezer(this, this.options)
32
+
29
33
  }
30
34
 
31
35
 
36
+ init(client) {
37
+
38
+ if (this.isReady) return this;
39
+
40
+ this.user = client.user.id;
41
+ this.sendData = (data) => {
42
+
43
+ const guild = client.guilds.cache.get(data.d.guild_id);
44
+ if (guild) guild.shard.send(data);
45
+ }
46
+
47
+ client.on("raw", async packet => {
48
+ await this.packetUpdate(packet);
49
+ })
50
+
51
+ this._nodes.forEach((node) => this.addNode(node));
52
+ }
53
+
54
+
55
+
32
56
 
33
57
  //create a node and connect it with lavalink
34
58
  addNode(options) {
@@ -45,66 +69,89 @@ class Poru extends EventEmitter {
45
69
 
46
70
  //remove node and destroy web socket connection
47
71
  removeNode(identifier) {
72
+ if (!identifier) throw new Error(`[Poru Error] Provide identifier as a parameter of removeNode`)
48
73
  const node = this.nodes.get(identifier);
49
74
  if (!node) return;
50
75
  node.destroy();
51
76
  this.nodes.delete(identifier)
52
77
  }
53
- //create connection with discord voice channel
54
- createConnection(data = {}) {
55
- const player = this.players.get(data.guild.id || data.guild);
56
- if (player) {
57
- return player;
58
- }
59
- this.sendData({
60
- op: 4,
61
- d: {
62
- guild_id: data.guild.id || data.guild,
63
- channel_id: data.voiceChannel.id || data.voiceChannel,
64
- self_mute: data.selfMute || false,
65
- self_deaf: data.selfDeaf || true,
66
- },
67
- });
68
- return this.#Player(data);
78
+
79
+ get leastUsedNodes() {
80
+ return [...this.nodes.values()]
81
+ .filter((node) => node.isConnected)
82
+ .sort((a, b) => {
83
+ const aLoad = a.stats.cpu ? (a.stats.cpu.systemLoad / a.stats.cpu.cores) * 100 : 0;
84
+ const bLoad = b.stats.cpu ? (b.stats.cpu.systemLoad / b.stats.cpu.cores) * 100 : 0;
85
+ return aLoad - bLoad;
86
+ });
69
87
  }
70
88
 
71
89
 
90
+ getNode(identifier = "best") {
91
+ if (!this.nodes.size) throw new Error(`No nodes avaliable currently`)
92
+ if (identifier === "best") return this.leastUsedNodes();
72
93
 
73
- init(client) {
94
+ const node = this.nodes.get(indetifier);
95
+ if (!node) throw new Error('The node identifier you provided is not found');
96
+ if (!node.isConnected) node.connect();
97
+ return node;
98
+ }
74
99
 
75
- this.user = client.user.id;
76
- this.sendData = (data) => {
77
- const guild = client.guilds.cache.get(data.d.guild_id);
78
- if (guild) guild.shard.send(data);
79
- }
80
- client.on("raw", async packet => {
81
- await this.packetUpdate(packet);
82
- })
83
100
 
84
- this._nodes.forEach((node) => this.addNode(node));
101
+ createConnection(options) {
102
+ this.checkConnection(options)
103
+ const player = this.players.get(options.guildId);
104
+ if (player) return player;
85
105
 
106
+ if (this.leastUsedNodes.length === 0) throw new Error("[Poru Error] No nodes are avaliable");
107
+ const node = this.nodes.get(this.leastUsedNodes[0].name || this.leastUsedNodes[0].host);
108
+ if (!node) throw new Error("[Poru Error] No nodes are avalible");
109
+
110
+ return this.#createPlayer(node, options);
86
111
 
87
- if (this.options.spotify && this.options.spotify.clientID && this.options.spotify.clientSecret) {
88
- this.spotify = new Spotify(this, this.options)
89
- }
90
- if (this.options.apple) {
91
- if (!this.options.apple.playlistLimit) {
92
- throw new Error("[Poru Apple Music] playlistLimit must be provided")
93
- }
94
- this.apple = new AppleMusic(this, this.options)
95
- this.apple.requestToken();
96
- }
97
- if (this.options.deezer) {
98
- if (!this.options.deezer.playlistLimit) {
99
- throw new Error("[Poru Deezer Music] playlistLimit must be provided")
100
112
 
101
- }
102
- this.deezer = new Deezer(this, this.options)
103
113
 
104
- }
105
- console.log(`Thanks for using Poru`)
106
114
  }
107
115
 
116
+ removeConnection(guildId) {
117
+ this.players.get(guildId)?.destroy();
118
+ }
119
+
120
+
121
+ checkConnection(options) {
122
+
123
+ let { guildId, voiceChannel, textChannel, shardId } = options;
124
+ if (!guildId) throw new Error(`[Poru Connection] you have to Provide guildId`)
125
+ if (!voiceChannel) throw new Error(`[Poru Connection] you have to Provide voiceChannel`)
126
+ if (!textChannel) throw new Error(`[Poru Connection] you have to Provide texteChannel`);
127
+ // if(shardId == null) throw new Error(`[Poru Connection] You must have to Provide shardId`);
128
+
129
+
130
+ if (typeof guildId !== "string") throw new Error(`[Poru Connection] guildId must be provided as a string`);
131
+ if (typeof voiceChannel !== "string") throw new Error(`[Poru Connection] voiceChannel must be provided as a string`);
132
+ if (typeof textChannel !== "string") throw new Error(`[Poru Connection] textChannel must be provided as a string`);
133
+ // if(typeof shardId !=="number") throw new Error(`[Poru Connection] shardId must be provided as a number`);
134
+
135
+
136
+
137
+ }
138
+
139
+
140
+ #createPlayer(node, options) {
141
+
142
+ if (this.players.has(options.guildId)) return this.players.get(options.guildId);
143
+
144
+ const player = new Player(this, node, options);
145
+ this.players.set(options.guildId, player);
146
+ player.connect(options)
147
+ return player;
148
+ }
149
+
150
+
151
+
152
+
153
+
154
+
108
155
 
109
156
  setServersUpdate(data) {
110
157
  let guild = data.guild_id
@@ -115,7 +162,7 @@ class Poru extends EventEmitter {
115
162
  const player = this.players.get(guild);
116
163
  if (!player) return false;
117
164
 
118
- player.connect({
165
+ player.updateSession({
119
166
  sessionId: state ? state.session_id : player.voiceUpdateState.sessionId,
120
167
  event: server,
121
168
  });
@@ -134,8 +181,7 @@ class Poru extends EventEmitter {
134
181
  if (!server) return false;
135
182
  const player = this.players.get(guild);
136
183
  if (!player) return false;
137
-
138
- player.connect({
184
+ player.updateSession({
139
185
  sessionId: state ? state.session_id : player.voiceUpdateState.sessionId,
140
186
  event: server,
141
187
  });
@@ -161,61 +207,65 @@ class Poru extends EventEmitter {
161
207
 
162
208
 
163
209
 
164
- get leastUsedNodes() {
165
- return [...this.nodes.values()]
166
- .filter((node) => node.isConnected)
167
- .sort((a, b) => {
168
- const aLoad = a.stats.cpu ? (a.stats.cpu.systemLoad / a.stats.cpu.cores) * 100 : 0;
169
- const bLoad = b.stats.cpu ? (b.stats.cpu.systemLoad / b.stats.cpu.cores) * 100 : 0;
170
- return aLoad - bLoad;
171
- });
172
- }
173
210
 
174
- #Player(data) {
175
- const guild = data.guild.id || data.guild;
176
- const Nodes = this.nodes.get(guild);
177
- if (Nodes) return Nodes;
178
- if (this.leastUsedNodes.length === 0) throw new Error("[Poru Error] No nodes are avaliable");
179
- const node = this.nodes.get(this.leastUsedNodes[0].name
180
- || this.leastUsedNodes[0].host);
181
- if (!node) throw new Error("[Poru Error] No nodes are avalible");
182
211
 
183
- // eslint-disable-next-line new-cap
184
- const player = new Player(this, node, data);
185
- this.players.set(guild, player);
186
- player.connect()
187
- return player;
188
- }
189
212
 
213
+ async resolve(query, source) {
190
214
 
215
+ const node = this.leastUsedNodes[0];
216
+ if (!node) throw new Error("No nodes are available.");
217
+ const regex = /^https?:\/\//;
191
218
 
219
+ if (regex.test(query)) {
220
+ return this.fetchURL(node, query, source)
221
+ } else {
222
+ return this.fetchTrack(node, query, source)
223
+ }
192
224
 
193
- async resolve(track, source) {
225
+ }
194
226
 
195
- const node = this.leastUsedNodes[0];
196
- if (!node) throw new Error("No nodes are available.");
227
+ async fetchURL(node, track, source) {
197
228
 
198
- if (this.spotify && this.spotify.check(track)) {
229
+ if (this.spotify.check(track)) {
199
230
  return await this.spotify.resolve(track);
200
- } else if (this.apple && this.apple.check(track)) {
231
+ } else if (this.apple.check(track)) {
201
232
  return await this.apple.resolve(track);
202
- } else if (this.deezer && this.deezer.check(track)) {
233
+ } else if (this.deezer.check(track)) {
203
234
  return await this.deezer.resolve(track);
235
+ } else {
236
+ const result = await this.#fetch(node, "loadtracks", `identifier=${encodeURIComponent(track)}`);
237
+ if (!result) throw new Error("[Poru Error] No tracks found.");
238
+ return new Response(result);
204
239
  }
240
+ }
205
241
 
206
242
 
243
+ async fetchTrack(node, query, source) {
244
+ switch (source) {
245
+
246
+ case "spotify": {
247
+ return this.spotify.fetch(query)
248
+ }
249
+ case "applemusic": {
250
+ return this.apple.fetch(query)
251
+ }
252
+ case "deezer": {
253
+ return this.deezer.fetch(query);
254
+ }
255
+ default:
256
+ {
257
+ let track = `${source || "ytsearch"}:${query}`;
258
+ const result = await this.#fetch(node, "loadtracks", `identifier=${encodeURIComponent(track)}`);
259
+ if (!result) throw new Error("[Poru Error] No tracks found.");
260
+ return new Response(result);
261
+
262
+
263
+ }
207
264
 
208
- const regex = /^https?:\/\//;
209
- if (!regex.test(track)) {
210
- // eslint-disable-next-line no-param-reassign
211
- track = `${source || "ytsearch"}:${track}`;
212
265
  }
213
- const result = await this.#fetch(node, "loadtracks", `identifier=${encodeURIComponent(track)}`);
214
266
 
215
- if (!result) throw new Error("[Poru Error] No tracks found.");
216
- return new Response(result);
217
- }
218
267
 
268
+ }
219
269
 
220
270
  async decodeTrack(track) {
221
271
  const node = this.leastUsedNodes[0];
@@ -5,7 +5,6 @@ class Filters {
5
5
  this.bassboost = options.bassboost || null;
6
6
  this.player = player;
7
7
  this.node = player.node
8
- this.volume = player.volume ?? 1.0 ;
9
8
  this.equalizer = options.equalizer || [];
10
9
  this.karaoke = options.karaoke || null;
11
10
  this.timescale = options.timescale || null;
@@ -18,8 +17,36 @@ class Filters {
18
17
  }
19
18
 
20
19
 
21
- setEqualizer(bands){
22
- this.equalizer = bands;
20
+ setEqualizer(band,gain){
21
+ this.band = band || this.band;
22
+ this.gain = gain || this.gain;
23
+
24
+ this.equalizer = [
25
+ {
26
+ band: this.band,
27
+ gain: this.gain,
28
+ },
29
+ {
30
+ band: this.band,
31
+ gain: this.gain,
32
+ },
33
+ {
34
+ band: this.band,
35
+ gain: this.gain,
36
+ },
37
+ {
38
+ band: this.band,
39
+ gain: this.gain,
40
+ },
41
+ {
42
+ band: this.band,
43
+ gain: this.gain,
44
+ },
45
+ {
46
+ band: this.band,
47
+ gain: this.gain,
48
+ },
49
+ ]
23
50
  this.updateFilters();
24
51
  return this;
25
52
  }
@@ -69,16 +96,16 @@ class Filters {
69
96
  }
70
97
 
71
98
  setFilters(options) {
72
- this.player.filters = new Filters(options);
99
+ this.player.filters = new Filters(this.player,options);
73
100
  this.updateFilters();
74
101
  return this;
75
102
  }
76
103
 
77
104
  clearFilters() {
78
- this.player.filters = new Filters();
79
- this.node.send({
105
+ this.player.filters = new Filters(this.player);
106
+ this.player.node.send({
80
107
  op: "filters",
81
- guildId: this.player.guild
108
+ guildId: this.player.guildId
82
109
  });
83
110
  return this;
84
111
  }
@@ -131,21 +158,14 @@ class Filters {
131
158
  if (!this.player) return;
132
159
  this.bassboost = !!val;
133
160
  this.bassboost = val / 100;
134
- this.player.setEqualizer(
135
- val
136
- ? Array(6)
137
- .fill(0.22)
138
- .map((x, i) => ({ band: i, gain: x * val }))
139
- : []
140
- );
161
+ this.setEqualizer(1,0.90);
141
162
  }
142
163
 
143
164
  updateFilters(){
144
- const { volume, equalizer, karaoke, timescale, tremolo, vibrato, rotation, distortion, channelMix, lowPass } = this;
165
+ const {equalizer, karaoke, timescale, tremolo, vibrato, rotation, distortion, channelMix, lowPass } = this;
145
166
  this.node.send({
146
167
  op:"filters",
147
- guildId: this.player.guild,
148
- volume,
168
+ guildId: this.player.guildId,
149
169
  equalizer,
150
170
  karaoke,
151
171
  timescale,
@@ -21,7 +21,7 @@ class PoruTrack {
21
21
  const query = [this.info.author, this.info.title].filter((x) => !!x).join(' - ');
22
22
 
23
23
 
24
- const result = await manager.resolve(query);
24
+ const result = await manager.resolve(query,manager.options.defaultPlatform || "ytsearch");
25
25
  if (!result || !result.tracks.length) return;
26
26
 
27
27
  if (this.info.author) {
@@ -21,7 +21,7 @@ class Track {
21
21
  const query = [this.info.author, this.info.title].filter((x) => !!x).join(' - ');
22
22
 
23
23
 
24
- const result = await manager.resolve(query);
24
+ const result = await manager.resolve(query,manager.options.defaultPlatform || "ytsearch");
25
25
  if (!result || !result.tracks.length) return;
26
26
 
27
27
  if (this.info.author) {
@@ -1,6 +1,4 @@
1
- const fetch = (...args) => import('node-fetch').then(({
2
- default: fetch
3
- }) => fetch(...args));
1
+ const {fetch} = require("undici")
4
2
  const PoruTrack = require("../guild/PoruTrack")
5
3
  let baseURL = /(?:https:\/\/music\.apple\.com\/)(?:.+)?(artist|album|music-video|playlist)\/([\w\-\.]+(\/)+[\w\-\.]+|[^&]+)\/([\w\-\.]+(\/)+[\w\-\.]+|[^&]+)/;
6
4
 
@@ -8,12 +6,12 @@ class AppleMusic {
8
6
  constructor(manager, options) {
9
7
  this.manager = manager;
10
8
  this.options = {
11
- playlistLimit: options.apple.playlistLimit || null,
12
- albumLimit: options.apple.albumLimit || null,
13
- artistLimit: options.apple.artistLimit || null,
14
- searchMarket: options.apple.searchMarket || "us",
15
- imageHeight: options.apple.imageHeight || 500,
16
- imageWeight: options.apple.imageWeight || 500,
9
+ playlistLimit: options.playlistLimit || null,
10
+ albumLimit: options.albumLimit || null,
11
+ artistLimit: options.artistLimit || null,
12
+ searchMarket: options.searchMarket || "us",
13
+ imageHeight: options.imageHeight || 500,
14
+ imageWeight: options.imageWeight || 500,
17
15
  }
18
16
  this.url = `https://amp-api.music.apple.com/v1/catalog/${this.options.searchMarket}`
19
17
  this.token = null;
@@ -245,4 +243,4 @@ module.exports = AppleMusic
245
243
 
246
244
  const apple = new AppleMusic("",{apple:{}})
247
245
 
248
- apple.resolve("https://music.apple.com/us/playlist/bollywood-hits/pl.d60caf02fcce4d7e9788fe01243b7c2c")
246
+ apple.resolve("https://music.apple.com/us/playlist/bollywood-hits/pl.d60caf02fcce4d7e9788fe01243b7c2c")
@@ -1,6 +1,5 @@
1
- const fetch = (...args) => import('node-fetch').then(({
2
- default: fetch
3
- }) => fetch(...args));
1
+ const {fetch} = require("undici")
2
+
4
3
  let REGEX = /^(?:https?:\/\/|)?(?:www\.)?deezer\.com\/(?:\w{2}\/)?(track|album|playlist|artist)\/(\d+)/
5
4
 
6
5
  const PoruTrack = require("../guild/PoruTrack")
@@ -12,9 +11,9 @@ class Deezer {
12
11
  this.manager = manager;
13
12
  this.baseURL = 'https://api.deezer.com';
14
13
  this.options = {
15
- playlistLimit: options.deezer.playlistLimit || null,
16
- albumLimit: options.deezer.albumLimit || null,
17
- artistLimit: options.deezer.artistLimit || null
14
+ playlistLimit: options.playlistLimit || null,
15
+ albumLimit: options.albumLimit || null,
16
+ artistLimit: options.artistLimit || null
18
17
 
19
18
  }
20
19
  }
@@ -230,4 +229,4 @@ module.exports = Deezer;
230
229
  let deezer = new Deezer("", { deezer: { playlistLimit: 10 } })
231
230
 
232
231
 
233
- deezer.resolve("https://www.deezer.com/en/playlist/4404579662")
232
+ deezer.resolve("https://www.deezer.com/en/playlist/4404579662")
@@ -1,31 +1,52 @@
1
- const fetch = (...args) => import('node-fetch').then(({
2
- default: fetch
3
- }) => fetch(...args));
1
+ const {fetch} = require("undici")
2
+
4
3
  let spotifyPattern =
5
4
  /^(?:https:\/\/open\.spotify\.com\/(?:user\/[A-Za-z0-9]+\/)?|spotify:)(album|playlist|track|artist)(?:[/:])([A-Za-z0-9]+).*$/;
6
5
 
7
- const Track = require("../guild/Track")
6
+ const PoruTrack = require("../guild/PoruTrack")
8
7
 
9
8
  class Spotify {
10
9
  constructor(manager) {
11
10
  this.manager = manager;
12
11
  this.baseURL = 'https://api.spotify.com/v1';
13
12
  this.options ={
14
- clientID : manager.options.spotify.clientID,
15
- clientSecret : manager.options.spotify.clientSecret,
16
- playlistLimit : manager.options.spotify.playlistLimit,
17
- albumLimit : manager.options.spotify.albumLimit,
18
- artistLimit : manager.options.spotify.artistLimit,
19
- searchMarket : manager.options.spotify.searchMarket
13
+
14
+ playlistLimit : manager.options.playlistLimit,
15
+ albumLimit : manager.options.albumLimit,
16
+ artistLimit : manager.options.artistLimit,
17
+ searchMarket : manager.options.searchMarket
20
18
  }
21
19
  this.authorization = Buffer.from(`${this.options.clientID}:${this.options.clientSecret}`).toString('base64');
22
20
  this.interval = 0;
23
21
  }
24
22
 
25
23
  check(url) {
24
+
26
25
  return spotifyPattern.test(url);
27
26
  }
28
27
 
28
+
29
+ async requestToken() {
30
+ try{
31
+ const data = await fetch('https://open.spotify.com/get_access_token?reason=transport&productType=embed', {
32
+ headers: {
33
+ 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.99 Safari/537.36'
34
+ }
35
+ })
36
+
37
+ const body = await data.json();
38
+ this.token = `Bearer ${body.accessToken}`;
39
+ this.interval = body.accessTokenExpirationTimestampMs * 1000;
40
+
41
+ } catch (e) {
42
+ if (e.status === 400) {
43
+ throw new Error('Invalid Spotify client.');
44
+ }
45
+
46
+ }
47
+ }
48
+
49
+ /*
29
50
  async requestToken() {
30
51
 
31
52
  try {
@@ -47,7 +68,7 @@ class Spotify {
47
68
  }
48
69
  }
49
70
  }
50
-
71
+ */
51
72
  async renew() {
52
73
  if (Date.now() >= this.interval) {
53
74
  await this.requestToken();
@@ -65,7 +86,7 @@ class Spotify {
65
86
  }
66
87
 
67
88
  async resolve(url) {
68
- if (!this.token) await this.requestToken();
89
+ if (!this.token) await this.requestToken();
69
90
  const [, type, id] = spotifyPattern.exec(url) ?? [];
70
91
 
71
92
  switch (type) {
@@ -81,7 +102,6 @@ class Spotify {
81
102
  case 'artist': {
82
103
  return this.fetchArtist(id);
83
104
  }
84
-
85
105
 
86
106
  }
87
107
  }
@@ -203,7 +223,7 @@ class Spotify {
203
223
  async buildUnresolved(track) {
204
224
  if (!track) throw new ReferenceError('The Spotify track object was not provided');
205
225
 
206
- return new Track({
226
+ return new PoruTrack({
207
227
  track: '',
208
228
  info: {
209
229
  sourceName: 'spotify',