aqualink 1.8.1-beta4 → 1.9.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
@@ -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,64 +16,89 @@ 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
22
  - Lyrics Support by Lavalink
23
23
  - https://github.com/topi314/LavaLyrics (RECOMMENDED)
24
24
  - https://github.com/DRSchlaubi/lyrics.kt (?)
25
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
+ - Fixed the `autoResume` system (now will actually work, for 60 seconds).
58
+ - New WebSocket System.
59
+ - Improved the events handling speed.
60
+ - Now does recalculation of the backoff time (for more efficiency on reconnect).
61
+ - Now avoids reconnecting if the WebSocket is already open (sorry, I forgot to add this before).
62
+ - Better cleaning system (improved, now removes listeners instead of setting to `null`).
63
+ - Avoids re-binding the functions every time `connect` is called (yay).
64
+ - This update also improves long-process running.
65
+
66
+ ---
67
+
68
+ ### **Remade the `Player` module (also a good one)**
69
+ - Remade every method.
70
+ - Fixed destroy system.
71
+ - Better event handling, I think.
72
+ - Made the events async.
73
+ - Removed `trackChange` (does not exist in Lavalink API, use `trackStart` instead).
74
+ - Uses a new listener system (way more efficient for creating/destroying players).
75
+ - Faster shuffle in V8 Engine (Math stuff).
76
+ - Improved overall configs (more precise).
77
+ - Use `pop()` instead of disabling the track 50 on the length.
78
+ - Improved overall speed on the check-ins and some stuff I forgot.
79
+
80
+ ---
81
+
82
+ ### **Remade the `Rest` module**
83
+ - Better speed (removed useless `buildEndpoint`).
84
+ - More compact code.
85
+ - Removed `stats/all` in the stats (correct by using the Lavalink API).
86
+ - Better `makeRequest`.
87
+
88
+ ---
89
+
90
+ ### **Small changes in `Track` module**
91
+ - More efficient final result (`author` + `track`).
92
+
93
+ ---
94
+
95
+ That’s all for **1.9.0** atm. I’m a lazy dev. 😴
26
96
 
27
97
  # Docs (Wiki)
28
98
  - https://github.com/ToddyTheNoobDud/AquaLink/wiki
29
99
 
30
100
  - Example bot: https://github.com/ToddyTheNoobDud/Thorium-Music
31
101
 
32
- # Brick by brick, 1.8.0 Update (yay)
33
-
34
- ### 1.8.1-beta4 Update:
35
- - Use pool for connections (Experimental, help me improve it. Undici pool)
36
- - Default will not leave the VC anymore (leaveOnEnd: false default)
37
- - Misc optimizations on node and player
38
-
39
- ### 1.8.0
40
- - Misc changes on FetchImage (improves the overall checking and speed)
41
- - Rewrite `AQUA` module
42
- - Remade the resolve logic (improves the speed by a lot)
43
- - Fixes many memory usages related to nodes
44
- - send is no longer required to be Applied (Applied by default now.)
45
- - Remade some stuff with discord VoiceGateway
46
-
47
- - Remade `CONNECTION` module
48
- - Way faster connections (Joining, reconnecting, connected, disconnect)
49
- - Reduced memory overload by removing useless code
50
- - Improved early Returns
51
-
52
- - Remade `NODE` module
53
- - MANY fixes for the connection logic (fixes reconnection, etc)
54
- - Fixed memory leaks in heartbeat system (hopefully, reduced memory by a lot.)
55
- - Faster connection speed and checkings
56
- - Remade the Options system, improve JSON parsing
57
-
58
- - Rewrite `PLAYER` module
59
- - Many memory related fixes
60
- - Improved the overall code speed by a lot
61
- - Rewrote setLoop, play, shuffle, replay methods (fixes + performance)
62
- - Added 2 new options:
63
-
64
- leaveOnEnd: false, // Optional
65
-
66
- shouldDeleteMessage: true // Optional
67
-
68
- - Uses array for better performance and less memory allocation
69
- - Rewrite the Events handling (speed and recourses fixes)
70
-
71
- - Updated `TRACK` module
72
- - Better object handling for internal code.
73
- - Removed an useless method
74
-
75
- Thats all for now, im lazy, help me fix code and improve this on github... i can't test properly 😭😭😭
76
-
77
102
  # How to install
