aqualink 1.7.0-beta4 → 1.7.0-beta6

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.
@@ -1,19 +1,12 @@
1
1
  const { request } = require("undici");
2
2
 
3
3
  const sourceHandlers = new Map([
4
- ['spotify', (uri) => fetchThumbnail(`https://open.spotify.com/oembed?url=${uri}`)],
5
- ['youtube', (identifier) => fetchYouTubeThumbnail(identifier)]
4
+ ['spotify', uri => fetchThumbnail(`https://open.spotify.com/oembed?url=${uri}`)],
5
+ ['youtube', identifier => fetchYouTubeThumbnail(identifier)]
6
6
  ]);
7
7
 
8
- const YOUTUBE_URL_TEMPLATE = (quality) =>
9
- (id) => `https://img.youtube.com/vi/${id}/${quality}.jpg`;
10
-
11
- const YOUTUBE_QUALITIES = [
12
- 'maxresdefault',
13
- 'hqdefault',
14
- 'mqdefault',
15
- 'default'
16
- ].map(YOUTUBE_URL_TEMPLATE);
8
+ const YOUTUBE_URL_TEMPLATE = quality => id => `https://img.youtube.com/vi/${id}/${quality}.jpg`;
9
+ const YOUTUBE_QUALITIES = ['maxresdefault', 'hqdefault', 'mqdefault', 'default'].map(YOUTUBE_URL_TEMPLATE);
17
10
 
