aqualink 1.0.5 → 1.1.0

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
@@ -20,6 +20,27 @@ This code is based in riffy, but its an 100% Rewrite made from scratch...
20
20
  # Docs (Wiki)
21
21
  - https://github.com/ToddyTheNoobDud/AquaLink/wiki
22
22
 
23
+ # Yay, Version 1.1.0 is released ! aqualink so cool
24
+
25
+ + Fixed stop
26
+ + Fixed Destroy
27
+ + Fixed disconnect
28
+ + Improved events
29
+ + Optimize more
30
+ + Improved queue system
31
+ + Improved speed
32
+ + Remove useless code
33
+ + Add more features...
34
+ + Fixed some resolve() methods
35
+ + Improved debug / logging
36
+ + Add new option: shouldDeleteMessage (true, false, if true will delete the nowPlayingMessage, false will not delete)
37
+ + Add nowPlayingMessage
38
+ + Rewrited REST Manager (1,5x faster, less memory usage, less cpu usage, less latency)
39
+ + Rewrited NODE Manager (A bit faster, less memory usage, less cpu, Less temp objects, better error handling)
40
+ + Rewrite Connection Manager (Faster, less bugs, less useless checking, fixed an random memory leak)
41
+ + Updated Aqua.js (added shouldDeleteMessage, some misc update for playlist)
42
+ + Added playlist support for Track (testing)
43
+
23
44
  # How to install
24
45
 
25
46
  `npm install aqualink`