78
103
 
79
104
  `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("ws");
2
+ const WebSocket = require("@toddynnn/pwsl-mini");
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;
@@ -59,6 +58,8 @@ class Node {
59
58
  }
60
59
 
61
60
  async connect() {
61
+ if (this.#ws && this.#ws.readyState === WebSocket.OPEN) return;
62
+
62
63
  this.#ws = new WebSocket(this.wsUrl.href, {
63
64
  headers: this.#constructHeaders(),
64
65
  perMessageDeflate: false,
@@ -70,35 +71,35 @@ class Node {
70
71
  }
71
72
 
72
73
  #constructHeaders() {
73
- return {
74
+ const headers = {
74
75
  Authorization: this.password,
75
76
  "User-Id": this.aqua.clientId,
76
77
  "Client-Name": `Aqua/${this.aqua.version}`,
77
- ...(this.sessionId && { "Session-Id": this.sessionId }),
78
- ...(this.resumeKey && { "Resume-Key": this.resumeKey })
79
78
  };
79
+ if (this.sessionId) headers["Session-Id"] = this.sessionId;
80
+ return headers;
80
81
  }
81
82
 
82
83
  async #onOpen() {
83
84
  this.connected = true;
84
85
  this.#reconnectAttempted = 0;
85
86
  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");
87
+
88
+ try {
89
+ this.info = await this.rest.makeRequest("GET", "/v4/info");
90
+ if (this.autoResume && this.sessionId) {
90
91
  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
- }
92
+ }
93
+ } catch (err) {
94
+ this.info = null;
95
+ if (!this.aqua.bypassChecks?.nodeFetchInfo) {
96
+ this.aqua.emit("error", this, `Failed to fetch node info: ${err.message}`);
96
97
  }
97
98
  }
98
99
  }
99
100
 
100
101
  async getStats() {
101
- const stats = await this.rest.makeRequest("GET", "/v4/stats");
102
+ const stats = await this.rest.getStats();
102
103
  this.stats = { ...this.defaultStats, ...stats };
103
104
  return this.stats;
104
105
  }
@@ -108,8 +109,9 @@ class Node {
108
109
  try {
109
110
  payload = JSON.parse(msg);
110
111
  } catch {
111
- return;
112
+ return; // Invalid JSON, ignore the message
112
113
  }
114
+
113
115
  const op = payload?.op;
114
116
  if (!op) return;
115
117
 
@@ -125,6 +127,18 @@ class Node {
125
127
  }
126
128
  }
127
129
 
130
+ async resumePlayers() {
131
+ try {
132
+ await this.rest.makeRequest("PATCH", `/v4/sessions/${this.sessionId}`, {
133
+ resuming: true,
134
+ timeout: this.resumeTimeout
135
+ });
136
+ this.aqua.emit("debug", this.name, `Successfully resumed session ${this.sessionId}`);
137
+ } catch (err) {
138
+ this.aqua.emit("error", this, `Failed to resume session: ${err.message}`);
139
+ }
140
+ }
141
+
128
142
  #updateStats(payload) {
129
143
  if (!payload) return;
130
144
  this.stats = {
@@ -170,14 +184,20 @@ class Node {
170
184
  }
171
185
 
172
186
  #handleReadyOp(payload) {
173
- if (this.sessionId !== payload.sessionId) {
174
- this.sessionId = payload.sessionId;
175
- this.rest.setSessionId(payload.sessionId);
187
+ if (!payload.sessionId) {
188
+ this.aqua.emit("error", this, "Ready payload missing sessionId");
189
+ return;
176
190
  }
191
+
192
+ this.sessionId = payload.sessionId;
193
+ this.rest.setSessionId(payload.sessionId);
194
+
195
+ // Don't resume here - we'll handle resuming in the #onOpen method if autoResume is enabled
177
196
  this.aqua.emit("nodeConnect", this);
178
197
  }
179
198
 
180
199
  #handlePlayerOp(payload) {
200
+ if (!payload.guildId) return;
181
201
  const player = this.aqua.players.get(payload.guildId);
182
202
  if (player) player.emit(payload.op, payload);
183
203
  }
@@ -188,7 +208,7 @@ class Node {
188
208
 
189
209
  #onClose(code, reason) {
190
210
  this.connected = false;
191
- this.aqua.emit("nodeDisconnect", this, { code, reason });
211
+ this.aqua.emit("nodeDisconnect", this, { code, reason: reason?.toString() || "No reason provided" });
192
212
  this.#reconnect();
193
213
  }
194
214
 
@@ -198,22 +218,24 @@ class Node {
198
218
  setTimeout(() => this.connect(), 10000);
199
219
  return;
200
220
  }
221
+
201
222
  if (this.#reconnectAttempted >= this.reconnectTries) {
202
223
  this.aqua.emit("nodeError", this,
203
224
  new Error(`Max reconnection attempts reached (${this.reconnectTries})`));
204
225
  this.destroy(true);
205
226
  return;
206
227
  }
228
+
207
229
  clearTimeout(this.#reconnectTimeoutId);
208
230
  const jitter = Math.random() * 10000;
209
231
  const backoffTime = Math.min(
210
232
  this.reconnectTimeout * Math.pow(Node.BACKOFF_MULTIPLIER, this.#reconnectAttempted) + jitter,
211
233
  Node.MAX_BACKOFF
212
234
  );
235
+
213
236
  this.#reconnectTimeoutId = setTimeout(() => {
214
237
  this.#reconnectAttempted++;
215
- this.aqua.emit("nodeReconnect", {
216
- nodeName: this.name,
238
+ this.aqua.emit("nodeReconnect", this, {
217
239
  attempt: this.#reconnectAttempted,
218
240
  backoffTime
219
241
  });
@@ -222,11 +244,22 @@ class Node {
222
244
  }
223
245
 
224
246
  destroy(clean = false) {
247
+ clearTimeout(this.#reconnectTimeoutId);
248
+
249
+ if (this.#ws) {
250
+ this.#ws.removeAllListeners();
251
+ if (this.#ws.readyState === WebSocket.OPEN) {
252
+ this.#ws.close();
253
+ }
254
+ this.#ws = null;
255
+ }
256
+
225
257
  if (clean) {
226
258
  this.aqua.emit("nodeDestroy", this);
227
259
  this.aqua.nodes.delete(this.name);
228
260
  return;
229
261
  }
262
+
230
263
  if (this.connected) {
231
264
  for (const player of this.aqua.players.values()) {
232
265
  if (player.node === this) {
@@ -234,11 +267,12 @@ class Node {
234
267
  }
235
268
  }
236
269
  }
270
+
237
271
  this.connected = false;
238
- this.aqua.nodeMap.delete(this.name);
272
+ this.aqua.nodes.delete(this.name);
239
273
  this.aqua.emit("nodeDestroy", this);
240
274
  this.info = null;
241
275
  }
242
276
  }
243
277
 
244
- module.exports = Node;
278
+ 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() {
@@ -238,19 +206,19 @@ class Player extends EventEmitter {
238
206
  this.aqua.emit("trackStart", player, track);
239
207
  }
240
208
 
241
- trackChange(player, track) {
209
+ async trackChange(player, track) {
242
210
  this.updateTrackState(true, false);
243
211
  this.aqua.emit("trackChange", player, track);
244
212
  }
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/all`;
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-beta4",
3
+ "version": "1.9.0",
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",
@@ -37,8 +37,8 @@
37
37
  "author": "mushroom0162 (https://github.com/ToddyTheNoobDud)",
38
38
  "license": "ISC",
39
39
  "dependencies": {
40
- "undici": "^7.3.0",
41
- "ws": "^8.18.1"
40
+ "undici": "^7.4.0",
41
+ "@toddynnn/pwsl-mini": "github:ToddyTheNoobDud/websocket"
42
42
  },
43
43
  "repository": {
44
44
  "type": "git",