18
11
  async function getImageUrl(info) {
19
12
  if (!info?.sourceName || !info?.uri) return null;
@@ -23,7 +16,8 @@ async function getImageUrl(info) {
23
16
 
24
17
  try {
25
18
  return await handler(info.uri);
26
- } catch {
19
+ } catch (error) {
20
+ console.error('Error fetching image URL:', error);
27
21
  return null;
28
22
  }
29
23
  }
@@ -32,26 +26,23 @@ async function fetchThumbnail(url) {
32
26
  try {
33
27
  const { body } = await request(url, {
34
28
  method: "GET",
35
- headers: {
36
- 'Accept': 'application/json'
37
- }
29
+ headers: { 'Accept': 'application/json' }
38
30
  });
39
-
40
31
  const json = await body.json();
41
32
  return json.thumbnail_url || null;
42
- } catch {
33
+ } catch (error) {
34
+ console.error('Error fetching thumbnail:', error);
43
35
  return null;
44
36
  }
45
37
  }
46
38
 
47
39
  async function fetchYouTubeThumbnail(identifier) {
48
- const fetchPromises = YOUTUBE_QUALITIES.map(urlFunc =>
49
- fetchThumbnail(urlFunc(identifier))
50
- );
40
+ const fetchPromises = YOUTUBE_QUALITIES.map(urlFunc => fetchThumbnail(urlFunc(identifier)));
51
41
 
52
42
  try {
53
43
  return await Promise.race(fetchPromises);
54
- } catch {
44
+ } catch (error) {
45
+ console.error('Error fetching YouTube thumbnail:', error);
55
46
  return null;
56
47
  }
57
48
  }
@@ -4,29 +4,29 @@ const { Player } = require("./Player");
4
4
  const { Track } = require("./Track");
5
5
  const { version: pkgVersion } = require("../../package.json");
6
6
  const URL_REGEX = /^https?:\/\//;
7
+
7
8
  class Aqua extends EventEmitter {
8
9
  /**
9
- * @param {Object} client - The client instance.
10
- * @param {Array<Object>} nodes - An array of node configurations.
11
- * @param {Object} options - Configuration options for Aqua.
12
- * @param {Function} options.send - Function to send data.
13
- * @param {string} [options.defaultSearchPlatform="ytsearch"] - Default search platform. Options include:
14
- * - "youtube music": "ytmsearch"
15
- * - "youtube": "ytsearch"
16
- * - "spotify": "spsearch"
17
- * - "jiosaavn": "jssearch"
18
- * - "soundcloud": "scsearch"
19
- * - "deezer": "dzsearch"
20
- * - "tidal": "tdsearch"
21
- * - "applemusic": "amsearch"
22
- * - "bandcamp": "bcsearch"
23
- * @param {string} [options.restVersion="v4"] - Version of the REST API.
24
- * @param {Array<Object>} [options.plugins=[]] - Plugins to load.
25
- * @param {string} [options.shouldDeleteMessage='none'] - Should delete your message? (true, false)
26
- * @param {boolean} [options.autoResume=false] - Automatically resume tracks on reconnect.
27
- * @param {boolean} [options.infiniteReconnects=false] - Reconnect infinitely (default: false).
28
- */
29
- constructor(client, nodes, options) {
10
+ * @param {Object} client - The client instance.
11
+ * @param {Array<Object>} nodes - An array of node configurations.
12
+ * @param {Object} options - Configuration options for Aqua.
13
+ * @param {Function} options.send - Function to send data.
14
+ * @param {string} [options.defaultSearchPlatform="ytsearch"] - Default search platform. Options include:
15
+ * - "youtube music": "ytmsearch"
16
+ * - "youtube": "ytsearch"
17
+ * - "spotify": "spsearch"
18
+ * - "jiosaavn": "jssearch"
19
+ * - "soundcloud": "scsearch"
20
+ * - "deezer": "dzsearch"
21
+ * - "tidal": "tdsearch"
22
+ * - "applemusic": "amsearch"
23
+ * - "bandcamp": "bcsearch"
24
+ * @param {string} [options.restVersion="v4"] - Version of the REST API.
25
+ * @param {Array<Object>} [options.plugins=[]] - Plugins to load.
26
+ * @param {boolean} [options.autoResume=false] - Automatically resume tracks on reconnect.
27
+ * @param {boolean} [options.infiniteReconnects=false] - Reconnect infinitely (default: false).
28
+ */
29
+ constructor(client, nodes, options = {}) {
30
30
  super();
31
31
  this.validateInputs(client, nodes, options);
32
32
  this.client = client;
@@ -35,28 +35,37 @@ class Aqua extends EventEmitter {
35
35
  this.players = new Map();
36
36
  this.clientId = null;
37
37
  this.initiated = false;
38
- this.shouldDeleteMessage = options.shouldDeleteMessage || false;
39
- this.defaultSearchPlatform = options.defaultSearchPlatform || "ytsearch";
40
- this.restVersion = options.restVersion || "v4";
41
- this.plugins = options.plugins || [];
42
- this.version = pkgVersion;
43
38
  this.options = options;
44
- this.send = options.send;
45
- this.autoResume = options.autoResume || false;
46
- this.infiniteReconnects = options.infiniteReconnects || false;
39
+
40
+ this.shouldDeleteMessage = this.getOption(options, 'shouldDeleteMessage', false);
41
+ this.defaultSearchPlatform = this.getOption(options, 'defaultSearchPlatform', 'ytsearch');
42
+ this.leaveOnEnd = this.getOption(options, 'leaveOnEnd', true);
43
+ this.restVersion = this.getOption(options, 'restVersion', 'v4');
44
+ this.plugins = this.getOption(options, 'plugins', []);
45
+ this.version = pkgVersion;
46
+ this.send = options.send || this.defaultSendFunction;
47
+ this.autoResume = this.getOption(options, 'autoResume', false);
48
+ this.infiniteReconnects = this.getOption(options, 'infiniteReconnects', false);
49
+
47
50
  this.setMaxListeners(0);
48
51
  }
49
52
 
53
+ getOption(options, key, defaultValue) {
54
+ return options.hasOwnProperty(key) ? options[key] : defaultValue;
55
+ }
56
+
57
+ defaultSendFunction(payload) {
58
+ const guild = this.client.guilds.cache.get(payload.d.guild_id);
59
+ if (guild) guild.shard.send(payload);
60
+ }
50
61
 
51
62
  validateInputs(client, nodes, options) {
52
63
  if (!client) throw new Error("Client is required to initialize Aqua");
53
64
  if (!Array.isArray(nodes) || !nodes.length) throw new Error(`Nodes must be a non-empty Array (Received ${typeof nodes})`);
54
- if (typeof options?.send !== "function") throw new Error("Send function is required to initialize Aqua");
55
65
  }
56
66
 
57
67
  get leastUsedNodes() {
58
- const activeNodes = [...this.nodeMap.values()].filter(node => node.connected);
59
- return activeNodes.length ? activeNodes.sort((a, b) => a.rest.calls - b.rest.calls) : [];
68
+ return [...this.nodeMap.values()].filter(node => node.connected).sort((a, b) => a.rest.calls - b.rest.calls);
60
69
  }
61
70
 
62
71
  init(clientId) {
@@ -78,41 +87,42 @@ class Aqua extends EventEmitter {
78
87
  this.destroyNode(nodeId); // Ensure no duplicate nodes
79
88
  const node = new Node(this, options, this.options);
80
89
  this.nodeMap.set(nodeId, node);
81
- try {
82
- node.connect();
83
- this.emit("nodeCreate", node);
84
- return node;
85
- } catch (error) {
86
- this.nodeMap.delete(nodeId);
87
- throw error;
88
- }
90
+ node.connect()
91
+ .then(() => this.emit("nodeCreate", node))
92
+ .catch(error => {
93
+ this.nodeMap.delete(nodeId);
94
+ throw error;
95
+ });
96
+ return node;
89
97
  }
90
98
 
91
99
  destroyNode(identifier) {
92
100
  const node = this.nodeMap.get(identifier);
93
101
  if (!node) return;
94
- try {
95
- node.disconnect();
96
- node.removeAllListeners();
97
- this.nodeMap.delete(identifier);
98
- this.emit("nodeDestroy", node);
99
- } catch (error) {
100
- console.error(`Error destroying node ${identifier}:`, error);
101
- }
102
+
103
+ node.disconnect()
104
+ .then(() => {
105
+ node.removeAllListeners();
106
+ this.nodeMap.delete(identifier);
107
+ this.emit("nodeDestroy", node);
108
+ })
109
+ .catch(error => console.error(`Error destroying node ${identifier}:`, error));
102
110
  }
103
111
 
104
112
  updateVoiceState({ d, t }) {
105
113
  const player = this.players.get(d.guild_id);
106
- if (player && (t === "VOICE_SERVER_UPDATE" || t === "VOICE_STATE_UPDATE" && d.user_id === this.clientId)) {
114
+ if (player && (t === "VOICE_SERVER_UPDATE" || (t === "VOICE_STATE_UPDATE" && d.user_id === this.clientId))) {
107
115
  player.connection[t === "VOICE_SERVER_UPDATE" ? "setServerUpdate" : "setStateUpdate"](d);
108
116
  if (d.status === "disconnected") this.cleanupPlayer(player);
109
117
  }
110
118
  }
119
+
111
120
  fetchRegion(region) {
112
121
  if (!region) return this.leastUsedNodes;
113
122
  const lowerRegion = region.toLowerCase();
114
- const eligibleNodes = [...this.nodeMap.values()].filter(node => node.connected && node.regions?.includes(lowerRegion));
115
- return eligibleNodes.sort((a, b) => this.calculateLoad(a) - this.calculateLoad(b));
123
+ return [...this.nodeMap.values()]
124
+ .filter(node => node.connected && node.regions?.includes(lowerRegion))
125
+ .sort((a, b) => this.calculateLoad(a) - this.calculateLoad(b));
116
126
  }
117
127
 
118
128
  calculateLoad(node) {
@@ -125,6 +135,7 @@ class Aqua extends EventEmitter {
125
135
  this.ensureInitialized();
126
136
  const player = this.players.get(options.guildId);
127
137
  if (player && player.voiceChannel) return player;
138
+
128
139
  const node = options.region ? this.fetchRegion(options.region)[0] : this.leastUsedNodes[0];
129
140
  if (!node) throw new Error("No nodes are available");
130
141
  return this.createPlayer(node, options);
@@ -140,22 +151,21 @@ class Aqua extends EventEmitter {
140
151
  return player;
141
152
  }
142
153
 
143
- async destroyPlayer(guildId) {
144
- const player = this.players.get(guildId);
145
- if (!player) return;
154
+ async destroyPlayer(guildId) {
155
+ const player = this.players.get(guildId);
156
+ if (!player) return;
146
157
 
147
- try {
148
- // Ensure that clearData and destroy are awaited if they are async
149
- await player.clearData(); // Assuming clearData is an async function
150
- player.removeAllListeners(); // This should not cause an infinite loop
151
- this.players.delete(guildId);
152
- this.emit("playerDestroy", player);
153
- } catch (error) {
154
- console.error(`Error destroying player for guild ${guildId}:`, error);
158
+ try {
159
+ await player.clearData(); // Assuming clearData is an async function
160
+ player.removeAllListeners();
161
+ this.players.delete(guildId);
162
+ this.emit("playerDestroy", player);
163
+ } catch (error) {
164
+ console.error(`Error destroying player for guild ${guildId}:`, error);
165
+ }
155
166
  }
156
- }
157
167
 
158
- async resolve({ query, source = this.defaultSearchPlatform , requester, nodes }) {
168
+ async resolve({ query, source = this.defaultSearchPlatform, requester, nodes }) {
159
169
  this.ensureInitialized();
160
170
  const requestNode = this.getRequestNode(nodes);
161
171
  const formattedQuery = this.formatQuery(query, source);
@@ -188,11 +198,11 @@ async destroyPlayer(guildId) {
188
198
  return URL_REGEX.test(query) ? query : `${source}:${query}`;
189
199
  }
190
200
 
191
- handleNoMatches(rest, query) {
201
+ async handleNoMatches(rest, query) {
192
202
  try {
193
- const youtubeResponse = rest.makeRequest("GET", `/v4/loadtracks?identifier=https://www.youtube.com/watch?v=${query}`, { signal: controller.signal });
203
+ const youtubeResponse = await rest.makeRequest("GET", `/v4/loadtracks?identifier=https://www.youtube.com/watch?v=${query}`);
194
204
  if (["empty", "NO_MATCHES"].includes(youtubeResponse.loadType)) {
195
- return rest.makeRequest("GET", `/v4/loadtracks?identifier=https://open.spotify.com/track/${query}`, { signal: controller.signal });
205
+ return await rest.makeRequest("GET", `/v4/loadtracks?identifier=https://open.spotify.com/track/${query}`);
196
206
  }
197
207
  return youtubeResponse;
198
208
  } catch (error) {
@@ -208,10 +218,12 @@ async destroyPlayer(guildId) {
208
218
  pluginInfo: response.pluginInfo ?? {},
209
219
  tracks: [],
210
220
  };
221
+
211
222
  if (response.loadType === "error" || response.loadType === "LOAD_FAILED") {
212
223
  baseResponse.exception = response.data ?? response.exception;
213
224
  return baseResponse;
214
225
  }
226
+
215
227
  const trackFactory = (trackData) => new Track(trackData, requester, requestNode);
216
228
  switch (response.loadType) {
217
229
  case "track":
@@ -283,4 +295,4 @@ async destroyPlayer(guildId) {
283
295
  }
284
296
  }
285
297
 
286
- module.exports = { Aqua };
298
+ module.exports = { Aqua };
@@ -1,106 +1,101 @@
1
1
  class Connection {
2
- constructor(player) {
3
- Object.assign(this, {
4
- player,
5
- voice: { sessionId: null, endpoint: null, token: null },
6
- region: null,
7
- selfDeaf: false,
8
- selfMute: false,
9
- voiceChannel: player.voiceChannel,
10
- _guildId: player.guildId,
11
- _aqua: player.aqua,
12
- _nodes: player.nodes
13
- });
2
+ constructor(player) {
3
+ this.player = player;
4
+ this.voice = {
5
+ sessionId: null,
6
+ endpoint: null,
7
+ token: null
8
+ };
9
+ this.region = null;
10
+ this.selfDeaf = false;
11
+ this.selfMute = false;
12
+ this.voiceChannel = player.voiceChannel;
13
+ this.guildId = player.guildId;
14
+ this.aqua = player.aqua;
15
+ this.nodes = player.nodes;
16
+ }
17
+
18
+ setServerUpdate(data) {
19
+ if (!data?.endpoint) {
20
+ throw new Error("Missing 'endpoint' property");
14
21
  }
15
-
16
- setServerUpdate(data) {
17
- const endpoint = data.endpoint;
18
- if (!endpoint) throw new Error("Missing 'endpoint' property");
19
-
20
- const dotIndex = endpoint.indexOf('.');
21
- if (dotIndex === -1) return;
22
-
23
- const newRegion = endpoint.substring(0, dotIndex).replace(/[0-9]/g, '');
22
+
23
+ const endpoint = data.endpoint;
24
+ const regionMatch = endpoint.match(/^([a-zA-Z]+)/);
25
+ if (!regionMatch) return;
26
+
27
+ const newRegion = regionMatch[1];
28
+ if (this.region !== newRegion) {
29
+ this.voice.endpoint = endpoint;
30
+ this.voice.token = data.token;
24
31
 
25
- if (this.region !== newRegion) {
26
- const prevRegion = this.region;
27
-
28
- Object.assign(this.voice, {
29
- endpoint,
30
- token: data.token
31
- });
32
- this.region = newRegion;
33
-
34
- this._aqua.emit(
35
- "debug",
36
- `[Player ${this._guildId} - CONNECTION] ${
37
- prevRegion
38
- ? `Changed Voice Region from ${prevRegion} to ${newRegion}`
39
- : `Voice Server: ${newRegion}`
40
- }`
41
- );
42
- }
43
-
44
- this._updatePlayerVoiceData();
32
+ const prevRegion = this.region;
33
+ this.region = newRegion;
34
+
35
+ this.aqua.emit(
36
+ "debug",
37
+ `[Player ${this.guildId} - CONNECTION] ${
38
+ prevRegion
39
+ ? `Changed Voice Region from ${prevRegion} to ${newRegion}`
40
+ : `Voice Server: ${newRegion}`
41
+ }`
42
+ );
45
43
  }
46
-
47
44
 
48
- setStateUpdate(data) {
49
- const channelId = data.channel_id;
50
- const sessionId = data.session_id;
51
-
52
- if (!channelId || !sessionId) {
53
- this._cleanup();
54
- return;
55
- }
45
+ this._updatePlayerVoiceData();
46
+ }
56
47
 
57
- if (this.voiceChannel !== channelId) {
58
- this._aqua.emit("playerMove", this.voiceChannel, channelId);
59
- this.voiceChannel = channelId;
60
- }
61
-
62
- Object.assign(this, {
63
- selfDeaf: data.self_deaf,
64
- selfMute: data.self_mute
65
- });
66
- this.voice.sessionId = sessionId;
48
+ setStateUpdate(data) {
49
+ const { channel_id: channelId, session_id: sessionId, self_deaf: selfDeaf, self_mute: selfMute } = data;
50
+
51
+ if (!channelId || !sessionId) {
52
+ this._cleanup();
53
+ return;
54
+ }
55
+
56
+ if (this.voiceChannel !== channelId) {
57
+ this.aqua.emit("playerMove", this.voiceChannel, channelId);
58
+ this.voiceChannel = channelId;
67
59
  }
68
-
69
- _updatePlayerVoiceData() {
70
- this._nodes.rest.updatePlayer({
71
- guildId: this._guildId,
60
+
61
+ this.selfDeaf = selfDeaf;
62
+ this.selfMute = selfMute;
63
+ this.voice.sessionId = sessionId;
64
+ }
65
+
66
+ async _updatePlayerVoiceData() {
67
+ try {
68
+ await this.nodes.rest.updatePlayer({
69
+ guildId: this.guildId,
72
70
  data: {
73
71
  voice: this.voice,
74
72
  volume: this.player.volume
75
73
  }
76
- }).catch(err => {
77
- this._aqua.emit("apiError", "updatePlayer", err);
78
- });
79
- }
80
-
81
- _cleanup() {
82
- const aqua = this._aqua;
83
- const channel = this.player.voiceChannel;
84
-
85
- aqua.emit("playerLeave", channel);
86
-
87
- this.player.voiceChannel = null;
88
- this.voiceChannel = null;
89
- this.player.destroy();
90
-
91
- aqua.emit("playerDestroy", this.player);
92
-
93
- Object.assign(this, {
94
- player: null,
95
- voice: null,
96
- region: null,
97
- selfDeaf: null,
98
- selfMute: null,
99
- _guildId: null,
100
- _aqua: null,
101
- _nodes: null
102
74
  });
75
+ } catch (err) {
76
+ this.aqua.emit("apiError", "updatePlayer", err);
103
77
  }
104
78
  }
105
-
106
- module.exports = { Connection };
79
+
80
+ _cleanup() {
81
+ const { aqua, player, voiceChannel } = this;
82
+
83
+ aqua.emit("playerLeave", voiceChannel);
84
+
85
+ player.voiceChannel = null;
86
+ this.voiceChannel = null;
87
+ player.destroy();
88
+
89
+ aqua.emit("playerDestroy", player);
90
+ this.player = null;
91
+ this.voice = null;
92
+ this.region = null;
93
+ this.selfDeaf = null;
94
+ this.selfMute = null;
95
+ this.guildId = null;
96
+ this.aqua = null;
97
+ this.nodes = null;
98
+ }
99
+ }
100
+
101
+ module.exports = { Connection };
@@ -3,9 +3,11 @@ const { Rest } = require("./Rest");
3
3
 
4
4
  class Node {
5
5
  #ws = null;
6
- #statsCache = {};
6
+ #statsCache = new Map();
7
7
  #lastStatsRequest = 0;
8
8
  #reconnectAttempted = 0;
9
+ #reconnectTimeoutId = null;
10
+
9
11
  constructor(aqua, nodes, options) {
10
12
  const {
11
13
  name,
@@ -35,7 +37,7 @@ class Node {
35
37
  this.infiniteReconnects = options?.infiniteReconnects ?? false;
36
38
  this.connected = false;
37
39
  this.info = null;
38
- this.stats = Object.freeze(this.#createStats());
40
+ this.stats = this.#createStats();
39
41
  }
40
42
 
41
43
  #createStats() {
@@ -43,75 +45,61 @@ class Node {
43
45
  players: 0,
44
46
  playingPlayers: 0,
45
47
  uptime: 0,
46
- memory: Object.freeze({
47
- free: 0,
48
- used: 0,
49
- allocated: 0,
50
- reservable: 0,
51
- freePercentage: 0,
52
- usedPercentage: 0
53
- }),
54
- cpu: Object.freeze({
55
- cores: 0,
56
- systemLoad: 0,
57
- lavalinkLoad: 0,
58
- lavalinkLoadPercentage: 0
59
- }),
60
- frameStats: Object.freeze({
61
- sent: 0,
62
- nulled: 0,
63
- deficit: 0
64
- }),
48
+ memory: { free: 0, used: 0, allocated: 0, reservable: 0, freePercentage: 0, usedPercentage: 0 },
49
+ cpu: { cores: 0, systemLoad: 0, lavalinkLoad: 0, lavalinkLoadPercentage: 0 },
50
+ frameStats: { sent: 0, nulled: 0, deficit: 0 },
65
51
  ping: 0
66
52
  };
67
53
  }
68
54
 
69
55
  async connect() {
70
56
  this.#cleanup();
71
- try {
72
- this.#ws = new WebSocket(this.wsUrl.href, {
73
- headers: this.#constructHeaders(),
74
- perMessageDeflate: false
75
- });
76
- this.#setupWebSocketListeners();
77
- this.aqua.emit('debug', this.name, 'Connecting...');
78
- } catch (err) {
79
- this.aqua.emit('debug', this.name, `Connection failed: ${err.message}`);
80
- this.#reconnect();
81
- }
57
+ this.#ws = new WebSocket(this.wsUrl.href, {
58
+ headers: this.#constructHeaders(),
59
+ perMessageDeflate: false,
60
+ handshakeTimeout: 5000
61
+ });
62
+
63
+ this.#ws.once('open', () => {
64
+ this.#onOpen();
65
+ });
66
+
67
+ this.#setupWebSocketListeners();
68
+ this.aqua.emit('debug', this.name, 'Connecting...');
82
69
  }
83
70
 
84
71
  #cleanup() {
85
72
  if (this.#ws) {
86
- try {
87
- this.#ws.removeAllListeners();
88
- this.#ws.terminate();
89
- } catch (err) {
90
- this.aqua.emit('debug', `Cleanup error: ${err.message}`);
91
- } finally {
92
- this.#ws = null;
93
- }
73
+ this.#ws.removeAllListeners();
74
+ this.#ws.terminate();
75
+ this.#ws = null;
94
76
  }
77
+ this.info = null;
78
+ this.#statsCache.clear();
95
79
  }
96
80
 
97
81
  #constructHeaders() {
98
- const headers = {
82
+ return {
99
83
  Authorization: this.password,
100
84
  "User-Id": this.aqua.clientId,
101
85
  "Client-Name": `Aqua/${this.aqua.version}`,
86
+ ...(this.sessionId && { "Session-Id": this.sessionId }),
87
+ ...(this.resumeKey && { "Resume-Key": this.resumeKey })
102
88
  };
103
- if (this.sessionId) headers["Session-Id"] = this.sessionId;
104
- if (this.resumeKey) headers["Resume-Key"] = this.resumeKey;
105
- return Object.freeze(headers);
106
89
  }
107
90
 
108
91
  #setupWebSocketListeners() {
109
92
  if (!this.#ws) return;
110
- const ws = this.#ws;
111
- ws.once("open", this.#onOpen.bind(this));
112
- ws.once("error", this.#onError.bind(this));
113
- ws.on("message", this.#onMessage.bind(this));
114
- ws.once("close", this.#onClose.bind(this));
93
+
94
+ this.#ws.removeAllListeners('open');
95
+ this.#ws.removeAllListeners('error');
96
+ this.#ws.removeAllListeners('message');
97
+ this.#ws.removeAllListeners('close');
98
+
99
+ this.#ws.once("open", this.#onOpen.bind(this));
100
+ this.#ws.once("error", this.#onError.bind(this));
101
+ this.#ws.on("message", this.#onMessage.bind(this));
102
+ this.#ws.once("close", this.#onClose.bind(this));
115
103
  }
116
104
 
117
105
  async #onOpen() {
@@ -119,25 +107,28 @@ class Node {
119
107
  this.aqua.emit('debug', this.name, `Connected to ${this.wsUrl.href}`);
120
108
  try {
121
109
  this.info = await this.rest.makeRequest("GET", "/v4/info");
122
- this.autoResume && await this.resumePlayers();
110
+ if (this.autoResume) await this.resumePlayers();
123
111
  } catch (err) {
124
112
  this.info = null;
125
- !this.aqua.bypassChecks?.nodeFetchInfo &&
113
+ if (!this.aqua.bypassChecks?.nodeFetchInfo) {
126
114
  this.aqua.emit('error', `Failed to fetch node info: ${err.message}`);
115
+ }
127
116
  }
128
117
  }
129
-
130
118
  async getStats() {
119
+ if (!this.connected) return this.stats;
120
+
131
121
  const now = Date.now();
132
122
  const STATS_COOLDOWN = 60000;
123
+
133
124
  if (now - this.#lastStatsRequest < STATS_COOLDOWN) {
134
- return this.#statsCache[this.name] ?? this.stats;
125
+ return this.stats;
135
126
  }
127
+
136
128
  try {
137
129
  const stats = await this.rest.makeRequest("GET", "/v4/stats");
138
- this.#updateStats(stats);
130
+ this.stats = { ...this.#createStats(), ...stats };
139
131
  this.#lastStatsRequest = now;
140
- this.#statsCache[this.name] = this.stats;
141
132
  return this.stats;
142
133
  } catch (err) {
143
134
  this.aqua.emit('debug', `Stats fetch error: ${err.message}`);
@@ -147,66 +138,58 @@ class Node {
147
138
 
148
139
  #updateStats(payload) {
149
140
  if (!payload) return;
150
- const newStats = {
151
- players: payload.players ?? 0,
152
- playingPlayers: payload.playingPlayers ?? 0,
153
- uptime: payload.uptime ?? 0,
154
- ping: payload.ping ?? 0,
141
+ this.stats = {
142
+ ...this.stats,
143
+ ...payload,
155
144
  memory: this.#updateMemoryStats(payload.memory),
156
145
  cpu: this.#updateCpuStats(payload.cpu),
157
146
  frameStats: this.#updateFrameStats(payload.frameStats)
158
147
  };
159
- this.stats = Object.freeze(newStats);
160
148
  }
161
149
 
162
150
  #updateMemoryStats(memory = {}) {
163
151
  const allocated = memory.allocated ?? 0;
164
152
  const free = memory.free ?? 0;
165
153
  const used = memory.used ?? 0;
166
- return Object.freeze({
154
+ return {
167
155
  free,
168
156
  used,
169
157
  allocated,
170
158
  reservable: memory.reservable ?? 0,
171
159
  freePercentage: allocated ? (free / allocated) * 100 : 0,
172
160
  usedPercentage: allocated ? (used / allocated) * 100 : 0
173
- });
161
+ };
174
162
  }
175
163
 
176
164
  #updateCpuStats(cpu = {}) {
177
165
  const cores = cpu.cores ?? 0;
178
- return Object.freeze({
166
+ return {
179
167
  cores,
180
168
  systemLoad: cpu.systemLoad ?? 0,
181
169
  lavalinkLoad: cpu.lavalinkLoad ?? 0,
182
170
  lavalinkLoadPercentage: cores ? (cpu.lavalinkLoad / cores) * 100 : 0
183
- });
171
+ };
184
172
  }
185
173
 
186
174
  #updateFrameStats(frameStats = {}) {
187
- if (!frameStats) {
188
- return Object.freeze({
189
- sent: 0,
190
- nulled: 0,
191
- deficit: 0
192
- });
193
- }
194
- return Object.freeze({
175
+ if (!frameStats) return {};
176
+ return {
195
177
  sent: frameStats.sent ?? 0,
196
178
  nulled: frameStats.nulled ?? 0,
197
179
  deficit: frameStats.deficit ?? 0
198
- });
180
+ };
199
181
  }
200
182
 
201
183
  #onMessage(msg) {
202
184
  try {
203
185
  const payload = JSON.parse(msg.toString());
204
186
  if (!payload?.op) return;
187
+
205
188
  switch (payload.op) {
206
- case "stats":
189
+ case 'stats':
207
190
  this.#updateStats(payload);
208
191
  break;
209
- case "ready":
192
+ case 'ready':
210
193
  this.#handleReadyOp(payload);
211
194
  break;
212
195
  default:
@@ -242,26 +225,33 @@ class Node {
242
225
 
243
226
  #reconnect() {
244
227
  if (this.infiniteReconnects) {
245
- this.aqua.emit("nodeReconnect", this, console.log("Experimental infinite reconnects enabled, will be trying non-stop..."));
246
- setTimeout(() => {
247
- this.connect();
248
- }, 10000);
228
+ this.aqua.emit("nodeReconnect", this, "Experimental infinite reconnects enabled, will be trying non-stop...");
229
+ setTimeout(() => this.connect(), 10000);
249
230
  return;
250
231
  }
251
232
 
252
- if (this.connected) {
253
- clearTimeout(this.reconnectTimeoutId);
254
- }
255
- if (++this.#reconnectAttempted >= this.reconnectTries) {
233
+ const jitter = Math.random() * 10000;
234
+ const backoffTime = Math.min(
235
+ this.reconnectTimeout * Math.pow(1.5, this.#reconnectAttempted) + jitter,
236
+ 30000
237
+ );
238
+
239
+ if (this.#reconnectAttempted >= this.reconnectTries && !this.infiniteReconnects) {
256
240
  this.aqua.emit("nodeError", this, new Error(`Max reconnection attempts reached (${this.reconnectTries})`));
257
- clearTimeout(this.reconnectTimeoutId);
258
- return this.destroy();
241
+ this.destroy(true);
242
+ return;
259
243
  }
260
- clearTimeout(this.reconnectTimeoutId);
261
- this.reconnectTimeoutId = setTimeout(() => {
262
- this.aqua.emit("nodeReconnect", this, this.#reconnectAttempted);
244
+
245
+ clearTimeout(this.#reconnectTimeoutId);
246
+ this.#reconnectTimeoutId = setTimeout(() => {
247
+ this.#reconnectAttempted++;
248
+ this.aqua.emit("nodeReconnect", {
249
+ nodeName: this.name,
250
+ attempt: this.#reconnectAttempted,
251
+ backoffTime
252
+ });
263
253
  this.connect();
264
- }, this.reconnectTimeout * Math.pow(2, this.#reconnectAttempted)); // Exponential backoff
254
+ }, backoffTime);
265
255
  }
266
256
 
267
257
  get penalties() {
@@ -271,8 +261,7 @@ class Node {
271
261
  penalties += Math.round(Math.pow(1.05, 100 * this.stats.cpu.systemLoad) * 10 - 10);
272
262
  }
273
263
  if (this.stats.frameStats) {
274
- penalties += this.stats.frameStats.deficit;
275
- penalties += this.stats.frameStats.nulled * 2;
264
+ penalties += this.stats.frameStats.deficit + this.stats.frameStats.nulled * 2;
276
265
  }
277
266
  return penalties;
278
267
  }
@@ -295,8 +284,8 @@ class Node {
295
284
  this.aqua.nodeMap.delete(this.name);
296
285
  this.aqua.emit("nodeDestroy", this);
297
286
  this.info = null;
298
- this.#statsCache = {};
299
- this.stats = Object.freeze(this.#createStats());
287
+ this.#statsCache.clear();
288
+ this.stats = this.#createStats();
300
289
  }
301
290
  }
302
291
 
@@ -4,6 +4,11 @@ const { Queue } = require("./Queue");
4
4
  const { Filters } = require("./Filters");
5
5
 
6
6
  class Player extends EventEmitter {
7
+ static LOOP_MODES = Object.freeze({
8
+ NONE: "none",
9
+ TRACK: "track",
10
+ QUEUE: "queue"
11
+ });
7
12
  constructor(aqua, nodes, options = {}) {
8
13
  super();
9
14
  this.aqua = aqua;
@@ -28,25 +33,26 @@ class Player extends EventEmitter {
28
33
  this.ping = 0;
29
34
  this.nowPlayingMessage = null;
30
35
  this.previousTracks = [];
31
- this.shouldDeleteMessage = options.shouldDeleteMessage ?? true;
36
+ this.shouldDeleteMessage = options.shouldDeleteMessage;
37
+ this.leaveOnEnd = options.leaveOnEnd
32
38
 
33
- this.on("playerUpdate", this.onPlayerUpdate.bind(this));
34
- this.on("event", this.handleEvent.bind(this));
39
+ this.boundHandleEvent = this.handleEvent.bind(this);
40
+ this.boundOnPlayerUpdate = this.onPlayerUpdate.bind(this);
41
+ this.on("playerUpdate", this.boundOnPlayerUpdate);
42
+ this.on("event", this.boundHandleEvent);
35
43
  }
36
44
 
37
- onPlayerUpdate(packet) {
38
- if (!packet?.state) return;
39
- const { state } = packet;
45
+ onPlayerUpdate({ state } = {}) {
46
+ if (!state) return;
47
+
40
48
  const { connected, position, ping, time } = state;
41
- this.connected = connected;
42
- this.position = position;
43
- this.ping = ping;
44
- this.timestamp = time;
45
- this.aqua.emit("playerUpdate", this, packet);
49
+ Object.assign(this, { connected, position, ping, timestamp: time });
50
+
51
+ this.aqua.emit("playerUpdate", this, { state });
46
52
  }
47
53
 
48
54
  get previous() {
49
- return this.previousTracks.length ? this.previousTracks[0] : null;
55
+ return this.previousTracks[0] || null;
50
56
  }
51
57
 
52
58
  get currenttrack() {
@@ -60,25 +66,28 @@ class Player extends EventEmitter {
60
66
  this.previousTracks.unshift(track);
61
67
  }
62
68
 
63
-
64
69
  async play() {
65
- if (!this.connected) throw new Error("Player must be connected first.");
70
+ if (!this.connected) {
71
+ throw new Error("Player must be connected first.");
72
+ }
66
73
  if (!this.queue.length) return;
67
-
74
+
68
75
  const track = this.queue.shift();
69
-
70
76
  this.current = track.track ? track : await track.resolve(this.aqua);
71
77
 
72
- this.playing = true;
73
- this.position = 0;
78
+ Object.assign(this, {
79
+ playing: true,
80
+ position: 0
81
+ });
82
+
74
83
  this.aqua.emit("debug", this.guildId, `Playing track: ${this.current.track}`);
75
- this.updatePlayer({ track: { encoded: this.current.track } });
76
- return this;
84
+ return this.updatePlayer({ track: { encoded: this.current.track } });
77
85
  }
78
86
 
79
- connect(options) {
80
- if (this.connected) throw new Error("Player is already connected.");
81
- const { guildId, voiceChannel, deaf = true, mute = false } = options;
87
+ connect({ guildId, voiceChannel, deaf = true, mute = false }) {
88
+ if (this.connected) {
89
+ throw new Error("Player is already connected.");
90
+ }
82
91
  this.send({
83
92
  guild_id: guildId,
84
93
  channel_id: voiceChannel,
@@ -93,7 +102,7 @@ class Player extends EventEmitter {
93
102
  destroy() {
94
103
  if (!this.connected) return this;
95
104
  this.disconnect();
96
- this.nowPlayingMessage?.delete().catch(() => { }); // ignore the error
105
+ this.nowPlayingMessage?.delete().catch(() => { });
97
106
  this.aqua.destroyPlayer(this.guildId);
98
107
  this.nodes.rest.destroyPlayer(this.guildId);
99
108
  return this;
@@ -107,30 +116,29 @@ class Player extends EventEmitter {
107
116
 
108
117
  async searchLyrics(query) {
109
118
  if (!query) return null;
110
-
111
- const response = await this.nodes.rest.getLyrics({
112
- track: {
119
+ const response = await this.nodes.rest.getLyrics({
120
+ track: {
113
121
  encoded: { info: { title: query } },
114
122
  guild_id: this.guildId,
115
123
  search: true
116
- }
124
+ }
117
125
  });
118
-
119
126
  return response || null;
120
127
  }
128
+
121
129
  async lyrics() {
122
130
  if (!this.playing) return null;
123
131
  const response = await this.nodes.rest.getLyrics({
124
132
  track: {
125
- encoded: this.current.track,
126
- guild_id: this.guildId
133
+ encoded: this.current.track,
134
+ guild_id: this.guildId
127
135
  },
128
- });
136
+ });
129
137
  return response || null;
130
138
  }
131
139
 
132
140
  seek(position) {
133
- if (!this.playing) return this;
141
+ if (!this.playing) return this;
134
142
  const newPosition = this.position + position;
135
143
  if (newPosition < 0) {
136
144
  throw new Error("Seek position cannot be negative.");
@@ -155,8 +163,7 @@ class Player extends EventEmitter {
155
163
  return this;
156
164
  }
157
165
 
158
- static validModes = new Set(["none", "track", "queue"]);
159
-
166
+ static validModes = new Set(["none", "track", "queue"]);
160
167
  setLoop(mode) {
161
168
  if (!Player.validModes.has(mode)) throw new Error("Loop mode must be 'none', 'track', or 'queue'.");
162
169
  this.loop = mode;
@@ -192,10 +199,10 @@ class Player extends EventEmitter {
192
199
  }
193
200
 
194
201
  shuffle() {
195
- const len = this.queue.length;
196
- for (let i = len - 1; i > 0; i--) {
197
- const j = Math.floor(Math.random() * (i + 1));
198
- [this.queue[i], this.queue[j]] = [this.queue[j], this.queue[i]];
202
+ const arr = this.queue;
203
+ for (let i = arr.length - 1; i > 0; i--) {
204
+ const j = (Math.random() * (i + 1)) | 0;
205
+ [arr[i], arr[j]] = [arr[j], arr[i]];
199
206
  }
200
207
  return this;
201
208
  }
@@ -213,30 +220,29 @@ class Player extends EventEmitter {
213
220
  return this.playing ? this.play() : undefined;
214
221
  }
215
222
 
216
- static EVENT_HANDLERS = new Map([
217
- ["TrackStartEvent", "trackStart"],
218
- ["TrackEndEvent", "trackEnd"],
219
- ["TrackExceptionEvent", "trackError"],
220
- ["TrackStuckEvent", "trackStuck"],
221
- ["TrackChangeEvent", "trackChange"],
222
- ["WebSocketClosedEvent", "socketClosed"]
223
- ]);
223
+ static EVENT_HANDLERS = Object.freeze({
224
+ TrackStartEvent: "trackStart",
225
+ TrackEndEvent: "trackEnd",
226
+ TrackExceptionEvent: "trackError",
227
+ TrackStuckEvent: "trackStuck",
228
+ TrackChangeEvent: "trackChange",
229
+ WebSocketClosedEvent: "socketClosed"
230
+ });
224
231
 
225
- handleEvent = (payload) => {
232
+ async handleEvent(payload) {
226
233
  const player = this.aqua.players.get(payload.guildId);
227
234
  if (!player) return;
228
- const track = player.current;
229
- const handlerName = Player.EVENT_HANDLERS.get(payload.type);
230
- if (handlerName) {
231
- this[handlerName](player, track, payload);
235
+
236
+ const handler = Player.EVENT_HANDLERS[payload.type];
237
+ if (handler) {
238
+ await this[handler](player, this.current, payload);
232
239
  } else {
233
- this.handleUnknownEvent(player, track, payload);
240
+ this.handleUnknownEvent(payload);
234
241
  }
235
- };
242
+ }
236
243
 
237
244
  trackStart(player, track) {
238
- this.playing = true;
239
- this.paused = false;
245
+ Object.assign(this, { playing: true, paused: false });
240
246
  this.aqua.emit("trackStart", player, track);
241
247
  }
242
248
 
@@ -248,36 +254,37 @@ class Player extends EventEmitter {
248
254
 
249
255
  async trackEnd(player, track, payload) {
250
256
  if (this.shouldDeleteMessage && this.nowPlayingMessage) {
251
- try {
252
- await this.nowPlayingMessage.delete();
253
- } catch (error) {
254
- } finally {
255
- this.nowPlayingMessage = null;
256
- }
257
+ await this.nowPlayingMessage?.delete().catch(() => { });
258
+ this.nowPlayingMessage = null;
257
259
  }
260
+
258
261
  const reason = payload.reason.replace("_", "").toLowerCase();
259
262
  if (reason === "loadfailed" || reason === "cleanup") {
260
- if (player.queue.isEmpty()) {
261
- this.aqua.emit("queueEnd", player);
262
- return;
263
- }
264
- return player.play();
263
+ return player.queue.isEmpty() ?
264
+ this.aqua.emit("queueEnd", player) :
265
+ player.play();
265
266
  }
267
+
266
268
  switch (this.loop) {
267
- case "track":
269
+ case Player.LOOP_MODES.TRACK:
268
270
  this.aqua.emit("trackRepeat", player, track);
269
271
  player.queue.unshift(track);
270
272
  break;
271
- case "queue":
273
+ case Player.LOOP_MODES.QUEUE:
272
274
  this.aqua.emit("queueRepeat", player, track);
273
275
  player.queue.push(track);
274
276
  break;
275
277
  }
278
+
276
279
  if (player.queue.isEmpty()) {
277
280
  this.playing = false;
278
- this.aqua.emit("queueEnd", player);
279
- return this.cleanup();
281
+ if (this.leaveOnEnd) {
282
+ await this.cleanup();
283
+ }
284
+ return this.aqua.emit("queueEnd", player);
280
285
  }
286
+
287
+
281
288
  return player.play();
282
289
  }
283
290
 
@@ -309,9 +316,8 @@ class Player extends EventEmitter {
309
316
  this.aqua.send({ op: 4, d: data });
310
317
  }
311
318
 
312
- #dataStore = new WeakMap();
313
-
314
- set(key, value) {
319
+ #dataStore = new Map();
320
+ set(key, value) {
315
321
  this.#dataStore.set(key, value);
316
322
  }
317
323
 
@@ -320,7 +326,7 @@ class Player extends EventEmitter {
320
326
  }
321
327
 
322
328
  clearData() {
323
- this.#dataStore.delete()
329
+ this.#dataStore.clear();
324
330
  return this;
325
331
  }
326
332
 
@@ -13,22 +13,22 @@ class Track {
13
13
  * @param {Node} nodes
14
14
  */
15
15
  constructor(data, requester, nodes) {
16
- const info = data?.info || {};
16
+ const { info = {}, encoded = null, playlist = null } = data || {};
17
17
 
18
- this.info = {
18
+ this.info = Object.freeze({
19
19
  identifier: info.identifier || '',
20
- isSeekable: !!info.isSeekable,
20
+ isSeekable: Boolean(info.isSeekable),
21
21
  author: info.author || '',
22
- length: ~~info.length,
23
- isStream: !!info.isStream,
22
+ length: info.length | 0,
23
+ isStream: Boolean(info.isStream),
24
24
  title: info.title || '',
25
25
  uri: info.uri || '',
26
26
  sourceName: info.sourceName || '',
27
27
  artworkUrl: info.artworkUrl || ''
28
- };
28
+ });
29
29
 
30
- this.track = data?.encoded || null;
31
- this.playlist = data?.playlist || null;
30
+ this.track = encoded;
31
+ this.playlist = playlist;
32
32
  this.requester = requester;
33
33
  this.nodes = nodes;
34
34
  }
@@ -38,8 +38,9 @@ class Track {
38
38
  * @returns {string|null}
39
39
  */
40
40
  resolveThumbnail(thumbnail) {
41
- if (!thumbnail) return null;
42
- return thumbnail.startsWith("http") ? thumbnail : getImageUrl(thumbnail, this.nodes);
41
+ return thumbnail && thumbnail.startsWith("http") ?
42
+ thumbnail :
43
+ thumbnail ? getImageUrl(thumbnail, this.nodes) : null;
43
44
  }
44
45
 
45
46
  /**
@@ -47,12 +48,14 @@ class Track {
47
48
  * @returns {Promise<Track|null>}
48
49
  */
49
50
  async resolve(aqua) {
50
- if (!aqua?.options?.defaultSearchPlatform) return null;
51
+ const searchPlatform = aqua?.options?.defaultSearchPlatform;
52
+ if (!searchPlatform) return null;
51
53
 
52
54
  try {
55
+ const query = `${this.info.author} - ${this.info.title}`;
53
56
  const result = await aqua.resolve({
54
- query: this.info.author + ' - ' + this.info.title,
55
- source: aqua.options.defaultSearchPlatform,
57
+ query,
58
+ source: searchPlatform,
56
59
  requester: this.requester,
57
60
  node: this.nodes
58
61
  });
@@ -62,7 +65,10 @@ class Track {
62
65
  const track = this._findMatchingTrack(result.tracks);
63
66
  if (!track) return null;
64
67
 
65
- this._updateTrack(track);
68
+ this.info.identifier = track.info.identifier;
69
+ this.track = track.track;
70
+ this.playlist = track.playlist || null;
71
+
66
72
  return this;
67
73
  } catch {
68
74
  return null;
@@ -74,40 +80,24 @@ class Track {
74
80
  */
75
81
  _findMatchingTrack(tracks) {
76
82
  const { author, title, length } = this.info;
77
-
78
- for (let i = 0; i < tracks.length; i++) {
79
- const track = tracks[i];
83
+
84
+ for (const track of tracks) {
80
85
  const tInfo = track.info;
81
86
 
82
- if (tInfo.author === author &&
83
- tInfo.title === title &&
84
- (!length || Math.abs(tInfo.length - length) <= 2000)) {
87
+ if (!author || !title || author !== tInfo.author || title !== tInfo.title) {
88
+ continue;
89
+ }
90
+
91
+ if (!length || Math.abs(tInfo.length - length) <= 2000) {
85
92
  return track;
86
93
  }
87
94
  }
88
95
 
89
96
  return tracks[0];
90
97
  }
91
-
92
- /**
93
- * @private
94
- */
95
- _updateTrack(track) {
96
- this.info.identifier = track.info.identifier;
97
- this.track = track.track;
98
- this.playlist = track.playlist || null;
99
- }
100
-
101
- /**
102
- * Fast cleanup
103
- */
104
98
  destroy() {
105
- this.requester = null;
106
- this.nodes = null;
107
- this.track = null;
108
- this.playlist = null;
109
- this.info = null;
99
+ Object.keys(this).forEach(key => this[key] = null);
110
100
  }
111
101
  }
112
102
 
113
- module.exports = { Track };
103
+ module.exports = { Track };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aqualink",
3
- "version": "1.7.0-beta4",
3
+ "version": "1.7.0-beta6",
4
4
  "description": "An Lavalink wrapper, focused in speed, performance, and features, Based in Riffy!",
5
5
  "main": "build/index.js",
6
6
  "scripts": {
@@ -36,7 +36,7 @@
36
36
  "author": "mushroom0162 (https://github.com/ToddyTheNoobDud)",
37
37
  "license": "ISC",
38
38
  "dependencies": {
39
- "undici": "^7.2.0",
39
+ "undici": "^7.3.0",
40
40
  "ws": "^8.18.0"
41
41
  },
42
42
  "repository": {