aqualink 1.8.1-beta5 → 1.9.0-beta2

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
@@ -4,7 +4,7 @@ An Stable, performant, Recourse friendly and fast lavalink wrapper
4
4
  This code is based in riffy, but its an 100% Rewrite made from scratch...
5
5
 
6
6
  # Why use AquaLink
7
- - In dev
7
+ - Uses my modified fork of @performanc/pwsl-mini, for an way faster WebSocket
8
8
  - Very Low memory comsuption
9
9
  - Built in Queue manager
10
10
  - Lots of features to use
@@ -16,66 +16,90 @@ This code is based in riffy, but its an 100% Rewrite made from scratch...
16
16
  - Lavalink v4 support (din't test v3)
17
17
  - Youtube and Spotify support (Soundcloud, deezer, vimeo, etc also works...)
18
18
  - Minimal Requests to the lavalink server (helps the lavalink recourses!)
19
- - Easy player, node, aqua manager
19
+ - Easy player, node, aqua manager
20
20
  - Fast responses from rest and node
21
21
  - Playlist support (My mix playlists, youtube playlists, spotify playlists, etc)
22
- - Uses @performanc/pwsl-mini as default WebSocket system
23
22
  - Lyrics Support by Lavalink
24
23
  - https://github.com/topi314/LavaLyrics (RECOMMENDED)
25
24
  - https://github.com/DRSchlaubi/lyrics.kt (?)
26
25
  - https://github.com/DuncteBot/java-timed-lyrics (RECOMMENDED)
26
+
27
+ # Tralalero Tralala 1.9.0 Released
28
+ **Whoa, lots of stuff to write here 😭**
29
+
30
+ ---
31
+
32
+ ### **Small changes on the `fetchImage` Handler**
33
+ - Improves the overall speed, less memory overhead.
34
+
35
+ ---
36
+
37
+ ### **Remade some stuff on `AQUA` module**
38
+ - This fixes some bugs related to destroying players.
39
+ - Faster node connection speeds.
40
+ - Uses an Array for getting the region instead (testing).
41
+ - Small change on the Voice Handler.
42
+ - Improved Error handling.
43
+ - Use `node.destroy()` method directly.
44
+
45
+ ---
46
+
47
+ ### **Remade `Connection` module**
48
+ - Removed lots of useless code.
49
+ - Improved joining voice channel speed.
50
+ - Improved configuration set/get speed.
51
+ - Improved overall checking.
52
+ - Improved debug messages.
53
+
54
+ ---
55
+
56
+ ### **Remade `Node` module (this one is good)**
57
+ - 1.9.1-beta1: Fixed the auto reconnect system
58
+ - Fixed the `autoResume` system (now will actually work, for 60 seconds).
59
+ - New WebSocket System.
60
+ - Improved the events handling speed.
61
+ - Now does recalculation of the backoff time (for more efficiency on reconnect).
62
+ - Now avoids reconnecting if the WebSocket is already open (sorry, I forgot to add this before).
63
+ - Better cleaning system (improved, now removes listeners instead of setting to `null`).
64
+ - Avoids re-binding the functions every time `connect` is called (yay).
65
+ - This update also improves long-process running.
66
+
67
+ ---
68
+
69
+ ### **Remade the `Player` module (also a good one)**
70
+ - Remade every method.
71
+ - Fixed destroy system.
72
+ - Better event handling, I think.
73
+ - Made the events async.
74
+ - Removed `trackChange` (does not exist in Lavalink API, use `trackStart` instead).
75
+ - Uses a new listener system (way more efficient for creating/destroying players).
76
+ - Faster shuffle in V8 Engine (Math stuff).
77
+ - Improved overall configs (more precise).
78
+ - Use `pop()` instead of disabling the track 50 on the length.
79
+ - Improved overall speed on the check-ins and some stuff I forgot.
80
+
81
+ ---
82
+
83
+ ### **Remade the `Rest` module**
84
+ - Better speed (removed useless `buildEndpoint`).
85
+ - More compact code.
86
+ - Removed `stats/all` in the stats (correct by using the Lavalink API).
87
+ - Better `makeRequest`.
88
+
89
+ ---
90
+
91
+ ### **Small changes in `Track` module**
92
+ - More efficient final result (`author` + `track`).
93
+
94
+ ---
95
+
96
+ That’s all for **1.9.0** atm. I’m a lazy dev. 😴
27
97
 
28
98
  # Docs (Wiki)
29
99
  - https://github.com/ToddyTheNoobDud/AquaLink/wiki
30
100
 
31
101
  - Example bot: https://github.com/ToddyTheNoobDud/Thorium-Music
32
102
 
33
- # Brick by brick, 1.8.0 Update (yay)
34
-
35
- ### 1.8.1-beta5 Update:
36
- - Use @performanc/pwsl-mini as main WebSocket
37
- - Use pool for connections (Experimental, help me improve it. Undici pool)
38
- - Default will not leave the VC anymore (leaveOnEnd: false default)
39
- - Misc optimizations on node and player
40
-
41
- ### 1.8.0
42
- - Misc changes on FetchImage (improves the overall checking and speed)
43
- - Rewrite `AQUA` module
44
- - Remade the resolve logic (improves the speed by a lot)
45
- - Fixes many memory usages related to nodes
46
- - send is no longer required to be Applied (Applied by default now.)
47
- - Remade some stuff with discord VoiceGateway
48
-
49
- - Remade `CONNECTION` module
50
- - Way faster connections (Joining, reconnecting, connected, disconnect)
51
- - Reduced memory overload by removing useless code
52
- - Improved early Returns
53
-
54
- - Remade `NODE` module
55
- - MANY fixes for the connection logic (fixes reconnection, etc)
56
- - Fixed memory leaks in heartbeat system (hopefully, reduced memory by a lot.)
57
- - Faster connection speed and checkings
58
- - Remade the Options system, improve JSON parsing
59
-
60
- - Rewrite `PLAYER` module
61
- - Many memory related fixes
62
- - Improved the overall code speed by a lot
63
- - Rewrote setLoop, play, shuffle, replay methods (fixes + performance)
64
- - Added 2 new options:
65
-
66
- leaveOnEnd: false, // Optional
67
-
68
- shouldDeleteMessage: true // Optional
69
-
70
- - Uses array for better performance and less memory allocation
71
- - Rewrite the Events handling (speed and recourses fixes)
72
-
73
- - Updated `TRACK` module
74
- - Better object handling for internal code.
75
- - Removed an useless method
76
-
77
- Thats all for now, im lazy, help me fix code and improve this on github... i can't test properly 😭😭😭
78
-
79
103
  # How to install
80
104
 
81
105
  `npm install aqualink`
@@ -1,15 +1,19 @@
1
1
  const { request } = require("undici");
2
+
2
3
  const sourceHandlers = new Map([
3
4
  ['spotify', uri => fetchThumbnail(`https://open.spotify.com/oembed?url=${uri}`)],
4
5
  ['youtube', identifier => fetchYouTubeThumbnail(identifier)]
5
6
  ]);
6
- const YOUTUBE_URL_TEMPLATE = quality => id => `https://img.youtube.com/vi/${id}/${quality}.jpg`;
7
+
8
+ const YOUTUBE_URL_TEMPLATE = (quality) => (id) => `https://img.youtube.com/vi/${id}/${quality}.jpg`;
7
9
  const YOUTUBE_QUALITIES = ['maxresdefault', 'hqdefault', 'mqdefault', 'default'].map(YOUTUBE_URL_TEMPLATE);
8
10
 
9
11
  async function getImageUrl(info) {
10
12
  if (!info?.sourceName || !info?.uri) return null;
13
+
11
14
  const handler = sourceHandlers.get(info.sourceName.toLowerCase());
12
15
  if (!handler) return null;
16
+
13
17
  try {
14
18
  return await handler(info.uri);
15
19
  } catch (error) {
@@ -33,15 +37,12 @@ async function fetchThumbnail(url) {
33
37
  }
34
38
 
35
39
  async function fetchYouTubeThumbnail(identifier) {
36
- try {
37
- const thumbnail = await Promise.race(
38
- YOUTUBE_QUALITIES.map(urlFunc => fetchThumbnail(urlFunc(identifier)))
39
- );
40
- return thumbnail || null;
41
- } catch (error) {
42
- console.error('No valid YouTube thumbnail found:', error);
40
+ return Promise.race(
41
+ YOUTUBE_QUALITIES.map(urlFunc => fetchThumbnail(urlFunc(identifier)))
42
+ ).catch(() => {
43
+ console.error('No valid YouTube thumbnail found.');
43
44
  return null;
44
- }
45
+ });
45
46
  }
46
47
 
47
- module.exports = { getImageUrl };
48
+ module.exports = { getImageUrl };
@@ -8,17 +8,6 @@ const { version: pkgVersion } = require("../../package.json");
8
8
  const URL_REGEX = /^https?:\/\//;
9
9
 
10
10
  class Aqua extends EventEmitter {
11
- /**
12
- * @param {Object} client - The client instance.
13
- * @param {Array<Object>} nodes - An array of node configurations.
14
- * @param {Object} options - Configuration options for Aqua.
15
- * @param {Function} options.send - Function to send data.
16
- * @param {string} [options.defaultSearchPlatform="ytsearch"] - Default search platform.
17
- * @param {string} [options.restVersion="v4"] - Version of the REST API.
18
- * @param {Array<Object>} [options.plugins=[]] - Plugins to load.
19
- * @param {boolean} [options.autoResume=false] - Automatically resume tracks on reconnect.
20
- * @param {boolean} [options.infiniteReconnects=false] - Reconnect infinitely.
21
- */
22
11
  constructor(client, nodes, options = {}) {
23
12
  super();
24
13
  this.validateInputs(client, nodes, options);
@@ -61,61 +50,68 @@ class Aqua extends EventEmitter {
61
50
  get leastUsedNodes() {
62
51
  const now = Date.now();
63
52
  if (now - this._leastUsedCache.timestamp < 50) return this._leastUsedCache.nodes;
53
+
64
54
  const nodes = [];
65
55
  for (const node of this.nodeMap.values()) {
66
56
  if (node.connected) nodes.push(node);
67
57
  }
68
58
  nodes.sort((a, b) => a.rest.calls - b.rest.calls);
59
+
69
60
  this._leastUsedCache = { nodes, timestamp: now };
70
61
  return nodes;
71
62
  }
72
63
 
73
64
  init(clientId) {
74
65
  if (this.initiated) return this;
66
+
75
67
  this.clientId = clientId;
76
68
  try {
77
69
  this.nodes.forEach(nodeConfig => this.createNode(nodeConfig));
78
- this.initiated = true;
79
70
  this.plugins.forEach(plugin => plugin.load(this));
71
+ this.initiated = true;
80
72
  } catch (error) {
81
73
  this.initiated = false;
82
74
  throw error;
83
75
  }
76
+
84
77
  return this;
85
78
  }
86
79
 
87
80
  createNode(options) {
88
81
  const nodeId = options.name || options.host;
89
82
  this.destroyNode(nodeId);
83
+
90
84
  const node = new Node(this, options, this.options);
91
85
  this.nodeMap.set(nodeId, node);
92
86
  this._leastUsedCache.timestamp = 0;
93
- node.connect().then(() => {
94
- this.emit("nodeCreate", node);
95
- }).catch(error => {
96
- this.nodeMap.delete(nodeId);
97
- throw error;
98
- });
87
+
88
+ node.connect()
89
+ .then(() => this.emit("nodeCreate", node))
90
+ .catch(error => {
91
+ this.nodeMap.delete(nodeId);
92
+ console.error("Failed to connect node:", error);
93
+ throw error;
94
+ });
95
+
99
96
  return node;
100
97
  }
101
98
 
102
99
  destroyNode(identifier) {
103
100
  const node = this.nodeMap.get(identifier);
104
101
  if (!node) return;
105
- node.disconnect().then(() => {
106
- node.removeAllListeners();
107
- this.nodeMap.delete(identifier);
108
- this._leastUsedCache.timestamp = 0;
109
- this.emit("nodeDestroy", node);
110
- }).catch(error => console.error(`Error destroying node ${identifier}:`, error));
102
+
103
+ node.destroy();
104
+ this.nodeMap.delete(identifier);
105
+ this.emit("nodeDestroy", node);
111
106
  }
112
107
 
108
+
113
109
  updateVoiceState({ d, t }) {
114
110
  const player = this.players.get(d.guild_id);
115
111
  if (!player) return;
116
112
 
113
+ const updateMethod = t === "VOICE_SERVER_UPDATE" ? "setServerUpdate" : "setStateUpdate";
117
114
  if (t === "VOICE_SERVER_UPDATE" || (t === "VOICE_STATE_UPDATE" && d.user_id === this.clientId)) {
118
- const updateMethod = t === "VOICE_SERVER_UPDATE" ? "setServerUpdate" : "setStateUpdate";
119
115
  if (player.connection && typeof player.connection[updateMethod] === "function") {
120
116
  player.connection[updateMethod](d);
121
117
  }
@@ -127,14 +123,13 @@ class Aqua extends EventEmitter {
127
123
 
128
124
  fetchRegion(region) {
129
125
  if (!region) return this.leastUsedNodes;
126
+
130
127
  const lowerRegion = region.toLowerCase();
131
- const regionNodes = [];
132
- for (const node of this.nodeMap.values()) {
133
- if (node.connected && node.regions?.includes(lowerRegion)) {
134
- regionNodes.push(node);
135
- }
136
- }
128
+ const regionNodes = Array.from(this.nodeMap.values()).filter(node =>
129
+ node.connected && node.regions?.includes(lowerRegion)
130
+ );
137
131
  regionNodes.sort((a, b) => this.calculateLoad(a) - this.calculateLoad(b));
132
+
138
133
  return regionNodes;
139
134
  }
140
135
 
@@ -148,9 +143,11 @@ class Aqua extends EventEmitter {
148
143
  this.ensureInitialized();
149
144
  const existingPlayer = this.players.get(options.guildId);
150
145
  if (existingPlayer && existingPlayer.voiceChannel) return existingPlayer;
146
+
151
147
  const availableNodes = options.region ? this.fetchRegion(options.region) : this.leastUsedNodes;
152
148
  const node = availableNodes[0];
153
149
  if (!node) throw new Error("No nodes are available");
150
+
154
151
  return this.createPlayer(node, options);
155
152
  }
156
153
 
@@ -167,6 +164,7 @@ class Aqua extends EventEmitter {
167
164
  async destroyPlayer(guildId) {
168
165
  const player = this.players.get(guildId);
169
166
  if (!player) return;
167
+
170
168
  try {
171
169
  await player.clearData();
172
170
  player.removeAllListeners();
@@ -276,4 +274,4 @@ class Aqua extends EventEmitter {
276
274
  }
277
275
  }
278
276
 
279
- module.exports = Aqua
277
+ module.exports = Aqua;
@@ -3,58 +3,41 @@
3
3
  class Connection {
4
4
  constructor(player) {
5
5
  this.playerRef = new WeakRef(player);
6
- const { voiceChannel, guildId, aqua, nodes } = player;
7
- this.voice = {
8
- sessionId: null,
9
- endpoint: null,
10
- token: null
11
- };
6
+ this.voice = { sessionId: null, endpoint: null, token: null };
12
7
  this.region = null;
13
8
  this.selfDeaf = false;
14
9
  this.selfMute = false;
15
- this.voiceChannel = voiceChannel;
16
- this.guildId = guildId;
17
- this.aqua = aqua;
18
- this.nodes = nodes;
10
+ this.voiceChannel = player.voiceChannel;
11
+ this.guildId = player.guildId;
12
+ this.aqua = player.aqua;
13
+ this.nodes = player.nodes;
19
14
  }
20
15
 
21
- setServerUpdate(data = {}) {
22
- const { endpoint, token } = data;
23
-
24
- if (!endpoint) {
25
- throw new Error("Missing 'endpoint' property");
26
- }
27
-
16
+ setServerUpdate({ endpoint, token } = {}) {
17
+ if (!endpoint) throw new Error("Missing 'endpoint' property");
28
18
  const newRegion = endpoint.split('.')[0];
29
19
  if (this.region !== newRegion) {
30
- this.voice.endpoint = endpoint;
31
- this.voice.token = token;
20
+ this.voice = { ...this.voice, endpoint, token };
32
21
  const prevRegion = this.region;
33
22
  this.region = newRegion;
34
-
35
- const message = prevRegion
36
- ? `Changed Voice Region from ${prevRegion} to ${newRegion}`
37
- : `Voice Server: ${newRegion}`;
38
-
39
- this.aqua.emit("debug", `[Player ${this.guildId} - CONNECTION] ${message}`);
23
+ this.aqua.emit("debug", `[Player ${this.guildId} - CONNECTION] Voice Server: ${prevRegion ? `Changed from ${prevRegion} to ${newRegion}` : newRegion}`);
40
24
  this._updatePlayerVoiceData();
41
25
  }
42
26
  }
43
27
 
44
- setStateUpdate({ channel_id: channelId, session_id: sessionId, self_deaf: selfDeaf, self_mute: selfMute } = {}) {
45
- if (!channelId || !sessionId) {
28
+ setStateUpdate({ channel_id, session_id, self_deaf, self_mute } = {}) {
29
+ if (!channel_id || !session_id) {
46
30
  this.playerRef.deref()?.destroy();
47
31
  return;
48
32
  }
49
-
50
- if (this.voiceChannel !== channelId) {
51
- this.aqua.emit("playerMove", this.voiceChannel, channelId);
52
- this.voiceChannel = channelId;
33
+
34
+ if (this.voiceChannel !== channel_id) {
35
+ this.aqua.emit("playerMove", this.voiceChannel, channel_id);
36
+ this.voiceChannel = channel_id;
53
37
  }
54
-
55
- this.selfDeaf = selfDeaf;
56
- this.selfMute = selfMute;
57
- this.voice.sessionId = sessionId;
38
+
39
+ Object.assign(this, { selfDeaf: self_deaf, selfMute: self_mute });
40
+ this.voice.sessionId = session_id;
58
41
  }
59
42
 
60
43
  async _updatePlayerVoiceData() {
@@ -64,19 +47,12 @@ class Connection {
64
47
  try {
65
48
  await this.nodes.rest.updatePlayer({
66
49
  guildId: this.guildId,
67
- data: {
68
- voice: this.voice,
69
- volume: player.volume
70
- }
50
+ data: { voice: this.voice, volume: player.volume }
71
51
  });
72
52
  } catch (err) {
73
- this.aqua.emit("apiError", "updatePlayer", {
74
- error: err,
75
- guildId: this.guildId,
76
- voiceData: this.voice
77
- });
53
+ this.aqua.emit("apiError", "updatePlayer", { error: err, guildId: this.guildId, voiceData: this.voice });
78
54
  }
79
55
  }
80
56
  }
81
57
 
82
- module.exports = Connection;
58
+ module.exports = Connection;
@@ -1,5 +1,5 @@
1
1
  "use strict";
2
- const WebSocket = require("@toddynnn/pwsl-mini");
2
+ const WebSocket = require('ws');
3
3
  const Rest = require("./Rest");
4
4
 
5
5
  class Node {
@@ -30,7 +30,6 @@ class Node {
30
30
  this.regions = regions;
31
31
  this.wsUrl = new URL(`ws${secure ? "s" : ""}://${host}:${port}/v4/websocket`);
32
32
  this.rest = new Rest(aqua, this);
33
- this.resumeKey = options.resumeKey || null;
34
33
  this.resumeTimeout = options.resumeTimeout || 60;
35
34
  this.autoResume = options.autoResume || false;
36
35
  this.reconnectTimeout = options.reconnectTimeout || 2000;
@@ -70,29 +69,29 @@ class Node {
70
69
  }
71
70
 
72
71
  #constructHeaders() {
73
- return {
72
+ const headers = {
74
73
  Authorization: this.password,
75
74
  "User-Id": this.aqua.clientId,
76
75
  "Client-Name": `Aqua/${this.aqua.version}`,
77
- ...(this.sessionId && { "Session-Id": this.sessionId }),
78
- ...(this.resumeKey && { "Resume-Key": this.resumeKey })
79
76
  };
77
+ if (this.sessionId) headers["Session-Id"] = this.sessionId;
78
+ return headers;
80
79
  }
81
80
 
82
81
  async #onOpen() {
83
82
  this.connected = true;
84
83
  this.#reconnectAttempted = 0;
85
84
  this.aqua.emit("debug", this.name, `Connected to ${this.wsUrl.href}`);
86
-
87
- if (this.autoResume) {
88
- try {
89
- this.info = await this.rest.makeRequest("GET", "/v4/info");
85
+
86
+ try {
87
+ this.info = await this.rest.makeRequest("GET", "/v4/info");
88
+ if (this.autoResume && this.sessionId) {
90
89
  await this.resumePlayers();
91
- } catch (err) {
92
- this.info = null;
93
- if (!this.aqua.bypassChecks?.nodeFetchInfo) {
94
- this.aqua.emit("error", `Failed to fetch node info: ${err.message}`);
95
- }
90
+ }
91
+ } catch (err) {
92
+ this.info = null;
93
+ if (!this.aqua.bypassChecks?.nodeFetchInfo) {
94
+ this.aqua.emit("error", this, `Failed to fetch node info: ${err.message}`);
96
95
  }
97
96
  }
98
97
  }
@@ -108,8 +107,9 @@ class Node {
108
107
  try {
109
108
  payload = JSON.parse(msg);
110
109
  } catch {
111
- return;
110
+ return; // Invalid JSON, ignore the message
112
111
  }
112
+
113
113
  const op = payload?.op;
114
114
  if (!op) return;
115
115
 
@@ -125,6 +125,18 @@ class Node {
125
125
  }
126
126
  }
127
127
 
128
+ async resumePlayers() {
129
+ try {
130
+ await this.rest.makeRequest("PATCH", `/v4/sessions/${this.sessionId}`, {
131
+ resuming: true,
132
+ timeout: this.resumeTimeout
133
+ });
134
+ this.aqua.emit("debug", this.name, `Successfully resumed session ${this.sessionId}`);
135
+ } catch (err) {
136
+ this.aqua.emit("error", this, `Failed to resume session: ${err.message}`);
137
+ }
138
+ }
139
+
128
140
  #updateStats(payload) {
129
141
  if (!payload) return;
130
142
  this.stats = {
@@ -170,14 +182,19 @@ class Node {
170
182
  }
171
183
 
172
184
  #handleReadyOp(payload) {
173
- if (this.sessionId !== payload.sessionId) {
174
- this.sessionId = payload.sessionId;
175
- this.rest.setSessionId(payload.sessionId);
185
+ if (!payload.sessionId) {
186
+ this.aqua.emit("error", this, "Ready payload missing sessionId");
187
+ return;
176
188
  }
189
+
190
+ this.sessionId = payload.sessionId;
191
+ this.rest.setSessionId(payload.sessionId);
192
+
177
193
  this.aqua.emit("nodeConnect", this);
178
194
  }
179
195
 
180
196
  #handlePlayerOp(payload) {
197
+ if (!payload.guildId) return;
181
198
  const player = this.aqua.players.get(payload.guildId);
182
199
  if (player) player.emit(payload.op, payload);
183
200
  }
@@ -188,7 +205,7 @@ class Node {
188
205
 
189
206
  #onClose(code, reason) {
190
207
  this.connected = false;
191
- this.aqua.emit("nodeDisconnect", this, { code, reason });
208
+ this.aqua.emit("nodeDisconnect", this, { code, reason: reason?.toString() || "No reason provided" });
192
209
  this.#reconnect();
193
210
  }
194
211
 
@@ -198,22 +215,24 @@ class Node {
198
215
  setTimeout(() => this.connect(), 10000);
199
216
  return;
200
217
  }
218
+
201
219
  if (this.#reconnectAttempted >= this.reconnectTries) {
202
220
  this.aqua.emit("nodeError", this,
203
221
  new Error(`Max reconnection attempts reached (${this.reconnectTries})`));
204
222
  this.destroy(true);
205
223
  return;
206
224
  }
225
+
207
226
  clearTimeout(this.#reconnectTimeoutId);
208
227
  const jitter = Math.random() * 10000;
209
228
  const backoffTime = Math.min(
210
229
  this.reconnectTimeout * Math.pow(Node.BACKOFF_MULTIPLIER, this.#reconnectAttempted) + jitter,
211
230
  Node.MAX_BACKOFF
212
231
  );
232
+
213
233
  this.#reconnectTimeoutId = setTimeout(() => {
214
234
  this.#reconnectAttempted++;
215
- this.aqua.emit("nodeReconnect", {
216
- nodeName: this.name,
235
+ this.aqua.emit("nodeReconnect", this, {
217
236
  attempt: this.#reconnectAttempted,
218
237
  backoffTime
219
238
  });
@@ -222,11 +241,22 @@ class Node {
222
241
  }
223
242
 
224
243
  destroy(clean = false) {
244
+ clearTimeout(this.#reconnectTimeoutId);
245
+
246
+ if (this.#ws) {
247
+ this.#ws.removeAllListeners();
248
+ if (this.#ws.readyState === WebSocket.OPEN) {
249
+ this.#ws.close();
250
+ }
251
+ this.#ws = null;
252
+ }
253
+
225
254
  if (clean) {
226
255
  this.aqua.emit("nodeDestroy", this);
227
256
  this.aqua.nodes.delete(this.name);
228
257
  return;
229
258
  }
259
+
230
260
  if (this.connected) {
231
261
  for (const player of this.aqua.players.values()) {
232
262
  if (player.node === this) {
@@ -234,11 +264,12 @@ class Node {
234
264
  }
235
265
  }
236
266
  }
267
+
237
268
  this.connected = false;
238
- this.aqua.nodeMap.delete(this.name);
269
+ this.aqua.nodes.delete(this.name);
239
270
  this.aqua.emit("nodeDestroy", this);
240
271
  this.info = null;
241
272
  }
242
273
  }
243
274
 
244
- module.exports = Node;
275
+ module.exports = Node;
@@ -1,9 +1,9 @@
1
1
  "use strict";
2
2
 
3
3
  const { EventEmitter } = require("events");
4
- const Connection = require("./Connection");
5
- const Queue = require("./Queue");
6
- const Filters = require("./Filters");
4
+ const Connection = require("./Connection");
5
+ const Queue = require("./Queue");
6
+ const Filters = require("./Filters");
7
7
 
8
8
  class Player extends EventEmitter {
9
9
  static LOOP_MODES = Object.freeze({
@@ -30,32 +30,22 @@ class Player extends EventEmitter {
30
30
  this.voiceChannel = options.voiceChannel;
31
31
  this.connection = new Connection(this);
32
32
  this.filters = new Filters(this);
33
- this.mute = options.mute ?? false;
34
- this.deaf = options.deaf ?? false;
35
- this.volume = options.defaultVolume ?? 100;
36
- this.loop = options.loop ?? Player.LOOP_MODES.NONE;
33
+ this.volume = Math.min(Math.max(options.defaultVolume ?? 100, 0), 200);
34
+ this.loop = Player.LOOP_MODES[options.loop?.toUpperCase()] || Player.LOOP_MODES.NONE;
37
35
  this.queue = new Queue();
38
- this.position = 0;
39
- this.current = null;
36
+ this.previousTracks = [];
37
+ this.shouldDeleteMessage = options.shouldDeleteMessage ?? false;
38
+ this.leaveOnEnd = options.leaveOnEnd ?? false;
39
+
40
40
  this.playing = false;
41
41
  this.paused = false;
42
42
  this.connected = false;
43
+ this.current = null;
43
44
  this.timestamp = 0;
44
45
  this.ping = 0;
45
46
  this.nowPlayingMessage = null;
46
- this.previousTracks = [];
47
- this.shouldDeleteMessage = options.shouldDeleteMessage ?? false;
48
- this.leaveOnEnd = options.leaveOnEnd ?? false;
49
-
50
- this.onPlayerUpdate = ({ state } = {}) => {
51
- if (!state) return;
52
- for (const key in state) {
53
- if (state.hasOwnProperty(key)) {
54
- this[key] = state[key];
55
- }
56
- }
57
- this.aqua.emit("playerUpdate", this, { state });
58
- };
47
+
48
+ this.onPlayerUpdate = ({ state }) => state && Object.assign(this, state) && this.aqua.emit("playerUpdate", this, { state });
59
49
  this.handleEvent = async (payload) => {
60
50
  const player = this.aqua.players.get(payload.guildId);
61
51
  if (!player) return;
@@ -66,8 +56,12 @@ class Player extends EventEmitter {
66
56
  this.handleUnknownEvent(payload);
67
57
  }
68
58
  };
69
- this.on("playerUpdate", this.onPlayerUpdate);
70
- this.on("event", this.handleEvent);
59
+ if (!this.listenerCount("playerUpdate")) {
60
+ this.on("playerUpdate", this.onPlayerUpdate);
61
+ }
62
+ if (!this.listenerCount("event")) {
63
+ this.on("event", this.handleEvent);
64
+ }
71
65
  }
72
66
 
73
67
  get previous() {
@@ -78,21 +72,18 @@ class Player extends EventEmitter {
78
72
  }
79
73
 
80
74
  addToPreviousTrack(track) {
81
- if (this.previousTracks.length >= 50) {
82
- this.previousTracks.length = 49;
83
- }
75
+ if (this.previousTracks.length >= 50) this.previousTracks.pop();
84
76
  this.previousTracks.unshift(track);
85
77
  }
86
78
 
87
79
  async play() {
88
- if (!this.connected) throw new Error("Player must be connected first.");
89
- if (!this.queue.length) return;
90
-
80
+ if (!this.connected || !this.queue.length) return;
91
81
  const item = this.queue.shift();
82
+
92
83
  this.current = item.track ? item : await item.resolve(this.aqua);
93
84
  this.playing = true;
94
85
  this.position = 0;
95
-
86
+
96
87
  this.aqua.emit("debug", this.guildId, `Playing track: ${this.current.track}`);
97
88
  return this.updatePlayer({ track: { encoded: this.current.track } });
98
89
  }
@@ -128,39 +119,25 @@ class Player extends EventEmitter {
128
119
 
129
120
  async searchLyrics(query) {
130
121
  if (!query) return null;
131
- const response = await this.nodes.rest.getLyrics({
132
- track: {
133
- encoded: { info: { title: query } },
134
- guild_id: this.guildId,
135
- search: true
136
- }
137
- });
138
- return response || null;
122
+ return await this.nodes.rest.getLyrics({ track: { info: { title: query } }, search: true }) || null;
139
123
  }
140
124
 
141
125
  async lyrics() {
142
126
  if (!this.playing) return null;
143
- const response = await this.nodes.rest.getLyrics({
144
- track: {
145
- encoded: this.current.track,
146
- guild_id: this.guildId
147
- }
148
- });
149
- return response || null;
127
+ return await this.nodes.rest.getLyrics({ track: { encoded: this.current.track } }) || null;
150
128
  }
151
129
 
152
130
  seek(position) {
153
131
  if (!this.playing) return this;
154
- this.position += position;
155
- this.updatePlayer({ position: this.position });
132
+ this.updatePlayer({ position: (this.position += position) });
156
133
  return this;
157
134
  }
158
135
 
159
136
  stop() {
160
137
  if (!this.playing) return this;
161
- this.updatePlayer({ track: { encoded: null } });
162
138
  this.playing = false;
163
139
  this.position = 0;
140
+ this.updatePlayer({ track: { encoded: null } });
164
141
  return this;
165
142
  }
166
143
 
@@ -179,6 +156,7 @@ class Player extends EventEmitter {
179
156
  }
180
157
 
181
158
  setTextChannel(channel) {
159
+ this.textChannel = channel;
182
160
  this.updatePlayer({ text_channel: channel });
183
161
  return this;
184
162
  }
@@ -199,23 +177,14 @@ class Player extends EventEmitter {
199
177
  }
200
178
 
201
179
  disconnect() {
202
- this.updatePlayer({ track: { encoded: null } });
203
180
  this.connected = false;
204
181
  this.send({ guild_id: this.guildId, channel_id: null });
205
- this.aqua?.emit("debug", this.guildId, "Player disconnected.");
182
+ this.voiceChannel = null;
183
+ this.aqua.emit("debug", this.guildId, "Player disconnected.");
206
184
  return this;
207
185
  }
208
-
209
186
  shuffle() {
210
- const array = this.queue;
211
- let currentIndex = array.length;
212
-
213
- while (currentIndex > 0) {
214
- const randomIndex = Math.floor(Math.random() * currentIndex);
215
- currentIndex--;
216
- [array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]];
217
- }
218
-
187
+ this.queue = this.queue.sort(() => Math.random() - 0.5);
219
188
  return this;
220
189
  }
221
190
 
@@ -225,7 +194,6 @@ class Player extends EventEmitter {
225
194
 
226
195
  replay() {
227
196
  this.seek(-this.position);
228
- return this;
229
197
  }
230
198
 
231
199
  skip() {
@@ -245,12 +213,12 @@ class Player extends EventEmitter {
245
213
 
246
214
  async trackEnd(player, track, payload) {
247
215
  if (this.shouldDeleteMessage && this.nowPlayingMessage) {
248
- await this.nowPlayingMessage.delete().catch(() => {});
216
+ await this.nowPlayingMessage.delete().catch(() => { });
249
217
  this.nowPlayingMessage = null;
250
218
  }
251
219
 
252
220
  const reason = payload.reason?.replace("_", "").toLowerCase();
253
-
221
+
254
222
  if (reason === "loadfailed" || reason === "cleanup") {
255
223
  if (player.queue.isEmpty()) {
256
224
  this.aqua.emit("queueEnd", player);
@@ -283,17 +251,17 @@ class Player extends EventEmitter {
283
251
  await player.play();
284
252
  }
285
253
 
286
- trackError(player, track, payload) {
254
+ async trackError(player, track, payload) {
287
255
  this.aqua.emit("trackError", player, track, payload);
288
256
  return this.stop();
289
257
  }
290
258
 
291
- trackStuck(player, track, payload) {
259
+ async trackStuck(player, track, payload) {
292
260
  this.aqua.emit("trackStuck", player, track, payload);
293
261
  return this.stop();
294
262
  }
295
263
 
296
- socketClosed(player, payload) {
264
+ async socketClosed(player, payload) {
297
265
  if (payload?.code === 4015 || payload?.code === 4009) {
298
266
  this.send({
299
267
  guild_id: payload.guildId,
@@ -311,22 +279,23 @@ class Player extends EventEmitter {
311
279
  this.aqua.send({ op: 4, d: data });
312
280
  }
313
281
 
314
- #dataStore = new Map();
282
+ #dataStore = new WeakMap();
283
+
284
+ set(key, value) {
285
+ this.#dataStore.set(key, value);
286
+ }
287
+
288
+ get(key) {
289
+ return this.#dataStore.get(key);
290
+ }
291
+
292
+ clearData() {
293
+ this.#dataStore = new WeakMap();
294
+ return this;
295
+ }
315
296
 
316
- set(key, value) {
317
- this.#dataStore.set(key, value);
318
- }
319
-
320
- get(key) {
321
- return this.#dataStore.get(key);
322
- }
323
-
324
- clearData() {
325
- this.#dataStore.clear();
326
- return this;
327
- }
328
297
 
329
- updatePlayer(data) {
298
+ updatePlayer(data) {
330
299
  return this.nodes.rest.updatePlayer({ guildId: this.guildId, data });
331
300
  }
332
301
 
@@ -2,7 +2,7 @@
2
2
  const { Pool } = require("undici");
3
3
 
4
4
  class Rest {
5
- constructor(aqua, { secure, host, port, sessionId, password,}) {
5
+ constructor(aqua, { secure, host, port, sessionId, password }) {
6
6
  this.aqua = aqua;
7
7
  this.sessionId = sessionId;
8
8
  this.version = "v4";
@@ -11,9 +11,7 @@ class Rest {
11
11
  "Content-Type": "application/json",
12
12
  Authorization: password,
13
13
  };
14
- this.client = new Pool(this.baseUrl, {
15
- pipelining: 1,
16
- });
14
+ this.client = new Pool(this.baseUrl, { pipelining: 1 });
17
15
  }
18
16
 
19
17
  setSessionId(sessionId) {
@@ -21,101 +19,77 @@ class Rest {
21
19
  }
22
20
 
23
21
  async makeRequest(method, endpoint, body = null) {
24
- const options = {
25
- path: endpoint,
26
- method,
27
- headers: this.headers,
28
- ...(body && { body: JSON.stringify(body) }),
29
- };
30
22
  try {
31
- const response = await this.client.request(options);
32
- const { statusCode } = response;
33
- return statusCode === 204 ? null : await response.body.json();
23
+ const response = await this.client.request({
24
+ path: endpoint,
25
+ method,
26
+ headers: this.headers,
27
+ body: body ? JSON.stringify(body) : undefined,
28
+ });
29
+
30
+ return response.statusCode === 204 ? null : await response.body.json();
34
31
  } catch (error) {
35
- throw new Error(`Request to ${endpoint} failed: ${error.message}`);
32
+ throw new Error(`Request failed (${method} ${endpoint}): ${error.message}`);
36
33
  }
37
34
  }
38
35
 
39
- buildEndpoint(...segments) {
40
- const validSegments = segments.filter(segment => segment && segment.trim());
41
- return '/' + validSegments.join('/');
42
- }
43
-
44
36
  validateSessionId() {
45
- if (!this.sessionId) {
46
- throw new Error("Session ID is not set.");
47
- }
37
+ if (!this.sessionId) throw new Error("Session ID is not set.");
48
38
  }
49
39
 
50
40
  async updatePlayer({ guildId, data }) {
51
- const hasEncodedTrack = data.track?.encoded && data.track?.identifier;
52
- const hasEncodedTrackAlt = data.encodedTrack && data.identifier;
53
-
54
- if (hasEncodedTrack || hasEncodedTrackAlt) {
41
+ if ((data.track?.encoded && data.track?.identifier) || (data.encodedTrack && data.identifier)) {
55
42
  throw new Error("Cannot provide both 'encoded' and 'identifier' for track");
56
43
  }
57
-
58
44
  this.validateSessionId();
59
- const endpoint = this.buildEndpoint(this.version, "sessions", this.sessionId, "players", guildId) + "?noReplace=false";
60
- return this.makeRequest("PATCH", endpoint, data);
45
+ return this.makeRequest("PATCH", `/${this.version}/sessions/${this.sessionId}/players/${guildId}?noReplace=false`, data);
61
46
  }
62
47
 
63
48
  async getPlayers() {
64
49
  this.validateSessionId();
65
- const endpoint = this.buildEndpoint(this.version, "sessions", this.sessionId, "players");
66
- return this.makeRequest("GET", endpoint);
50
+ return this.makeRequest("GET", `/${this.version}/sessions/${this.sessionId}/players`);
67
51
  }
68
52
 
69
53
  async destroyPlayer(guildId) {
70
54
  this.validateSessionId();
71
- const endpoint = this.buildEndpoint(this.version, "sessions", this.sessionId, "players", guildId);
72
- return this.makeRequest("DELETE", endpoint);
55
+ return this.makeRequest("DELETE", `/${this.version}/sessions/${this.sessionId}/players/${guildId}`);
73
56
  }
74
57
 
75
58
  async getTracks(identifier) {
76
- const endpoint = `/${this.version}/loadtracks?identifier=${encodeURIComponent(identifier)}`;
77
- return this.makeRequest("GET", endpoint);
59
+ return this.makeRequest("GET", `/${this.version}/loadtracks?identifier=${encodeURIComponent(identifier)}`);
78
60
  }
79
61
 
80
62
  async decodeTrack(track) {
81
- const endpoint = `/${this.version}/decodetrack?encodedTrack=${encodeURIComponent(track)}`;
82
- return this.makeRequest("GET", endpoint);
63
+ return this.makeRequest("GET", `/${this.version}/decodetrack?encodedTrack=${encodeURIComponent(track)}`);
83
64
  }
84
65
 
85
66
  async decodeTracks(tracks) {
86
- const endpoint = `/${this.version}/decodetracks`;
87
- return this.makeRequest("POST", endpoint, tracks);
67
+ return this.makeRequest("POST", `/${this.version}/decodetracks`, tracks);
88
68
  }
89
69
 
90
70
  async getStats() {
91
- const endpoint = `/${this.version}/stats`;
92
- return this.makeRequest("GET", endpoint);
71
+ return this.makeRequest("GET", `/${this.version}/stats`);
93
72
  }
94
73
 
95
74
  async getInfo() {
96
- const endpoint = `/${this.version}/info`;
97
- return this.makeRequest("GET", endpoint);
75
+ return this.makeRequest("GET", `/${this.version}/info`);
98
76
  }
99
77
 
100
78
  async getRoutePlannerStatus() {
101
- const endpoint = `/${this.version}/routeplanner/status`;
102
- return this.makeRequest("GET", endpoint);
79
+ return this.makeRequest("GET", `/${this.version}/routeplanner/status`);
103
80
  }
104
81
 
105
82
  async getRoutePlannerAddress(address) {
106
- const endpoint = `/${this.version}/routeplanner/free/address`;
107
- return this.makeRequest("POST", endpoint, { address });
83
+ return this.makeRequest("POST", `/${this.version}/routeplanner/free/address`, { address });
108
84
  }
109
85
 
110
86
  async getLyrics({ track }) {
111
87
  if (track.search) {
112
- const endpoint = `/v4/lyrics/search?query=${encodeURIComponent(track.encoded.info.title)}&source=genius`;
113
- const res = await this.makeRequest("GET", endpoint);
88
+ const res = await this.makeRequest("GET", `/${this.version}/lyrics/search?query=${encodeURIComponent(track.encoded.info.title)}&source=genius`);
114
89
  if (res) return res;
115
90
  }
116
91
  this.validateSessionId();
117
- const endpoint = this.buildEndpoint(this.version, "sessions", this.sessionId, "players", track.guild_id, "track", "lyrics") + "?skipTrackSource=false";
118
- return this.makeRequest("GET", endpoint);
92
+ return this.makeRequest("GET", `/${this.version}/sessions/${this.sessionId}/players/${track.guild_id}/track/lyrics?skipTrackSource=false`);
119
93
  }
120
94
  }
121
95
 
@@ -1,16 +1,7 @@
1
1
  "use strict";
2
2
  const { getImageUrl } = require("../handlers/fetchImage");
3
- /**
4
- * @typedef {import("../Aqua")} Aqua
5
- * @typedef {import("../structures/Player")} Player
6
- * @typedef {import("../structures/Node")} Node
7
- */
3
+
8
4
  class Track {
9
- /**
10
- * @param {Object} data
11
- * @param {Player} requester
12
- * @param {Node} nodes
13
- */
14
5
  constructor(data = {}, requester, nodes) {
15
6
  const { info = {}, encoded = null, playlist = null } = data;
16
7
  this.info = {
@@ -29,21 +20,18 @@ class Track {
29
20
  this.requester = requester;
30
21
  this.nodes = nodes;
31
22
  }
32
- /**
33
- * @param {string} thumbnail
34
- * @returns {string|null}
35
- */
23
+
36
24
  resolveThumbnail(thumbnail) {
37
- if (!thumbnail) return null;
38
- return getImageUrl(thumbnail);
25
+ return thumbnail ? getImageUrl(thumbnail) : null;
39
26
  }
40
- /**
41
- * @param {Aqua} aqua
42
- * @returns {Promise<Track|null>}
43
- */
27
+
44
28
  async resolve(aqua) {
45
29
  const searchPlatform = aqua?.options?.defaultSearchPlatform;
46
- if (!searchPlatform) return null;
30
+ if (!searchPlatform) {
31
+ console.warn("No search platform configured.");
32
+ return null;
33
+ }
34
+
47
35
  try {
48
36
  const query = `${this.info.author} - ${this.info.title}`;
49
37
  const result = await aqua.resolve({
@@ -52,9 +40,12 @@ class Track {
52
40
  requester: this.requester,
53
41
  node: this.nodes
54
42
  });
43
+
55
44
  if (!result?.tracks?.length) return null;
56
45
  const track = this._findMatchingTrack(result.tracks);
57
46
  if (!track) return null;
47
+
48
+ // Update track info if a match is found
58
49
  this.info.identifier = track.info.identifier;
59
50
  this.track = track.track;
60
51
  this.playlist = track.playlist || null;
@@ -64,6 +55,7 @@ class Track {
64
55
  return null;
65
56
  }
66
57
  }
58
+
67
59
  _findMatchingTrack(tracks) {
68
60
  const { author, title, length } = this.info;
69
61
  for (const track of tracks) {
@@ -77,4 +69,5 @@ class Track {
77
69
  return null;
78
70
  }
79
71
  }
80
- module.exports = Track;
72
+
73
+ module.exports = Track;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aqualink",
3
- "version": "1.8.1-beta5",
3
+ "version": "1.9.0-beta2",
4
4
  "description": "An Lavalink wrapper, focused in speed, performance, and features, Based in Riffy!",
5
5
  "main": "build/index.js",
6
6
  "types": "index.d.ts",
@@ -38,7 +38,7 @@
38
38
  "license": "ISC",
39
39
  "dependencies": {
40
40
  "undici": "^7.4.0",
41
- "@toddynnn/pwsl-mini": "github:ToddyTheNoobDud/websocket"
41
+ "ws": "^8.18.1"
42
42
  },
43
43
  "repository": {
44
44
  "type": "git",