@@ -13,6 +13,7 @@ class Aqua extends EventEmitter {
13
13
  * @param {string} [options.defaultSearchPlatform="ytsearch"] - Default search platform.
14
14
  * @param {string} [options.restVersion="v4"] - Version of the REST API.
15
15
  * @param {Array<Object>} [options.plugins=[]] - Plugins to load.
16
+ * @param {string} [options.shouldDeleteMessage='none'] - Should delete your message? (true, false)
16
17
  */
17
18
  constructor(client, nodes, options) {
18
19
  super();
@@ -27,6 +28,7 @@ class Aqua extends EventEmitter {
27
28
  this.clientId = null;
28
29
  this.initiated = false;
29
30
  this.sessionId = null;
31
+ this.shouldDeleteMessage = options.shouldDeleteMessage || "false";
30
32
  this.defaultSearchPlatform = options.defaultSearchPlatform || "ytmsearch";
31
33
  this.restVersion = options.restVersion || "v3";
32
34
  this.plugins = options.plugins || [];
@@ -225,21 +227,28 @@ class Aqua extends EventEmitter {
225
227
  * @param {Node} requestNode - The node that handled the request.
226
228
  */
227
229
  loadTracks(response, requester, requestNode) {
228
- this.tracks = [];
229
- if (response.loadType === "track") {
230
- if (response.data) {
231
- this.tracks.push(new Track(response.data, requester, requestNode));
232
- }
233
- } else if (response.loadType === "playlist") {
234
- this.tracks = response.data?.tracks.map(track => new Track(track, requester, requestNode)) || [];
235
- this.playlistInfo = response.data?.info || null;
236
- } else if (response.loadType === "search") {
237
- this.tracks = response.data.map(track => new Track(track, requester, requestNode));
230
+
231
+ switch (response.loadType) {
232
+ case "track":
233
+ if (response.data) {
234
+ this.tracks.push(new Track(response.data, requester, requestNode));
235
+ }
236
+ break;
237
+ case "playlist":
238
+ this.tracks = response.data?.tracks?.map(track => new Track(track, requester, requestNode)) || [];
239
+ this.playlistInfo = {
240
+ name: response.data?.info?.name || response.data?.info?.title,
241
+ ...response.data?.info,
242
+ } || null;
243
+ break;
244
+ case "search":
245
+ this.tracks = response.data?.map(track => new Track(track, requester, requestNode));
246
+ break;
238
247
  }
248
+
239
249
  this.loadType = response.loadType;
240
250
  this.pluginInfo = response.pluginInfo || {};
241
251
  }
242
-
243
252
  /**
244
253
  * Constructs the response object for the resolved tracks.
245
254
  * @returns {Object} The constructed response.
@@ -254,6 +263,7 @@ class Aqua extends EventEmitter {
254
263
  };
255
264
  }
256
265
 
266
+
257
267
  /**
258
268
  * Gets the player associated with the specified guild ID.
259
269
  * @param {string} guildId - The ID of the guild.
@@ -10,6 +10,8 @@ class Connection {
10
10
  this.selfDeaf = false;
11
11
  this.selfMute = false;
12
12
  this.voiceChannel = player.voiceChannel;
13
+ this.lastUpdateTime = 0; // Track the last update time to throttle updates
14
+ this.updateThrottle = 1000; // Throttle updates to every 1000 ms (1 second)
13
15
  }
14
16
 
15
17
  /**
@@ -27,6 +29,7 @@ class Connection {
27
29
  this.voice.endpoint = endpoint;
28
30
  this.voice.token = token;
29
31
  this.region = endpoint.split(".")[0].replace(/[0-9]/g, "");
32
+
30
33
  this.player.aqua.emit("debug", `[Player ${this.player.guildId} - CONNECTION] ${previousVoiceRegion ? `Changed Voice Region from ${previousVoiceRegion} to ${this.region}` : `Voice Server: ${this.region}`}`);
31
34
 
32
35
  if (this.player.paused) {
@@ -70,15 +73,15 @@ class Connection {
70
73
  /**
71
74
  * Updates the player voice data.
72
75
  */
73
- updatePlayerVoiceData() {
74
- this.player.nodes.rest.updatePlayer({
75
- guildId: this.player.guildId,
76
- data: {
77
- voice: this.voice,
78
- volume: this.player.volume
79
- }
80
- });
81
- }
82
- }
76
+ updatePlayerVoiceData() {
77
+ this.player.nodes.rest.updatePlayer({
78
+ guildId: this.player.guildId,
79
+ data: {
80
+ voice: this.voice,
81
+ volume: this.player.volume
82
+ }
83
+ });
84
+ }
85
+ }
83
86
 
84
- module.exports = { Connection };
87
+ module.exports = { Connection };
@@ -1,4 +1,3 @@
1
-
2
1
  const WebSocket = require("ws");
3
2
  const { Rest } = require("./Rest");
4
3
 
@@ -19,22 +18,18 @@ class Node {
19
18
  this.secure = nodes.secure || false;
20
19
  this.sessionId = nodes.sessionId || null;
21
20
  this.rest = new Rest(aqua, this);
22
-
23
21
  this.wsUrl = `ws${this.secure ? 's' : ''}://${this.host}:${this.port}/v4/websocket`;
24
- this.restUrl = `http${this.secure ? 's' : ''}://${this.host}:${this.port}`;
25
-
26
22
  this.ws = null;
27
23
  this.regions = nodes.regions || [];
28
24
  this.info = null;
29
25
  this.connected = false;
30
-
31
26
  this.resumeKey = options.resumeKey || null;
32
27
  this.resumeTimeout = options.resumeTimeout || 60;
33
28
  this.autoResume = options.autoResume || false;
34
-
35
29
  this.reconnectTimeout = options.reconnectTimeout || 5000;
36
30
  this.reconnectTries = options.reconnectTries || 3;
37
31
  this.reconnectAttempted = 0;
32
+ this.lastStatsRequest = 0; // Track the last time stats were requested
38
33
  }
39
34
 
40
35
  initializeStats() {
@@ -61,16 +56,9 @@ class Node {
61
56
  };
62
57
  }
63
58
 
64
- /**
65
- * Fetches the lavalink node's information.
66
- * @param {Object} [options] Options to pass to the rest request.
67
- * @param {boolean} [options.includeHeaders=false] Include headers in the response.
68
- * @returns {Promise<Object>} The lavalink node's information.
69
- */
70
59
  async fetchInfo(options = {}) {
71
60
  return await this.rest.makeRequest("GET", `/v4/info`, null, options.includeHeaders);
72
61
  }
73
-
74
62
 
75
63
  async connect() {
76
64
  if (this.ws) this.ws.close();
@@ -100,12 +88,12 @@ class Node {
100
88
  async onOpen() {
101
89
  this.connected = true;
102
90
  this.aqua.emit('debug', this.name, `Connected to Lavalink at ${this.wsUrl}`);
103
-
104
- this.info = await this.fetchInfo()
105
- .catch(err => {
106
- this.aqua.emit('debug', `Failed to fetch info: ${err.message}`);
107
- return null;
108
- });
91
+ try {
92
+ this.info = await this.fetchInfo();
93
+ } catch (err) {
94
+ this.aqua.emit('debug', `Failed to fetch info: ${err.message}`);
95
+ this.info = null;
96
+ }
109
97
 
110
98
  if (!this.info && !this.aqua.bypassChecks.nodeFetchInfo) {
111
99
  throw new Error(`Failed to fetch node info.`);
@@ -114,30 +102,26 @@ class Node {
114
102
  if (this.autoResume) {
115
103
  this.resumePlayers();
116
104
  }
117
-
118
105
  this.lastStats = 0;
119
106
  }
120
107
 
121
108
  async getStats() {
122
- if (Date.now() - this.lastStats < 5000) {
123
- return this.stats;
109
+ const now = Date.now();
110
+ if (now - this.lastStatsRequest < 5000) {
111
+ return this.stats; // Return cached stats if requested too soon
124
112
  }
125
-
126
-
127
- const stats = await this.rest.makeRequest("GET", `/v4/stats`)
128
- .catch(err => {
129
- this.aqua.emit('debug', `Error fetching stats: ${err.message}`);
130
- return null;
131
- });
132
-
133
- if (stats) {
113
+
114
+ try {
115
+ const stats = await this.rest.makeRequest("GET", `/v4/stats`);
134
116
  this.stats = { ...this.stats, ...stats };
135
- this.lastStats = Date.now();
117
+ this.lastStatsRequest = now; // Update last request time
118
+ return stats;
119
+ } catch (err) {
120
+ this.aqua.emit('debug', `Error fetching stats: ${err.message}`);
121
+ return this.stats; // Return last known stats on error
136
122
  }
137
- return stats;
138
123
  }
139
124
 
140
-
141
125
  resumePlayers() {
142
126
  for (const player of this.aqua.players.values()) {
143
127
  if (player.node === this) {
@@ -153,13 +137,10 @@ class Node {
153
137
  onMessage(msg) {
154
138
  if (Array.isArray(msg)) msg = Buffer.concat(msg);
155
139
  if (msg instanceof ArrayBuffer) msg = Buffer.from(msg);
156
-
157
140
  const payload = JSON.parse(msg.toString());
158
141
  if (!payload.op) return;
159
-
160
142
  this.aqua.emit("raw", "Node", payload);
161
143
  this.aqua.emit("debug", this.name, `Received update: ${JSON.stringify(payload)}`);
162
-
163
144
  this.handlePayload(payload);
164
145
  }
165
146
 
@@ -201,7 +182,6 @@ class Node {
201
182
  this.aqua.emit("nodeError", this, new Error(`Unable to connect after ${this.reconnectTries} attempts.`));
202
183
  return this.destroy();
203
184
  }
204
-
205
185
  setTimeout(() => {
206
186
  this.aqua.emit("nodeReconnect", this);
207
187
  this.connect();
@@ -216,17 +196,13 @@ class Node {
216
196
  this.aqua.nodes.delete(this.name);
217
197
  return;
218
198
  }
219
-
220
199
  if (!this.connected) return;
221
-
222
200
  this.aqua.players.forEach((player) => {
223
201
  if (player.node === this) player.destroy();
224
202
  });
225
-
226
203
  if (this.ws) this.ws.close(1000, "destroy");
227
204
  this.ws?.removeAllListeners();
228
205
  this.ws = null;
229
-
230
206
  this.aqua.emit("nodeDestroy", this);
231
207
  this.aqua.nodeMap.delete(this.name);
232
208
  this.connected = false;
@@ -255,4 +231,4 @@ class Node {
255
231
  }
256
232
  }
257
233
 
258
- module.exports = { Node };
234
+ module.exports = { Node };
@@ -16,7 +16,7 @@ class Player extends EventEmitter {
16
16
  * @param {number} [options.defaultVolume=100] - The default volume level (0-200).
17
17
  * @param {string} [options.loop='none'] - The loop mode ('none', 'track', 'queue').
18
18
  */
19
- constructor(aqua, nodes, options) {
19
+ constructor(aqua, nodes, options = {}) {
20
20
  super();
21
21
  this.aqua = aqua;
22
22
  this.nodes = nodes;
@@ -40,6 +40,9 @@ class Player extends EventEmitter {
40
40
  this.timestamp = 0;
41
41
  this.ping = 0;
42
42
  this.isAutoplay = false;
43
+ this.nowPlayingMessage = null;
44
+
45
+ this.shouldDeleteMessage = options.shouldDeleteMessage ?? true;
43
46
 
44
47
  this.setupEventListeners();
45
48
  }
@@ -62,7 +65,6 @@ class Player extends EventEmitter {
62
65
  this.position = state.position;
63
66
  this.ping = state.ping;
64
67
  this.timestamp = state.time;
65
-
66
68
  }
67
69
 
68
70
  /**
@@ -83,18 +85,24 @@ class Player extends EventEmitter {
83
85
 
84
86
  /**
85
87
  * Plays the next track in the queue.
88
+ * @param {Object} options - Options for playing the next track.
89
+ * @param {string} options.query - The query to search for the next track.
90
+ * @param {boolean} options.force - Whether to force play the next track even if the queue is empty.
86
91
  * @returns {Promise<Player>} The player instance.
87
92
  * @throws {Error} If the player is not connected.
93
+ * @throws {Error} If the queue is empty and force is not set to true.
94
+ * @description This method plays the next track in the queue.
95
+ * @event play
88
96
  */
89
97
  async play() {
90
- if (!this.connected) throw new Error("Player connection is not established. Please connect first.");
98
+ if (!this.connected) throw new Error("Bro go on and use the connection first");
99
+ if (!this.queue.length) return;
100
+
91
101
  this.current = this.queue.shift();
92
- if (!this.current) return this;
93
102
 
94
103
  if (!this.current.track) {
95
104
  this.current = await this.current.resolve(this.aqua);
96
105
  }
97
-
98
106
  this.playing = true;
99
107
  this.position = 0;
100
108
 
@@ -102,6 +110,7 @@ class Player extends EventEmitter {
102
110
  await this.updatePlayer({ track: { encoded: this.current.track } });
103
111
  return this;
104
112
  }
113
+
105
114
  /**
106
115
  * Connects the player to the voice channel.
107
116
  * @param {Object} [options=this] - Connection options.
@@ -307,10 +316,12 @@ class Player extends EventEmitter {
307
316
  * @param {Object} payload - The event payload.
308
317
  */
309
318
  trackEnd(player, track, payload) {
310
- this.addToPreviousTrack(this.current);
319
+ if (this.shouldDeleteMessage && this.nowPlayingMessage) {
320
+ this.nowPlayingMessage.delete();
321
+ this.nowPlayingMessage = null;
322
+ }
311
323
  if (["loadfailed", "cleanup"].includes(payload.reason.replace("_", "").toLowerCase())) {
312
324
  if (player.queue.length === 0) {
313
- this.playing = false;
314
325
  return this.aqua.emit("queueEnd", player);
315
326
  }
316
327
  this.aqua.emit("trackEnd", player, payload);
@@ -326,7 +337,6 @@ class Player extends EventEmitter {
326
337
  return player.play();
327
338
  }
328
339
  if (player.queue.length === 0) {
329
- this.playing = false;
330
340
  return this.aqua.emit("queueEnd", player);
331
341
  } else {
332
342
  this.aqua.emit("trackEnd", player, payload);
@@ -439,4 +449,5 @@ class Player extends EventEmitter {
439
449
  }
440
450
  }
441
451
 
442
- module.exports = { Player };
452
+ module.exports = { Player };
453
+
@@ -100,3 +100,4 @@ class Queue extends Array {
100
100
  }
101
101
 
102
102
  module.exports = { Queue };
103
+
@@ -1,5 +1,4 @@
1
1
  const { fetch: undiciFetch } = require("undici");
2
- const nodeUtil = require("node:util");
3
2
 
4
3
  class Rest {
5
4
  constructor(aqua, options) {
@@ -9,10 +8,10 @@ class Rest {
9
8
  this.password = options.password;
10
9
  this.version = options.restVersion;
11
10
  this.calls = 0;
12
- this.queue = [];
13
- this.maxQueueSize = options.maxQueueSize || 100;
14
- this.maxConcurrentRequests = options.maxConcurrentRequests || 5;
15
- this.activeRequests = 0;
11
+ this.queue = [];
12
+ this.maxQueueSize = options.maxQueueSize || 100;
13
+ this.maxConcurrentRequests = options.maxConcurrentRequests || 5;
14
+ this.activeRequests = 0;
16
15
  }
17
16
 
18
17
  setSessionId(sessionId) {
@@ -24,22 +23,23 @@ class Rest {
24
23
  "Content-Type": "application/json",
25
24
  Authorization: this.password,
26
25
  };
27
- const requestOptions = {
28
- method,
29
- headers,
30
- body: body ? JSON.stringify(body) : null,
31
- };
26
+
32
27
  try {
33
- const response = await undiciFetch(`${this.url}${endpoint}`, requestOptions);
28
+ const response = await undiciFetch(`${this.url}${endpoint}`, {
29
+ method,
30
+ headers,
31
+ body: body && JSON.stringify(body),
32
+ });
34
33
  this.calls++;
35
34
  const data = await this.parseResponse(response);
36
35
  this.aqua.emit("apiResponse", endpoint, response);
37
36
  this.aqua.emit(
38
37
  "debug",
39
- `[Rest] ${method} ${endpoint} ${body ? `body: ${JSON.stringify(body)}` : ""} -> Status Code: ${response.status} Response: ${nodeUtil.inspect(data)}`
38
+ `[Rest] ${method} ${endpoint} ${body ? `body: ${JSON.stringify(body)}` : ""} -> Status Code: ${response.status} Response(body): ${JSON.stringify(data)}`
40
39
  );
41
40
  return includeHeaders ? { data, headers: response.headers } : data;
42
41
  } catch (error) {
42
+ this.aqua.emit("debug", `Network error during request: ${method} ${this.url}${endpoint}`, { cause: error });
43
43
  throw new Error(`Network error during request: ${method} ${this.url}${endpoint}`, { cause: error });
44
44
  }
45
45
  }
@@ -50,15 +50,18 @@ class Rest {
50
50
 
51
51
  async updatePlayer(options) {
52
52
  const requestBody = { ...options.data };
53
+
53
54
  if ((requestBody.track && requestBody.track.encoded && requestBody.track.identifier) ||
54
55
  (requestBody.encodedTrack && requestBody.identifier)) {
55
56
  throw new Error(`Cannot provide both 'encoded' and 'identifier' for track in Update Player Endpoint`);
56
57
  }
58
+
57
59
  if (this.version === "v3" && options.data?.track) {
58
60
  const { track } = requestBody;
59
61
  delete requestBody.track;
60
62
  Object.assign(requestBody, track.encoded ? { encodedTrack: track.encoded } : { identifier: track.identifier });
61
63
  }
64
+
62
65
  return this.makeRequest("PATCH", `/${this.version}/sessions/${this.sessionId}/players/${options.guildId}?noReplace=false`, requestBody);
63
66
  }
64
67
 
@@ -70,7 +73,7 @@ class Rest {
70
73
  return this.makeRequest("GET", `/${this.version}/loadtracks?identifier=${encodeURIComponent(identifier)}`);
71
74
  }
72
75
 
73
- async decodeTrack(track, node) {
76
+ async decodeTrack(track) {
74
77
  return this.makeRequest("GET", `/${this.version}/decodetrack?encodedTrack=${encodeURIComponent(track)}`);
75
78
  }
76
79
 
@@ -79,10 +82,7 @@ class Rest {
79
82
  }
80
83
 
81
84
  async getStats() {
82
- if (this.version === "v3") {
83
- return this.makeRequest("GET", `/${this.version}/stats`);
84
- }
85
- return this.makeRequest("GET", `/${this.version}/stats/all`);
85
+ return this.makeRequest("GET", this.version === "v3" ? `/${this.version}/stats` : `/${this.version}/stats/all`);
86
86
  }
87
87
 
88
88
  async getInfo() {
@@ -98,23 +98,19 @@ class Rest {
98
98
  }
99
99
 
100
100
  async parseResponse(response) {
101
- if (response.status === 204) {
102
- return null;
103
- }
101
+ if (response.status === 204) return null;
104
102
  try {
105
- const contentType = response.headers.get("Content-Type");
106
- return await response[contentType.includes("text/plain") ? "text" : "json"]();
103
+ return response.headers.get("Content-Type").includes("text/plain") ? await response.text() : await response.json();
107
104
  } catch (error) {
108
105
  this.aqua.emit("debug", `[Rest - Error] Failed to process response from ${response.url}: ${error}`);
109
106
  return null;
110
107
  }
111
108
  }
112
- /**
113
- * Cleans up resources related to the queue.
114
- */
109
+
115
110
  cleanupQueue() {
116
- this.queue = [];
111
+ this.queue = [];
117
112
  }
118
113
  }
119
114
 
120
- module.exports = { Rest };
115
+ module.exports = { Rest };
116
+
@@ -7,7 +7,7 @@ const { getImageUrl } = require("../handlers/fetchImage");
7
7
  */
8
8
  class Track {
9
9
  /**
10
- * @param {{ encoded: string, info: { identifier: string, isSeekable: boolean, author: string, length: number, isStream: boolean, position: number, title: string, uri: string, sourceName: string, thumbnail: string, track: string, tracks: Array<Track>, playlist: { name: string, selectedTrack: number } } }} data
10
+ * @param {{ encoded: string, info: { identifier: string, isSeekable: boolean, author: string, length: number, isStream: boolean, position: number, title: string, uri: string, sourceName: string, artworkUrl: string, track: string, tracks: Array<Track>, playlist: { name: string, selectedTrack: number } } }} data
11
11
  * @param {Player} requester
12
12
  * @param {Node} nodes
13
13
  */
@@ -17,6 +17,7 @@ class Track {
17
17
  this.requester = requester;
18
18
  this.nodes = nodes;
19
19
  this.track = data.encoded || Buffer.from(data.track, "base64").toString("utf8");
20
+ this.playlist = data.playlist || null;
20
21
  }
21
22
 
22
23
  /**
@@ -62,14 +63,18 @@ class Track {
62
63
  updateTrackInfo(track) {
63
64
  this.info.identifier = track.info.identifier;
64
65
  this.track = track.track;
66
+ if (track.playlist) {
67
+ this.playlist = track.playlist;
68
+ }
65
69
  }
66
70
 
67
71
  /**
68
72
  * @private
69
73
  */
70
74
  cleanup() {
71
- this.rawData = this.track = this.info = null;
75
+ this.rawData = this.track = this.info = this.playlist = null;
72
76
  }
73
77
  }
74
78
 
75
79
  module.exports = { Track };
80
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aqualink",
3
- "version": "1.0.5",
3
+ "version": "1.1.0",
4
4
  "description": "An Lavalink wrapper, focused in speed, performance, and features, Based in Riffy!",
5
5
  "main": "build/index.js",
6
6
  "scripts": {