aqualink 1.0.2 → 1.0.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.
@@ -5,9 +5,17 @@ const { Track } = require("./Track");
5
5
  const { version: pkgVersion } = require("../../package.json");
6
6
 
7
7
  class Aqua extends EventEmitter {
8
+ /**
9
+ * @param {Object} client - The client instance.
10
+ * @param {Array<Object>} nodes - An array of node configurations.
11
+ * @param {Object} options - Configuration options for Aqua.
12
+ * @param {Function} options.send - Function to send data.
13
+ * @param {string} [options.defaultSearchPlatform="ytsearch"] - Default search platform.
14
+ * @param {string} [options.restVersion="v4"] - Version of the REST API.
15
+ * @param {Array<Object>} [options.plugins=[]] - Plugins to load.
16
+ */
8
17
  constructor(client, nodes, options) {
9
- super()
10
- // Input validation
18
+ super();
11
19
  if (!client) throw new Error("Client is required to initialize Aqua");
12
20
  if (!Array.isArray(nodes) || nodes.length === 0) throw new Error(`Nodes must be a non-empty Array (Received ${typeof nodes})`);
13
21
  if (typeof options.send !== "function") throw new Error("Send function is required to initialize Aqua");
@@ -30,22 +38,36 @@ class Aqua extends EventEmitter {
30
38
  this.options = options;
31
39
  this.send = options.send || null;
32
40
  }
41
+
42
+ /**
43
+ * Gets the least used nodes based on call count.
44
+ * @returns {Array<Node>} Array of least used nodes.
45
+ */
33
46
  get leastUsedNodes() {
34
47
  return [...this.nodeMap.values()]
35
48
  .filter(node => node.connected)
36
49
  .sort((a, b) => a.rest.calls - b.rest.calls);
37
50
  }
38
51
 
52
+ /**
53
+ * Initializes Aqua with the provided client ID.
54
+ * @param {string} clientId - The client ID.
55
+ * @returns {Aqua} The Aqua instance.
56
+ */
39
57
  init(clientId) {
40
58
  if (this.initiated) return this;
41
-
42
59
  this.clientId = clientId;
43
60
  this.nodes.forEach(nodeConfig => this.createNode(nodeConfig));
44
61
  this.initiated = true;
45
-
46
62
  this.plugins.forEach(plugin => plugin.load(this));
63
+ return this;
47
64
  }
48
65
 
66
+ /**
67
+ * Creates a new node with the specified options.
68
+ * @param {Object} options - The configuration for the node.
69
+ * @returns {Node} The created node instance.
70
+ */
49
71
  createNode(options) {
50
72
  const node = new Node(this, options, this.options);
51
73
  this.nodeMap.set(options.name || options.host, node);
@@ -54,29 +76,33 @@ class Aqua extends EventEmitter {
54
76
  return node;
55
77
  }
56
78
 
79
+ /**
80
+ * Destroys a node identified by the given identifier.
81
+ * @param {string} identifier - The identifier of the node to destroy.
82
+ */
57
83
  destroyNode(identifier) {
58
84
  const node = this.nodeMap.get(identifier);
59
85
  if (!node) return;
60
-
61
86
  node.disconnect();
62
87
  this.nodeMap.delete(identifier);
63
88
  this.emit("nodeDestroy", node);
64
89
  }
65
90
 
91
+ /**
92
+ * Updates the voice state based on the received packet.
93
+ * @param {Object} packet - The packet containing voice state information.
94
+ */
66
95
  updateVoiceState(packet) {
67
- if (!["VOICE_STATE_UPDATE", "VOICE_SERVER_UPDATE"].includes(packet.t)) return;
68
-
69
96
  const player = this.players.get(packet.d.guild_id);
70
97
  if (!player) return;
71
-
72
- if (packet.t === "VOICE_SERVER_UPDATE") {
73
- player.connection.setServerUpdate(packet.d);
74
- } else if (packet.t === "VOICE_STATE_UPDATE") {
75
- if (packet.d.user_id !== this.clientId) return;
76
- player.connection.setStateUpdate(packet.d);
77
- }
98
+ if (packet.t === "VOICE_SERVER_UPDATE") player.connection.setServerUpdate(packet.d);
99
+ else if (packet.t === "VOICE_STATE_UPDATE" && packet.d.user_id === this.clientId) player.connection.setStateUpdate(packet.d);
78
100
  }
79
-
101
+ /**
102
+ * Fetches nodes by the specified region.
103
+ * @param {string} region - The region to filter nodes by.
104
+ * @returns {Array<Node>} Array of nodes in the specified region.
105
+ */
80
106
  fetchRegion(region) {
81
107
  const nodesByRegion = [...this.nodeMap.values()]
82
108
  .filter(node => node.connected && node.regions?.includes(region?.toLowerCase()))
@@ -85,38 +111,54 @@ class Aqua extends EventEmitter {
85
111
  const bLoad = b.stats.cpu ? (b.stats.cpu.systemLoad / b.stats.cpu.cores) * 100 : 0;
86
112
  return aLoad - bLoad;
87
113
  });
88
-
89
114
  return nodesByRegion;
90
115
  }
91
116
 
117
+ /**
118
+ * Creates a connection for a player.
119
+ * @param {Object} options - Connection options.
120
+ * @param {string} options.guildId - The ID of the guild.
121
+ * @param {string} [options.region] - The region to connect to.
122
+ * @returns {Player} The created player instance.
123
+ */
92
124
  createConnection(options) {
93
125
  if (!this.initiated) throw new Error("BRO! Get aqua on before !!!");
94
- if (this.leastUsedNodes.length === 0) throw new Error("No nodes are available");
95
- const node = options.region
96
- ? this.fetchRegion(options.region)[0] || this.leastUsedNodes[0]
97
- : this.leastUsedNodes[0];
126
+ if (!this.leastUsedNodes.length) throw new Error("No nodes are available");
127
+
128
+ const node = (options.region ? this.fetchRegion(options.region) : this.leastUsedNodes)[0];
98
129
  if (!node) throw new Error("No nodes are available");
130
+
99
131
  return this.createPlayer(node, options);
100
132
  }
101
-
133
+ /**
134
+ * Creates a player using the specified node.
135
+ * @param {Node} node - The node to create the player with.
136
+ * @param {Object} options - The player options.
137
+ * @returns {Player} The created player instance.
138
+ */
102
139
  createPlayer(node, options) {
103
140
  const player = new Player(this, node, options);
104
141
  this.players.set(options.guildId, player);
105
142
  player.connect(options);
106
-
107
143
  this.emit("playerCreate", player);
108
144
  return player;
109
145
  }
110
-
146
+ /**
147
+ * Destroys the player associated with the given guild ID.
148
+ * @param {string} guildId - The ID of the guild.
149
+ */
111
150
  destroyPlayer(guildId) {
112
151
  const player = this.players.get(guildId);
113
152
  if (!player) return;
114
-
115
153
  player.destroy();
116
154
  this.players.delete(guildId);
117
155
  this.emit("playerDestroy", player);
118
156
  }
119
157
 
158
+ /**
159
+ * Removes the connection for the specified guild ID.
160
+ * @param {string} guildId - The ID of the guild.
161
+ */
120
162
  removeConnection(guildId) {
121
163
  const player = this.players.get(guildId);
122
164
  if (player) {
@@ -125,16 +167,23 @@ class Aqua extends EventEmitter {
125
167
  }
126
168
  }
127
169
 
170
+ /**
171
+ * Resolves a query to tracks using the available nodes.
172
+ * @param {Object} options - The options for resolving tracks.
173
+ * @param {string} options.query - The query string to resolve.
174
+ * @param {string} [options.source] - The source of the query.
175
+ * @param {Object} [options.requester] - The requester of the query.
176
+ * @param {string|Node} [options.nodes] - Specific nodes to use for the request.
177
+ * @returns {Promise<Object>} The resolved tracks and related information.
178
+ */
128
179
  async resolve({ query, source, requester, nodes }) {
129
180
  if (!this.initiated) throw new Error("Aqua must be initialized before resolving");
130
-
131
181
  if (nodes && (typeof nodes !== "string" && !(nodes instanceof Node))) {
132
182
  throw new Error(`'nodes' must be a string or Node instance, but received: ${typeof nodes}`);
133
183
  }
134
184
 
135
185
  const searchSources = source || this.defaultSearchPlatform;
136
186
  const requestNode = (typeof nodes === 'string' ? this.nodeMap.get(nodes) : nodes) || this.leastUsedNodes[0];
137
-
138
187
  if (!requestNode) throw new Error("No nodes are available.");
139
188
 
140
189
  const formattedQuery = /^https?:\/\//.test(query) ? query : `${searchSources}:${query}`;
@@ -149,6 +198,12 @@ class Aqua extends EventEmitter {
149
198
  return this.constructResponse();
150
199
  }
151
200
 
201
+ /**
202
+ * Handles cases where no matches were found for a query.
203
+ * @param {Object} rest - The REST client for making requests.
204
+ * @param {string} query - The original query string.
205
+ * @returns {Promise<Object>} The response object from the request.
206
+ */
152
207
  async handleNoMatches(rest, query) {
153
208
  let response = await rest.makeRequest("GET", `/v4/loadtracks?identifier=https://open.spotify.com/track/${query}`);
154
209
  if (["empty", "NO_MATCHES"].includes(response.loadType)) {
@@ -157,6 +212,12 @@ class Aqua extends EventEmitter {
157
212
  return response;
158
213
  }
159
214
 
215
+ /**
216
+ * Loads tracks from the resolved response.
217
+ * @param {Object} response - The response from the track resolution.
218
+ * @param {Object} requester - The requester of the tracks.
219
+ * @param {Node} requestNode - The node that handled the request.
220
+ */
160
221
  loadTracks(response, requester, requestNode) {
161
222
  this.tracks = [];
162
223
  if (response.loadType === "track") {
@@ -169,11 +230,14 @@ class Aqua extends EventEmitter {
169
230
  } else if (response.loadType === "search") {
170
231
  this.tracks = response.data.map(track => new Track(track, requester, requestNode));
171
232
  }
172
-
173
233
  this.loadType = response.loadType;
174
234
  this.pluginInfo = response.pluginInfo || {};
175
235
  }
176
236
 
237
+ /**
238
+ * Constructs the response object for the resolved tracks.
239
+ * @returns {Object} The constructed response.
240
+ */
177
241
  constructResponse() {
178
242
  return {
179
243
  loadType: this.loadType,
@@ -184,13 +248,30 @@ class Aqua extends EventEmitter {
184
248
  };
185
249
  }
186
250
 
251
+ /**
252
+ * Gets the player associated with the specified guild ID.
253
+ * @param {string} guildId - The ID of the guild.
254
+ * @returns {Player} The player instance.
255
+ * @throws {Error} If the player is not found.
256
+ */
187
257
  get(guildId) {
188
258
  const player = this.players.get(guildId);
189
259
  if (!player) throw new Error(`Player not found for guild ID: ${guildId}`);
190
260
  return player;
191
261
  }
262
+
263
+ /**
264
+ * Cleans up idle players and nodes to free up resources.
265
+ */
266
+ cleanupIdle() {
267
+ for (const [guildId, player] of this.players) {
268
+ if (!player.playing && !player.paused && player.queue.isEmpty()) {
269
+ player.destroy();
270
+ this.players.delete(guildId);
271
+ this.emit("playerDestroy", player);
272
+ }
273
+ }
274
+ }
192
275
  }
193
276
 
194
277
  module.exports = { Aqua };
195
-
196
-
@@ -22,7 +22,7 @@ class Connection {
22
22
  if (!endpoint) {
23
23
  throw new Error("Missing 'endpoint' property in VOICE_SERVER_UPDATE packet/payload. Please wait or disconnect the bot from the voice channel and try again.");
24
24
  }
25
-
25
+
26
26
  const previousVoiceRegion = this.region;
27
27
  this.voice.endpoint = endpoint;
28
28
  this.voice.token = token;
@@ -45,7 +45,10 @@ class Connection {
45
45
  * @param {boolean} data.self_mute The self-muted status of the player.
46
46
  */
47
47
  setStateUpdate({ session_id, channel_id, self_deaf, self_mute }) {
48
- if (channel_id == null) {
48
+ if (channel_id == null || session_id == null) {
49
+ this.player.aqua.emit("playerLeave", this.player.voiceChannel);
50
+ this.player.voiceChannel = null;
51
+ this.voiceChannel = null;
49
52
  this.player.destroy();
50
53
  this.player.aqua.emit("playerDestroy", this.player);
51
54
  return;
@@ -59,7 +62,7 @@ class Connection {
59
62
 
60
63
  this.selfDeaf = self_deaf;
61
64
  this.selfMute = self_mute;
62
- this.voice.sessionId = session_id || null;
65
+ this.voice.sessionId = session_id;
63
66
 
64
67
  this.updatePlayerVoiceData();
65
68
  }
@@ -1,12 +1,12 @@
1
1
  const { EventEmitter } = require("events");
2
2
  const { Connection } = require("./Connection");
3
3
  const { Queue } = require("./Queue");
4
- const {Filters} = require("./Filters");
4
+ const { Filters } = require("./Filters");
5
5
 
6
6
  class Player extends EventEmitter {
7
7
  /**
8
8
  * @param {Object} aqua - The Aqua instance.
9
- * @param {Object} node - The node instance.
9
+ * @param {Object} nodes - The node instances.
10
10
  * @param {Object} options - Configuration options for the player.
11
11
  * @param {string} options.guildId - The ID of the guild.
12
12
  * @param {string} options.textChannel - The ID of the text channel.
@@ -40,24 +40,33 @@ class Player extends EventEmitter {
40
40
  this.timestamp = 0;
41
41
  this.ping = 0;
42
42
  this.isAutoplay = false;
43
+
43
44
  this.setupEventListeners();
44
45
  }
45
46
 
47
+ /**
48
+ * Sets up event listeners for player events.
49
+ */
46
50
  setupEventListeners() {
47
- this.on("playerUpdate", this.onPlayerUpdate);
51
+ this.on("playerUpdate", this.onPlayerUpdate.bind(this));
48
52
  this.on("event", this.handleEvent.bind(this));
49
53
  }
50
54
 
55
+ /**
56
+ * Handles player update events.
57
+ * @param {Object} packet - The packet containing the player update data.
58
+ */
51
59
  onPlayerUpdate(packet) {
52
60
  const { state } = packet;
53
61
  this.connected = state.connected;
54
62
  this.position = state.position;
55
63
  this.ping = state.ping;
56
64
  this.timestamp = state.time;
65
+
57
66
  }
58
67
 
59
68
  /**
60
- * Get the previous track.
69
+ * Gets the previous track.
61
70
  * @returns {Object|null} The previous track or null if none exists.
62
71
  */
63
72
  get previous() {
@@ -65,7 +74,7 @@ class Player extends EventEmitter {
65
74
  }
66
75
 
67
76
  /**
68
- * Add a track to the previous tracks list.
77
+ * Adds a track to the previous tracks list.
69
78
  * @param {Object} track - The track object to add.
70
79
  */
71
80
  addToPreviousTrack(track) {
@@ -73,13 +82,12 @@ class Player extends EventEmitter {
73
82
  }
74
83
 
75
84
  /**
76
- * Play the next track in the queue.
85
+ * Plays the next track in the queue.
77
86
  * @returns {Promise<Player>} The player instance.
78
87
  * @throws {Error} If the player is not connected.
79
88
  */
80
89
  async play() {
81
90
  if (!this.connected) throw new Error("Player connection is not established. Please connect first.");
82
-
83
91
  this.current = this.queue.shift();
84
92
  if (!this.current) return this;
85
93
 
@@ -90,19 +98,12 @@ class Player extends EventEmitter {
90
98
  this.playing = true;
91
99
  this.position = 0;
92
100
 
93
- // Log info for debugging
94
101
  this.aqua.emit("debug", this.guildId, `Playing track: ${this.current.track}`);
95
-
96
- await this.nodes.rest.updatePlayer({
97
- guildId: this.guildId,
98
- data: {
99
- track: { encoded: this.current.track },
100
- },
101
- });
102
+ await this.updatePlayer({ track: { encoded: this.current.track } });
102
103
  return this;
103
104
  }
104
105
  /**
105
- * Connect to the voice channel.
106
+ * Connects the player to the voice channel.
106
107
  * @param {Object} [options=this] - Connection options.
107
108
  * @param {string} options.guildId - The ID of the guild.
108
109
  * @param {string} options.voiceChannel - The ID of the voice channel.
@@ -121,18 +122,20 @@ class Player extends EventEmitter {
121
122
  this.connected = true;
122
123
  this.aqua.emit("debug", this.guildId, `Player has connected to voice channel: ${voiceChannel}.`);
123
124
  }
125
+
124
126
  /**
125
- * Disconnect the player from the voice channel.
127
+ * Disconnects the player from the voice channel and cleans up resources.
126
128
  * @returns {Promise<Player>} The player instance.
127
129
  */
128
130
  async destroy() {
129
131
  await this.updatePlayer({ track: null });
130
132
  this.connected = false;
133
+ this.clearData(); // Clear data when destroyed
131
134
  this.aqua.emit("debug", this.guildId, "Player has disconnected from voice channel.");
132
135
  }
133
136
 
134
137
  /**
135
- * Pause or resume the player.
138
+ * Pauses or resumes the player.
136
139
  * @param {boolean} paused - Whether to pause the player.
137
140
  * @returns {Promise<Player>} The player instance.
138
141
  */
@@ -143,7 +146,7 @@ class Player extends EventEmitter {
143
146
  }
144
147
 
145
148
  /**
146
- * Seek to a specific position in the current track.
149
+ * Seeks to a specific position in the current track.
147
150
  * @param {number} position - The position in milliseconds to seek to.
148
151
  * @returns {Promise<Player>} The player instance.
149
152
  */
@@ -154,7 +157,7 @@ class Player extends EventEmitter {
154
157
  }
155
158
 
156
159
  /**
157
- * Stop playback and clear the current track.
160
+ * Stops playback and clears the current track.
158
161
  * @returns {Promise<Player>} The player instance.
159
162
  */
160
163
  async stop() {
@@ -163,7 +166,7 @@ class Player extends EventEmitter {
163
166
  }
164
167
 
165
168
  /**
166
- * Set the volume of the player.
169
+ * Sets the volume of the player.
167
170
  * @param {number} volume - The volume level (0-200).
168
171
  * @returns {Promise<Player>} The player instance.
169
172
  * @throws {Error} If the volume is out of bounds.
@@ -176,7 +179,7 @@ class Player extends EventEmitter {
176
179
  }
177
180
 
178
181
  /**
179
- * Set the loop mode for the player.
182
+ * Sets the loop mode for the player.
180
183
  * @param {string} mode - The loop mode ('none', 'track', 'queue').
181
184
  * @returns {Promise<Player>} The player instance.
182
185
  * @throws {Error} If the loop mode is invalid.
@@ -189,7 +192,7 @@ class Player extends EventEmitter {
189
192
  }
190
193
 
191
194
  /**
192
- * Send data to the player.
195
+ * Sends data to the player.
193
196
  * @param {Object} data - The data to send.
194
197
  * @returns {Promise<Player>} The player instance.
195
198
  */
@@ -199,7 +202,7 @@ class Player extends EventEmitter {
199
202
  }
200
203
 
201
204
  /**
202
- * Set the text channel for the player.
205
+ * Sets the text channel for the player.
203
206
  * @param {string} channel - The ID of the text channel.
204
207
  * @returns {Promise<Player>} The player instance.
205
208
  */
@@ -209,7 +212,7 @@ class Player extends EventEmitter {
209
212
  }
210
213
 
211
214
  /**
212
- * Set the voice channel for the player.
215
+ * Sets the voice channel for the player.
213
216
  * @param {string} channel - The ID of the voice channel.
214
217
  * @returns {Promise<Player>} The player instance.
215
218
  * @throws {TypeError} If the channel is not a string.
@@ -221,18 +224,12 @@ class Player extends EventEmitter {
221
224
  throw new ReferenceError(`Player is already connected to ${channel}.`);
222
225
  }
223
226
  this.voiceChannel = channel;
224
- await this.connect({
225
- deaf: this.deaf,
226
- guildId: this.guildId,
227
- voiceChannel: this.voiceChannel,
228
- textChannel: this.textChannel,
229
- mute: this.mute,
230
- });
227
+ await this.connect({ deaf: this.deaf, guildId: this.guildId, voiceChannel: this.voiceChannel, mute: this.mute });
231
228
  return this;
232
229
  }
233
230
 
234
231
  /**
235
- * Disconnect the player from the voice channel.
232
+ * Disconnects the player from the voice channel.
236
233
  * @returns {Promise<Player>} The player instance.
237
234
  */
238
235
  async disconnect() {
@@ -242,12 +239,13 @@ class Player extends EventEmitter {
242
239
  }
243
240
 
244
241
  /**
245
- * Handle events from the player.
242
+ * Handles events from the player.
246
243
  * @param {Object} payload - The event payload.
247
244
  */
248
245
  async handleEvent(payload) {
249
246
  const player = this.aqua.players.get(payload.guildId);
250
247
  if (!player) return;
248
+
251
249
  switch (payload.type) {
252
250
  case "TrackStartEvent":
253
251
  this.trackStart(player, payload);
@@ -271,7 +269,7 @@ class Player extends EventEmitter {
271
269
  }
272
270
 
273
271
  /**
274
- * Handle track start events.
272
+ * Handles track start events.
275
273
  * @param {Object} player - The player instance.
276
274
  * @param {Object} payload - The event payload.
277
275
  */
@@ -282,7 +280,7 @@ class Player extends EventEmitter {
282
280
  }
283
281
 
284
282
  /**
285
- * Handle track end events.
283
+ * Handles track end events.
286
284
  * @param {Object} player - The player instance.
287
285
  * @param {Object} payload - The event payload.
288
286
  */
@@ -315,7 +313,7 @@ class Player extends EventEmitter {
315
313
  }
316
314
 
317
315
  /**
318
- * Handle track error events.
316
+ * Handles track error events.
319
317
  * @param {Object} player - The player instance.
320
318
  * @param {Object} payload - The event payload.
321
319
  */
@@ -325,7 +323,7 @@ class Player extends EventEmitter {
325
323
  }
326
324
 
327
325
  /**
328
- * Handle track stuck events.
326
+ * Handles track stuck events.
329
327
  * @param {Object} player - The player instance.
330
328
  * @param {Object} payload - The event payload.
331
329
  */
@@ -335,7 +333,7 @@ class Player extends EventEmitter {
335
333
  }
336
334
 
337
335
  /**
338
- * Handle socket closed events.
336
+ * Handles socket closed events.
339
337
  * @param {Object} player - The player instance.
340
338
  * @param {Object} payload - The event payload.
341
339
  */
@@ -354,7 +352,7 @@ class Player extends EventEmitter {
354
352
  }
355
353
 
356
354
  /**
357
- * Send data to the Aqua instance.
355
+ * Sends data to the Aqua instance.
358
356
  * @param {Object} data - The data to send.
359
357
  */
360
358
  send(data) {
@@ -362,34 +360,34 @@ class Player extends EventEmitter {
362
360
  }
363
361
 
364
362
  /**
365
- * Set a custom value in the player's data.
363
+ * Sets a custom value in the player's data.
366
364
  * @param {string} key - The key of the data.
367
365
  * @param {any} value - The value to set.
368
366
  */
369
367
  set(key, value) {
370
- this.data[key] = value;
368
+ this.data.set(key, value); // Use WeakMap to set data
371
369
  }
372
370
 
373
371
  /**
374
- * Get a custom value from the player's data.
372
+ * Gets a custom value from the player's data.
375
373
  * @param {string} key - The key of the data.
376
374
  * @returns {any} The value associated with the key.
377
375
  */
378
376
  get(key) {
379
- return this.data[key];
377
+ return this.data.get(key); // Use WeakMap to get data
380
378
  }
381
379
 
382
380
  /**
383
- * Clear all custom data set on the player.
381
+ * Clears all custom data set on the player.
384
382
  * @returns {Player} The player instance.
385
383
  */
386
384
  clearData() {
387
- this.data = {}; // Clear all custom data efficiently
385
+ this.data = {} // Clear all custom data efficiently
388
386
  return this;
389
387
  }
390
388
 
391
389
  /**
392
- * Update the player with new data.
390
+ * Updates the player with new data.
393
391
  * @param {Object} data - The data to update the player with.
394
392
  * @returns {Promise<void>}
395
393
  */
@@ -401,15 +399,23 @@ class Player extends EventEmitter {
401
399
  }
402
400
 
403
401
  /**
404
- * Handle unknown events from the node.
402
+ * Handles unknown events from the node.
405
403
  * @param {Object} payload - The event payload.
406
404
  */
407
405
  handleUnknownEvent(payload) {
408
406
  const error = new Error(`Node encountered an unknown event: '${payload.type}'`);
409
407
  this.aqua.emit("nodeError", this, error);
410
408
  }
409
+
410
+ /**
411
+ * Cleans up the player when idle.
412
+ * @returns {Promise<void>}
413
+ */
414
+ async cleanup() {
415
+ if (!this.playing && !this.paused && this.queue.isEmpty()) {
416
+ await this.destroy();
417
+ }
418
+ }
411
419
  }
412
420
 
413
421
  module.exports = { Player };
414
-
415
-
@@ -26,9 +26,8 @@ class Queue extends Array {
26
26
  * @param {*} track - The track to add.
27
27
  */
28
28
  add(track) {
29
- if (track) {
30
- this.push(track);
31
- }
29
+ this.push(track);
30
+ return this;
32
31
  }
33
32
 
34
33
  /**
@@ -79,6 +78,15 @@ class Queue extends Array {
79
78
  return this.shift(); // Removes and returns the first element
80
79
  }
81
80
 
81
+ // Check if the queue is empty
82
+ /**
83
+ * Check if the queue is empty.
84
+ * @returns {boolean} Whether the queue is empty.
85
+ */
86
+ isEmpty() {
87
+ return this.length === 0;
88
+ }
89
+
82
90
  /**
83
91
  * Add multiple tracks to the queue.
84
92
  * @param {Array} tracks - The tracks to add.
@@ -88,6 +96,8 @@ class Queue extends Array {
88
96
  this.push(...tracks);
89
97
  }
90
98
  }
99
+
91
100
  }
92
101
 
93
102
  module.exports = { Queue };
103
+
@@ -9,6 +9,10 @@ class Rest {
9
9
  this.password = options.password;
10
10
  this.version = options.restVersion;
11
11
  this.calls = 0;
12
+ this.queue = [];
13
+ this.maxQueueSize = options.maxQueueSize || 100;
14
+ this.maxConcurrentRequests = options.maxConcurrentRequests || 5;
15
+ this.activeRequests = 0;
12
16
  }
13
17
 
14
18
  setSessionId(sessionId) {
@@ -20,24 +24,20 @@ class Rest {
20
24
  "Content-Type": "application/json",
21
25
  Authorization: this.password,
22
26
  };
23
-
24
27
  const requestOptions = {
25
28
  method,
26
29
  headers,
27
30
  body: body ? JSON.stringify(body) : null,
28
31
  };
29
-
30
32
  try {
31
33
  const response = await undiciFetch(`${this.url}${endpoint}`, requestOptions);
32
34
  this.calls++;
33
35
  const data = await this.parseResponse(response);
34
-
35
36
  this.aqua.emit("apiResponse", endpoint, response);
36
37
  this.aqua.emit(
37
38
  "debug",
38
39
  `[Rest] ${method} ${endpoint} ${body ? `body: ${JSON.stringify(body)}` : ""} -> Status Code: ${response.status} Response: ${nodeUtil.inspect(data)}`
39
40
  );
40
-
41
41
  return includeHeaders ? { data, headers: response.headers } : data;
42
42
  } catch (error) {
43
43
  throw new Error(`Network error during request: ${method} ${this.url}${endpoint}`, { cause: error });
@@ -50,18 +50,15 @@ class Rest {
50
50
 
51
51
  async updatePlayer(options) {
52
52
  const requestBody = { ...options.data };
53
-
54
53
  if ((requestBody.track && requestBody.track.encoded && requestBody.track.identifier) ||
55
54
  (requestBody.encodedTrack && requestBody.identifier)) {
56
55
  throw new Error(`Cannot provide both 'encoded' and 'identifier' for track in Update Player Endpoint`);
57
56
  }
58
-
59
57
  if (this.version === "v3" && options.data?.track) {
60
58
  const { track } = requestBody;
61
59
  delete requestBody.track;
62
60
  Object.assign(requestBody, track.encoded ? { encodedTrack: track.encoded } : { identifier: track.identifier });
63
61
  }
64
-
65
62
  return this.makeRequest("PATCH", `/${this.version}/sessions/${this.sessionId}/players/${options.guildId}?noReplace=false`, requestBody);
66
63
  }
67
64
 
@@ -85,7 +82,6 @@ class Rest {
85
82
  if (this.version === "v3") {
86
83
  return this.makeRequest("GET", `/${this.version}/stats`);
87
84
  }
88
-
89
85
  return this.makeRequest("GET", `/${this.version}/stats/all`);
90
86
  }
91
87
 
@@ -105,7 +101,6 @@ class Rest {
105
101
  if (response.status === 204) {
106
102
  return null;
107
103
  }
108
-
109
104
  try {
110
105
  const contentType = response.headers.get("Content-Type");
111
106
  return await response[contentType.includes("text/plain") ? "text" : "json"]();
@@ -114,6 +109,44 @@ class Rest {
114
109
  return null;
115
110
  }
116
111
  }
112
+
113
+ /**
114
+ * Adds a request to the queue and processes it.
115
+ * @param {function} requestFunction - The request function to execute.
116
+ */
117
+ async queueRequest(requestFunction) {
118
+ if (this.queue.length >= this.maxQueueSize) {
119
+ this.aqua.emit("debug", "[Rest] Queue is full, discarding oldest request.");
120
+ this.queue.shift();
121
+ }
122
+ this.queue.push(requestFunction);
123
+ this.processQueue();
124
+ }
125
+
126
+ /**
127
+ * Processes the queue of requests with concurrency control.
128
+ */
129
+ async processQueue() {
130
+ while (this.activeRequests < this.maxConcurrentRequests && this.queue.length > 0) {
131
+ const requestFunction = this.queue.shift();
132
+ this.activeRequests++;
133
+ try {
134
+ await requestFunction();
135
+ } catch (error) {
136
+ this.aqua.emit("error", error);
137
+ } finally {
138
+ this.activeRequests--;
139
+ this.processQueue();
140
+ }
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Cleans up resources related to the queue.
146
+ */
147
+ cleanupQueue() {
148
+ this.queue = [];
149
+ }
117
150
  }
118
151
 
119
152
  module.exports = { Rest };
@@ -16,12 +16,7 @@ class Track {
16
16
  this.info = data.info;
17
17
  this.requester = requester;
18
18
  this.nodes = nodes;
19
-
20
- if (data.encoded) {
21
- this.track = data.encoded;
22
- } else {
23
- this.track = Buffer.from(data.track, "base64").toString("utf8");
24
- }
19
+ this.track = data.encoded || Buffer.from(data.track, "base64").toString("utf8");
25
20
  }
26
21
 
27
22
  /**
@@ -29,8 +24,7 @@ class Track {
29
24
  * @returns {string|null}
30
25
  */
31
26
  resolveThumbnail(thumbnail) {
32
- if (!thumbnail) return null;
33
- return thumbnail.startsWith("http") ? thumbnail : getImageUrl(thumbnail, this.nodes);
27
+ return thumbnail ? (thumbnail.startsWith("http") ? thumbnail : getImageUrl(thumbnail, this.nodes)) : null;
34
28
  }
35
29
 
36
30
  /**
@@ -40,16 +34,9 @@ class Track {
40
34
  async resolve(aqua) {
41
35
  const query = `${this.info.author} - ${this.info.title}`;
42
36
  const result = await aqua.resolve({ query, source: aqua.options.defaultSearchPlatform, requester: this.requester, node: this.nodes });
43
-
44
- if (!result || !result.tracks.length) return null;
45
-
46
- const matchedTrack = this.findBestMatch(result.tracks);
47
- if (matchedTrack) {
48
- this.updateTrackInfo(matchedTrack);
49
- return this;
50
- }
51
- this.updateTrackInfo(result.tracks[0]);
52
- return this;
37
+ const matchedTrack = result?.tracks?.length ? this.findBestMatch(result.tracks) || result.tracks[0] : null;
38
+ if (matchedTrack) this.updateTrackInfo(matchedTrack);
39
+ return matchedTrack ? this : null;
53
40
  }
54
41
 
55
42
  /**
@@ -57,23 +44,16 @@ class Track {
57
44
  * @returns {Track|null}
58
45
  */
59
46
  findBestMatch(tracks) {
60
- const titleLower = this.info.title.toLowerCase();
61
- const authorLower = this.info.author.toLowerCase();
62
- const exactMatch = tracks.find(track =>
63
- track.info.author.toLowerCase() === authorLower && track.info.title.toLowerCase() === titleLower
64
- );
65
- if (exactMatch) return exactMatch;
66
- const authorMatch = tracks.find(track => track.info.author.toLowerCase() === authorLower);
67
- if (authorMatch) return authorMatch;
68
- const titleMatch = tracks.find(track => track.info.title.toLowerCase() === titleLower);
69
- if (titleMatch) return titleMatch;
70
- if (this.info.length) {
71
- return tracks.find(track =>
72
- track.info.length >= (this.info.length - 2000) && track.info.length <= (this.info.length + 2000)
73
- );
74
- }
75
-
76
- return null;
47
+ const { title, author, length } = this.info;
48
+ const titleLower = title.toLowerCase();
49
+ const authorLower = author.toLowerCase();
50
+ return tracks.find(track => {
51
+ const { author, title, length: tLength } = track.info;
52
+ return (author.toLowerCase() === authorLower && title.toLowerCase() === titleLower) ||
53
+ (author.toLowerCase() === authorLower) ||
54
+ (title.toLowerCase() === titleLower) ||
55
+ (length && tLength >= (length - 2000) && tLength <= (length + 2000));
56
+ });
77
57
  }
78
58
 
79
59
  /**
@@ -88,9 +68,7 @@ class Track {
88
68
  * @private
89
69
  */
90
70
  cleanup() {
91
- this.rawData = null;
92
- this.track = null;
93
- this.info = null;
71
+ this.rawData = this.track = this.info = null;
94
72
  }
95
73
  }
96
74
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aqualink",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "An Lavalink wrapper, focused in speed, performance, and features, Based in Riffy!",
5
5
  "main": "build/index.js",
6
6
  "scripts": {