aqualink 1.6.2-beta → 1.7.0-beta1

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
@@ -25,41 +25,51 @@ This code is based in riffy, but its an 100% Rewrite made from scratch...
25
25
 
26
26
  - Example bot: https://github.com/ToddyTheNoobDud/Thorium-Music
27
27
 
28
- # Omg version 1.6.0 || 1.6.1 || 1.6.2 woah aqualink
28
+ # Real changelog for 1.7.0-beta1
29
+ Note: Not features are widely tested / Not fully Complete
30
+
31
+ - Reformated the `PLAYER` System (removed Documentation for now)
32
+ - Notable Changes:
33
+ - New WeakMap System (Properly handling, deleting, setting)
34
+ - Around 2x faster (by my tests, taked 0ms to resolve an song)
35
+ - Uses less recourses (reduced by around ~0,5mb, also less cpu instensive)
36
+
37
+ - Fix Some errors in `REST`, Now destroyPlayer, etc, should work as expected.
38
+ - Rewrited out the `TRACK` System
39
+ - Reduced object creation
40
+ - Use direct acess
41
+ - use direct destroy() instead of Object.assing()
42
+ - Separate _findMatchingTrack()
43
+ - Rewrite the search system, Removed useless caching, Improved speed, use traditional for ... of instead of find() - Experimental
44
+
45
+ - Rewrited out the `NODE` System
46
+ - Implement the InfiniteReconnects Option (this will make the code try to connect to an node non-stop.)
47
+ - WeakMap has been replaced with statsCache (experimental)
48
+ - Optimized by using free, used and allocated direct.
49
+ - Backoff in reconnect logic (by using Math)
50
+ - Clear reconnectTimeoutId (prevent memory leaks)
51
+ - Improve the overall speed by a bit
52
+
53
+ - Rewrited the `CONNECTION` System
54
+ - Improved the Connecting, Resolving, Reconnecting Speed (around 1,5x faster now)
55
+ - Improved checking
56
+ - Cached frequently used Code
57
+ - Object.assign Implemented for Batch updates
58
+ - Still in testing, pls report bugs
59
+
60
+ - Some Additions for `AQUA`
61
+ - Implement the InfiniteReconnects Options
62
+ - Re-added our DOCS (Now autocomplete works again!)
63
+ - Add platforms + search system on DefaultPlatform
64
+
65
+ - Rewrited the updateVoiceState System
66
+ - Misc changes to createConnection
67
+ - Document + fix destroyPlayer
68
+
69
+ - Remade some stuff in `FetchImage`
70
+ - Use promise.race since only first sucess is required. (will be tested, may revert to promise.any)
71
+ - Use map cuz its faster and more efficient than Objects.
29
72
 
30
- Version 1.6.2
31
- - Fixed the rejected error (connection, for example using destroy returns an error)
32
- - Added bun support (now it should work with bun too)
33
-
34
- Version 1.6.1:
35
-
36
- - Many fixes related to caching and mapping.
37
- - Rewrited `AQUA` again, Should fix a lot of stuff related to speed, async memory leaks, remade some methods, and others stuff;
38
- - Rewrited `CONNECTION` manager again, should be an better cleanup system less memory leaks, correct updating.
39
- - Fixed various stuff + Various speed improvements, and way less recourses used on `NODE` manager.
40
- - Updated `REST` Manager to use better checkings, speed, and dumping system.
41
- - Rewrote the `TRACK` System, fixed small memory leaks, improved speed by wayyy more
42
- - Some misc changes on player, small optimziations, use Fisher-Yates algorithm, and remove useless asyncs.
43
- - Improved the internal code Garbage collection.
44
- - Use more WeakSet and WeakMaps for memory friendly
45
- - Extra: Also fixed requesting every 1 sec, reducing the requests system and memory usage by a lot i think;
46
- -- Im working on new features, ex: autoplay, lyrics system, and more to come... its hard to me as an solo dev
47
-
48
-
49
- Version 1.6.0:
50
-
51
- - Reworked the `TRACK` Manager (This improves the speed by wayyy more, also uses objects, removed useless code)
52
- - Improved the `REST` Manager (This improves the garbage collector, an faster code, and more optimized)
53
- - Added enqueue to `QUEUE` (this gets the previous, made for dev), removed addMultiple (useless)
54
- - Fully Rewrite the `PLAYER` Manager (Way faster resolving, way less recourse intensive, more responsive, better error handling)
55
-
56
- ^^ Now uses the WeakMap and WeakSet for an garbage collector, making it with an better memory management.
57
-
58
- - Rewrite the `NODE` Manager (reconnect speeds improved, various methods improved, Rewrite the cache and status handler, improve the performance) - Also fixed player resuming.
59
- - Remade some stuff in `CONNECTION` (this improves error handling, cleaning up, and speed)
60
- - Rewrite `AQUA` Manager (remade every single method, improved the resolve, made the code dynamic, fixed lots of bugs, uses weakMap too.) - Added autoResume option (true false)
61
-
62
- - There are way more stuff that i forgot to add on changelog. pls report bugs on my github !
63
73
  # How to install
64
74
 
65
75
  `npm install aqualink`
@@ -1,47 +1,59 @@
1
1
  const { request } = require("undici");
2
2
 
3
- const YOUTUBE_URLS = [
4
- (id) => `https://img.youtube.com/vi/${id}/maxresdefault.jpg`,
5
- (id) => `https://img.youtube.com/vi/${id}/hqdefault.jpg`,
6
- (id) => `https://img.youtube.com/vi/${id}/mqdefault.jpg`,
7
- (id) => `https://img.youtube.com/vi/${id}/default.jpg`,
8
- ];
3
+ const sourceHandlers = new Map([
4
+ ['spotify', (uri) => fetchThumbnail(`https://open.spotify.com/oembed?url=${uri}`)],
5
+ ['youtube', (identifier) => fetchYouTubeThumbnail(identifier)]
6
+ ]);
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);
9
17
 
10
18
  async function getImageUrl(info) {
11
- if (!info || !info.sourceName || !info.uri) return null;
12
-
13
- switch (info.sourceName.toLowerCase()) {
14
- case "spotify":
15
- return await fetchThumbnail(`https://open.spotify.com/oembed?url=${info.uri}`);
16
- case "youtube":
17
- return await fetchYouTubeThumbnail(info.identifier);
18
- default:
19
- return null;
19
+ if (!info?.sourceName || !info?.uri) return null;
20
+
21
+ const handler = sourceHandlers.get(info.sourceName.toLowerCase());
22
+ if (!handler) return null;
23
+
24
+ try {
25
+ return await handler(info.uri);
26
+ } catch {
27
+ return null;
20
28
  }
21
29
  }
22
30
 
23
31
  async function fetchThumbnail(url) {
24
32
  try {
25
- const { body } = await request(url, { method: "GET" });
33
+ const { body } = await request(url, {
34
+ method: "GET",
35
+ headers: {
36
+ 'Accept': 'application/json'
37
+ }
38
+ });
39
+
26
40
  const json = await body.json();
27
- await body.dump();
28
41
  return json.thumbnail_url || null;
29
- } catch (error) {
30
- console.error(`Error fetching ${url}:`, error);
42
+ } catch {
31
43
  return null;
32
44
  }
33
45
  }
34
46
 
35
47
  async function fetchYouTubeThumbnail(identifier) {
36
- const fetchPromises = YOUTUBE_URLS.map(urlFunc => fetchThumbnail(urlFunc(identifier)));
37
- const results = await Promise.allSettled(fetchPromises);
48
+ const fetchPromises = YOUTUBE_QUALITIES.map(urlFunc =>
49
+ fetchThumbnail(urlFunc(identifier))
50
+ );
38
51
 
39
- for (const result of results) {
40
- if (result.status === "fulfilled" && result.value) {
41
- return result.value;
42
- }
52
+ try {
53
+ return await Promise.race(fetchPromises);
54
+ } catch {
55
+ return null;
43
56
  }
44
- return null;
45
57
  }
46
58
 
47
59
  module.exports = { getImageUrl };
@@ -5,6 +5,27 @@ const { Track } = require("./Track");
5
5
  const { version: pkgVersion } = require("../../package.json");
6
6
  const URL_REGEX = /^https?:\/\//;
7
7
  class Aqua extends EventEmitter {
8
+ /**
9
+ * @param {Object} client - The client instance.
10
+ * @param {Array<Object>} nodes - An array of node configurations.
11
+ * @param {Object} options - Configuration options for Aqua.
12
+ * @param {Function} options.send - Function to send data.
13
+ * @param {string} [options.defaultSearchPlatform="ytsearch"] - Default search platform. 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
+ */
8
29
  constructor(client, nodes, options) {
9
30
  super();
10
31
  this.validateInputs(client, nodes, options);
@@ -22,9 +43,11 @@ class Aqua extends EventEmitter {
22
43
  this.options = options;
23
44
  this.send = options.send;
24
45
  this.autoResume = options.autoResume || false;
46
+ this.infiniteReconnects = options.infiniteReconnects || false;
25
47
  this.setMaxListeners(0);
26
48
  }
27
49
 
50
+
28
51
  validateInputs(client, nodes, options) {
29
52
  if (!client) throw new Error("Client is required to initialize Aqua");
30
53
  if (!Array.isArray(nodes) || !nodes.length) throw new Error(`Nodes must be a non-empty Array (Received ${typeof nodes})`);
@@ -78,17 +101,13 @@ class Aqua extends EventEmitter {
78
101
  }
79
102
  }
80
103
 
81
- updateVoiceState(packet) {
82
- if (!packet?.d?.guild_id) return;
83
- const player = this.players.get(packet.d.guild_id);
84
- if (!player) return;
85
- const updateType = packet.t === "VOICE_SERVER_UPDATE" ? "setServerUpdate" : "setStateUpdate";
86
- player.connection[updateType](packet.d);
87
- if (packet.d.status === "disconnected") {
88
- this.cleanupPlayer(player);
104
+ updateVoiceState({ d, t }) {
105
+ const player = this.players.get(d.guild_id);
106
+ if (player && (t === "VOICE_SERVER_UPDATE" || t === "VOICE_STATE_UPDATE" && d.user_id === this.clientId)) {
107
+ player.connection[t === "VOICE_SERVER_UPDATE" ? "setServerUpdate" : "setStateUpdate"](d);
108
+ if (d.status === "disconnected") this.cleanupPlayer(player);
89
109
  }
90
110
  }
91
-
92
111
  fetchRegion(region) {
93
112
  if (!region) return this.leastUsedNodes;
94
113
  const lowerRegion = region.toLowerCase();
@@ -104,8 +123,8 @@ class Aqua extends EventEmitter {
104
123
 
105
124
  createConnection(options) {
106
125
  this.ensureInitialized();
107
- const existingPlayer = this.players.get(options.guildId);
108
- if (existingPlayer?.voiceChannel) return existingPlayer;
126
+ const player = this.players.get(options.guildId);
127
+ if (player && player.voiceChannel) return player;
109
128
  const node = options.region ? this.fetchRegion(options.region)[0] : this.leastUsedNodes[0];
110
129
  if (!node) throw new Error("No nodes are available");
111
130
  return this.createPlayer(node, options);
@@ -121,21 +140,22 @@ class Aqua extends EventEmitter {
121
140
  return player;
122
141
  }
123
142
 
124
- destroyPlayer(guildId) {
125
- const player = this.players.get(guildId);
126
- if (!player) return;
127
- try {
128
- player.clearData();
129
- player.removeAllListeners();
130
- player.destroy();
131
- this.players.delete(guildId);
132
- this.emit("playerDestroy", player);
133
- } catch (error) {
134
- console.error(`Error destroying player for guild ${guildId}:`, error);
135
- }
143
+ async destroyPlayer(guildId) {
144
+ const player = this.players.get(guildId);
145
+ if (!player) return;
146
+
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);
136
155
  }
156
+ }
137
157
 
138
- async resolve({ query, source = this.defaultSearchPlatform, requester, nodes }) {
158
+ async resolve({ query, source = this.defaultSearchPlatform , requester, nodes }) {
139
159
  this.ensureInitialized();
140
160
  const requestNode = this.getRequestNode(nodes);
141
161
  const formattedQuery = this.formatQuery(query, source);
@@ -263,4 +283,4 @@ class Aqua extends EventEmitter {
263
283
  }
264
284
  }
265
285
 
266
- module.exports = { Aqua };
286
+ module.exports = { Aqua };
@@ -1,85 +1,106 @@
1
1
  class Connection {
2
2
  constructor(player) {
3
- this.player = player;
4
- this.voice = { sessionId: null, endpoint: null, token: null };
5
- this.region = null;
6
- this.selfDeaf = false;
7
- this.selfMute = false;
8
- this.voiceChannel = player.voiceChannel;
9
- this.lastUpdateTime = 0;
10
- this.updateThrottle = 1000; // Throttle time in milliseconds
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
+ });
11
14
  }
12
-
13
- setServerUpdate({ endpoint, token }) {
14
- if (!endpoint) throw new Error("Missing 'endpoint' property in VOICE_SERVER_UPDATE");
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, '');
24
+
25
+ if (this.region !== newRegion) {
26
+ const prevRegion = this.region;
15
27
 
16
- const newRegion = endpoint.split('.')[0].replace(/[0-9]/g, "");
17
- if (this.region !== newRegion) {
18
- this.updateRegion(newRegion, endpoint, token);
19
- }
20
- this.updatePlayerVoiceData();
21
- }
22
-
23
- updateRegion(newRegion, endpoint, token) {
24
- const previousVoiceRegion = this.region;
28
+ Object.assign(this.voice, {
29
+ endpoint,
30
+ token: data.token
31
+ });
25
32
  this.region = newRegion;
26
- this.voice.endpoint = endpoint;
27
- this.voice.token = token;
28
-
29
- const message = previousVoiceRegion
30
- ? `Changed Voice Region from ${previousVoiceRegion} to ${this.region}`
31
- : `Voice Server: ${this.region}`;
32
-
33
- this.player.aqua.emit("debug", `[Player ${this.player.guildId} - CONNECTION] ${message}`);
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();
34
45
  }
46
+
35
47
 
36
48
  setStateUpdate(data) {
37
- if (!data.channel_id || !data.session_id) {
38
- this.cleanup();
39
- return;
40
- }
49
+ const channelId = data.channel_id;
50
+ const sessionId = data.session_id;
51
+
52
+ if (!channelId || !sessionId) {
53
+ this._cleanup();
54
+ return;
55
+ }
41
56
 
42
- if (this.voiceChannel !== data.channel_id) {
43
- this.player.aqua.emit("playerMove", this.voiceChannel, data.channel_id);
44
- this.voiceChannel = data.channel_id;
45
- }
46
-
47
- this.selfDeaf = data.self_deaf;
48
- this.selfMute = data.self_mute;
49
- this.voice.sessionId = data.session_id;
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;
50
67
  }
51
-
52
- updatePlayerVoiceData() {
53
- const currentTime = Date.now();
54
- if (currentTime - this.lastUpdateTime >= this.updateThrottle) {
55
- this.lastUpdateTime = currentTime;
56
-
57
- const data = {
58
- voice: this.voice,
59
- volume: this.player.volume,
60
- };
61
-
62
- this.player.nodes.rest.updatePlayer({
63
- guildId: this.player.guildId,
64
- data,
65
- }).catch(err => {
66
- this.player.aqua.emit("apiError", "updatePlayer", err);
67
- });
68
+
69
+ _updatePlayerVoiceData() {
70
+ this._nodes.rest.updatePlayer({
71
+ guildId: this._guildId,
72
+ data: {
73
+ voice: this.voice,
74
+ volume: this.player.volume
68
75
  }
76
+ }).catch(err => {
77
+ this._aqua.emit("apiError", "updatePlayer", err);
78
+ });
69
79
  }
70
-
71
- cleanup() {
72
- this.player.aqua.emit("playerLeave", this.player.voiceChannel);
73
- this.player.voiceChannel = null;
74
- this.voiceChannel = null;
75
- this.player.destroy();
76
- this.player.aqua.emit("playerDestroy", this.player);
77
- this.player = null;
78
- this.voice = null;
79
- this.region = null;
80
- this.selfDeaf = null;
81
- this.selfMute = null;
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
+ });
82
103
  }
83
- }
84
-
85
- module.exports = { Connection };
104
+ }
105
+
106
+ module.exports = { Connection };
@@ -2,24 +2,21 @@ const WebSocket = require("ws");
2
2
  const { Rest } = require("./Rest");
3
3
 
4
4
  class Node {
5
- // Private fields using # for better encapsulation
6
5
  #ws = null;
7
- #statsCache = new WeakMap();
6
+ #statsCache = {};
8
7
  #lastStatsRequest = 0;
9
8
  #reconnectAttempted = 0;
10
-
11
9
  constructor(aqua, nodes, options) {
12
- const {
13
- name,
14
- host = "localhost",
15
- port = 2333,
10
+ const {
11
+ name,
12
+ host = "localhost",
13
+ port = 2333,
16
14
  password = "youshallnotpass",
17
15
  secure = false,
18
16
  sessionId = null,
19
17
  regions = []
20
18
  } = nodes;
21
19
 
22
- // Core properties
23
20
  this.aqua = aqua;
24
21
  this.name = name || host;
25
22
  this.host = host;
@@ -28,23 +25,16 @@ class Node {
28
25
  this.secure = secure;
29
26
  this.sessionId = sessionId;
30
27
  this.regions = regions;
31
-
32
- // Configuration
33
28
  this.wsUrl = new URL(`ws${secure ? 's' : ''}://${host}:${port}/v4/websocket`);
34
29
  this.rest = new Rest(aqua, this);
35
-
36
- // Connection options
37
30
  this.resumeKey = options?.resumeKey ?? null;
38
31
  this.resumeTimeout = options?.resumeTimeout ?? 60;
39
32
  this.autoResume = options?.autoResume ?? false;
40
33
  this.reconnectTimeout = options?.reconnectTimeout ?? 2000;
41
34
  this.reconnectTries = options?.reconnectTries ?? 3;
42
-
43
- // State
35
+ this.infiniteReconnects = options?.infiniteReconnects ?? false;
44
36
  this.connected = false;
45
37
  this.info = null;
46
-
47
- // Initialize stats with frozen objects to prevent modifications
48
38
  this.stats = Object.freeze(this.#createStats());
49
39
  }
50
40
 
@@ -78,16 +68,13 @@ class Node {
78
68
 
79
69
  async connect() {
80
70
  this.#cleanup();
81
-
82
71
  try {
83
72
  this.#ws = new WebSocket(this.wsUrl.href, {
84
73
  headers: this.#constructHeaders(),
85
- perMessageDeflate: false // Disable compression for better performance
74
+ perMessageDeflate: false
86
75
  });
87
-
88
76
  this.#setupWebSocketListeners();
89
77
  this.aqua.emit('debug', this.name, 'Connecting...');
90
-
91
78
  } catch (err) {
92
79
  this.aqua.emit('debug', this.name, `Connection failed: ${err.message}`);
93
80
  this.#reconnect();
@@ -98,9 +85,10 @@ class Node {
98
85
  if (this.#ws) {
99
86
  try {
100
87
  this.#ws.removeAllListeners();
101
- this.#ws.terminate(); // Force close connection
102
- } catch {}
103
- finally {
88
+ this.#ws.terminate();
89
+ } catch (err) {
90
+ this.aqua.emit('debug', `Cleanup error: ${err.message}`);
91
+ } finally {
104
92
  this.#ws = null;
105
93
  }
106
94
  }
@@ -112,18 +100,14 @@ class Node {
112
100
  "User-Id": this.aqua.clientId,
113
101
  "Client-Name": `Aqua/${this.aqua.version}`,
114
102
  };
115
-
116
103
  if (this.sessionId) headers["Session-Id"] = this.sessionId;
117
104
  if (this.resumeKey) headers["Resume-Key"] = this.resumeKey;
118
-
119
105
  return Object.freeze(headers);
120
106
  }
121
107
 
122
108
  #setupWebSocketListeners() {
123
109
  if (!this.#ws) return;
124
-
125
110
  const ws = this.#ws;
126
-
127
111
  ws.once("open", this.#onOpen.bind(this));
128
112
  ws.once("error", this.#onError.bind(this));
129
113
  ws.on("message", this.#onMessage.bind(this));
@@ -133,30 +117,27 @@ class Node {
133
117
  async #onOpen() {
134
118
  this.connected = true;
135
119
  this.aqua.emit('debug', this.name, `Connected to ${this.wsUrl.href}`);
136
-
137
120
  try {
138
121
  this.info = await this.rest.makeRequest("GET", "/v4/info");
139
122
  this.autoResume && await this.resumePlayers();
140
123
  } catch (err) {
141
124
  this.info = null;
142
- !this.aqua.bypassChecks?.nodeFetchInfo &&
125
+ !this.aqua.bypassChecks?.nodeFetchInfo &&
143
126
  this.aqua.emit('error', `Failed to fetch node info: ${err.message}`);
144
127
  }
145
128
  }
146
129
 
147
130
  async getStats() {
148
131
  const now = Date.now();
149
- const STATS_COOLDOWN = 60000; // Update every 1 minute.
150
-
132
+ const STATS_COOLDOWN = 60000;
151
133
  if (now - this.#lastStatsRequest < STATS_COOLDOWN) {
152
- return this.#statsCache.get(this) ?? this.stats;
134
+ return this.#statsCache[this.name] ?? this.stats;
153
135
  }
154
-
155
136
  try {
156
137
  const stats = await this.rest.makeRequest("GET", "/v4/stats");
157
138
  this.#updateStats(stats);
158
139
  this.#lastStatsRequest = now;
159
- this.#statsCache.set(this, this.stats);
140
+ this.#statsCache[this.name] = this.stats;
160
141
  return this.stats;
161
142
  } catch (err) {
162
143
  this.aqua.emit('debug', `Stats fetch error: ${err.message}`);
@@ -166,7 +147,6 @@ class Node {
166
147
 
167
148
  #updateStats(payload) {
168
149
  if (!payload) return;
169
-
170
150
  const newStats = {
171
151
  players: payload.players ?? 0,
172
152
  playingPlayers: payload.playingPlayers ?? 0,
@@ -176,19 +156,20 @@ class Node {
176
156
  cpu: this.#updateCpuStats(payload.cpu),
177
157
  frameStats: this.#updateFrameStats(payload.frameStats)
178
158
  };
179
-
180
159
  this.stats = Object.freeze(newStats);
181
160
  }
182
161
 
183
162
  #updateMemoryStats(memory = {}) {
184
163
  const allocated = memory.allocated ?? 0;
164
+ const free = memory.free ?? 0;
165
+ const used = memory.used ?? 0;
185
166
  return Object.freeze({
186
- free: memory.free ?? 0,
187
- used: memory.used ?? 0,
167
+ free,
168
+ used,
188
169
  allocated,
189
170
  reservable: memory.reservable ?? 0,
190
- freePercentage: allocated ? (memory.free / allocated) * 100 : 0,
191
- usedPercentage: allocated ? (memory.used / allocated) * 100 : 0
171
+ freePercentage: allocated ? (free / allocated) * 100 : 0,
172
+ usedPercentage: allocated ? (used / allocated) * 100 : 0
192
173
  });
193
174
  }
194
175
 
@@ -202,20 +183,25 @@ class Node {
202
183
  });
203
184
  }
204
185
 
205
- #updateFrameStats() {
186
+ #updateFrameStats(frameStats = {}) {
187
+ if (!frameStats) {
188
+ return Object.freeze({
189
+ sent: 0,
190
+ nulled: 0,
191
+ deficit: 0
192
+ });
193
+ }
206
194
  return Object.freeze({
207
- sent: 0,
208
- nulled: 0,
209
- deficit: 0
195
+ sent: frameStats.sent ?? 0,
196
+ nulled: frameStats.nulled ?? 0,
197
+ deficit: frameStats.deficit ?? 0
210
198
  });
211
199
  }
212
200
 
213
- // More optimized message handling
214
201
  #onMessage(msg) {
215
202
  try {
216
203
  const payload = JSON.parse(msg.toString());
217
204
  if (!payload?.op) return;
218
-
219
205
  switch (payload.op) {
220
206
  case "stats":
221
207
  this.#updateStats(payload);
@@ -255,32 +241,33 @@ class Node {
255
241
  }
256
242
 
257
243
  #reconnect() {
244
+ if (this.infiniteReconnects) {
245
+ this.aqua.emit("nodeReconnect", this, console.log("Experimental infinite reconnects enabled, will be trying non-stop..."));
246
+ this.connect();
247
+ return;
248
+ }
258
249
  if (++this.#reconnectAttempted >= this.reconnectTries) {
259
250
  this.aqua.emit("nodeError", this, new Error(`Max reconnection attempts reached (${this.reconnectTries})`));
251
+ clearTimeout(this.reconnectTimeoutId);
260
252
  return this.destroy();
261
253
  }
262
-
263
- setTimeout(() => {
264
- this.aqua.emit("nodeReconnect", this);
254
+ clearTimeout(this.reconnectTimeoutId);
255
+ this.reconnectTimeoutId = setTimeout(() => {
256
+ this.aqua.emit("nodeReconnect", this, this.#reconnectAttempted);
265
257
  this.connect();
266
- }, this.reconnectTimeout * this.#reconnectAttempted); // Exponential backoff
258
+ }, this.reconnectTimeout * Math.pow(2, this.#reconnectAttempted)); // Exponential backoff
267
259
  }
268
260
 
269
- // Performance optimized penalties calculation
270
261
  get penalties() {
271
262
  if (!this.connected) return Number.MAX_SAFE_INTEGER;
272
-
273
263
  let penalties = this.stats.players;
274
-
275
264
  if (this.stats.cpu?.systemLoad) {
276
265
  penalties += Math.round(Math.pow(1.05, 100 * this.stats.cpu.systemLoad) * 10 - 10);
277
266
  }
278
-
279
267
  if (this.stats.frameStats) {
280
268
  penalties += this.stats.frameStats.deficit;
281
269
  penalties += this.stats.frameStats.nulled * 2;
282
270
  }
283
-
284
271
  return penalties;
285
272
  }
286
273
 
@@ -290,8 +277,6 @@ class Node {
290
277
  this.aqua.nodes.delete(this.name);
291
278
  return;
292
279
  }
293
-
294
- // Cleanup all players using this node
295
280
  if (this.connected) {
296
281
  for (const player of this.aqua.players.values()) {
297
282
  if (player.node === this) {
@@ -299,17 +284,14 @@ class Node {
299
284
  }
300
285
  }
301
286
  }
302
-
303
287
  this.#cleanup();
304
288
  this.connected = false;
305
289
  this.aqua.nodeMap.delete(this.name);
306
290
  this.aqua.emit("nodeDestroy", this);
307
-
308
- // Clear references
309
291
  this.info = null;
310
- this.#statsCache = new WeakMap();
292
+ this.#statsCache = {};
311
293
  this.stats = Object.freeze(this.#createStats());
312
294
  }
313
295
  }
314
296
 
315
- module.exports = { Node };
297
+ module.exports = { Node };
@@ -30,11 +30,8 @@ class Player extends EventEmitter {
30
30
  this.previousTracks = [];
31
31
  this.shouldDeleteMessage = options.shouldDeleteMessage ?? true;
32
32
 
33
- this._boundPlayerUpdate = this.onPlayerUpdate.bind(this);
34
- this._boundHandleEvent = this.handleEvent.bind(this);
35
-
36
- this.on("playerUpdate", this._boundPlayerUpdate);
37
- this.on("event", this._boundHandleEvent);
33
+ this.on("playerUpdate", this.onPlayerUpdate.bind(this));
34
+ this.on("event", this.handleEvent.bind(this));
38
35
  }
39
36
 
40
37
  onPlayerUpdate(packet) {
@@ -58,162 +55,96 @@ class Player extends EventEmitter {
58
55
  }
59
56
  this.previousTracks.unshift(track);
60
57
  }
61
- /**
62
- * Play the next track in the queue.
63
- *
64
- * @throws {Error} If the player is not connected.
65
- * @returns {Promise<Player>} The player instance.
66
- */
67
- play() {
58
+
59
+
60
+ async play() {
68
61
  if (!this.connected) throw new Error("Player must be connected first.");
69
62
  if (!this.queue.length) return;
70
-
63
+
71
64
  const track = this.queue.shift();
72
- this.current = track.track ? track : track.resolve(this.aqua);
73
-
65
+
66
+ this.current = track.track ? track : await track.resolve(this.aqua);
67
+
74
68
  this.playing = true;
75
69
  this.position = 0;
76
-
77
70
  this.aqua.emit("debug", this.guildId, `Playing track: ${this.current.track}`);
78
- this.updatePlayer({ track: { encoded: this.current.track } });
71
+ this.updatePlayer({ track: { encoded: this.current.track } });
79
72
  return this;
80
73
  }
81
- /**
82
- * Connects the player to a specified voice channel.
83
- *
84
- * @param {Object} options - Options for connecting the player.
85
- * @param {string} options.guildId - The ID of the guild.
86
- * @param {string} options.voiceChannel - The ID of the voice channel to connect to.
87
- * @param {boolean} [options.deaf=true] - Whether the player should be self-deafened.
88
- * @param {boolean} [options.mute=false] - Whether the player should be self-muted.
89
- * @throws {Error} If the player is already connected.
90
- * @returns {Promise<Player>} The player instance.
91
- */
92
- connect(options) {
93
- if (this.connected) throw new Error("Player is already connected.");
94
-
95
- const {
96
- guildId,
97
- voiceChannel,
98
- deaf = true,
99
- mute = false
100
- } = options;
101
74
 
75
+ connect(options) {
76
+ if (this.connected) throw new Error("Player is already connected.");
77
+ const { guildId, voiceChannel, deaf = true, mute = false } = options;
102
78
  this.send({
103
79
  guild_id: guildId,
104
80
  channel_id: voiceChannel,
105
81
  self_deaf: deaf,
106
82
  self_mute: mute
107
83
  });
108
-
109
84
  this.connected = true;
110
85
  this.aqua.emit("debug", this.guildId, `Player connected to voice channel: ${voiceChannel}.`);
111
86
  return this;
112
87
  }
113
88
 
114
- destroy() {
89
+ destroy() {
115
90
  if (!this.connected) return this;
116
- this.updatePlayer({ track: { encoded: null } });
117
- this.queue.clear();
118
- this.current = null;
119
- this.previousTracks.length = 0;
120
- this.playing = false;
121
- this.position = 0;
122
- this.send({ guild_id: this.guildId, channel_id: null });
123
- this.connected = false;
124
-
125
- this.removeListener("playerUpdate", this._boundPlayerUpdate);
126
- this.removeListener("event", this._boundHandleEvent);
127
-
91
+ this.disconnect();
92
+ this.nowPlayingMessage?.delete().catch(() => { }); // ignore the error
93
+ this.aqua.destroyPlayer(this.guildId);
94
+ this.nodes.rest.destroyPlayer(this.guildId);
128
95
  return this;
129
96
  }
130
- /**
131
- * Pauses or resumes the player.
132
- *
133
- * @param {boolean} paused - If true, the player will be paused; if false, it will resume.
134
- * @returns {Promise<Player>} The player instance.
135
- */
136
97
 
137
- pause(paused) {
98
+ pause(paused) {
138
99
  this.paused = paused;
139
- this.updatePlayer({ paused });
100
+ this.updatePlayer({ paused });
140
101
  return this;
141
102
  }
142
- /**
143
- * Seeks to a position in the currently playing track.
144
- *
145
- * @param {number} position - The position in milliseconds to seek to.
146
- * @throws {Error} If the position is negative.
147
- * @returns {Promise<Player>} The player instance.
148
- */
149
- seek(position) {
103
+
104
+ seek(position) {
150
105
  if (position < 0) throw new Error("Seek position cannot be negative.");
151
106
  if (!this.playing) return this;
152
-
153
107
  this.position = position;
154
- this.updatePlayer({ position });
108
+ this.updatePlayer({ position });
155
109
  return this;
156
110
  }
157
- stop() {
111
+
112
+ stop() {
158
113
  if (!this.playing) return this;
159
- this.updatePlayer({ track: { encoded: null } });
114
+ this.updatePlayer({ track: { encoded: null } });
160
115
  this.playing = false;
161
116
  this.position = 0;
162
117
  return this;
163
118
  }
164
- /**
165
- * Sets the volume of the player.
166
- *
167
- * @param {number} volume - The volume to set, between 0 and 200.
168
- * @throws {Error} If the volume is out of range.
169
- * @returns {Promise<Player>} The player instance.
170
- */
171
- setVolume(volume) {
119
+
120
+ setVolume(volume) {
172
121
  if (volume < 0 || volume > 200) throw new Error("Volume must be between 0 and 200.");
173
122
  this.volume = volume;
174
- this.updatePlayer({ volume });
123
+ this.updatePlayer({ volume });
175
124
  return this;
176
125
  }
177
- /**
178
- * Sets the loop mode of the player.
179
- *
180
- * @param {string} mode - The loop mode to set, either "none", "track", or "queue".
181
- * @throws {Error} If the mode is not one of the above.
182
- * @returns {Promise<Player>} The player instance.
183
- */
184
- setLoop(mode) {
185
- const validModes = new Set(["none", "track", "queue"]);
186
- if (!validModes.has(mode)) throw new Error("Loop mode must be 'none', 'track', or 'queue'.");
126
+
127
+ static validModes = new Set(["none", "track", "queue"]);
128
+
129
+ setLoop(mode) {
130
+ if (!Player.validModes.has(mode)) throw new Error("Loop mode must be 'none', 'track', or 'queue'.");
187
131
  this.loop = mode;
188
- this.updatePlayer({ loop: mode });
132
+ this.updatePlayer({ loop: mode });
189
133
  return this;
190
134
  }
191
- /**
192
- * Sets the text channel for the player.
193
- *
194
- * @param {string} channel - The ID of the text channel to set.
195
- * @returns {Promise<Player>} The player instance.
196
- */
197
135
 
198
- setTextChannel(channel) {
199
- this.updatePlayer({ text_channel: channel });
136
+ setTextChannel(channel) {
137
+ this.updatePlayer({ text_channel: channel });
200
138
  return this;
201
139
  }
202
- /**
203
- * Sets the voice channel for the player.
204
- *
205
- * @param {string} channel - The ID of the voice channel to set.
206
- * @throws {TypeError} If the channel is not a non-empty string.
207
- * @throws {ReferenceError} If the player is already connected to the channel.
208
- * @returns {Promise<Player>} The player instance.
209
- */
210
- setVoiceChannel(channel) {
140
+
141
+ setVoiceChannel(channel) {
211
142
  if (!channel?.length) throw new TypeError("Channel must be a non-empty string.");
212
143
  if (this.connected && channel === this.voiceChannel) {
213
144
  throw new ReferenceError(`Player already connected to ${channel}.`);
214
145
  }
215
146
  this.voiceChannel = channel;
216
- this.connect({
147
+ this.connect({
217
148
  deaf: this.deaf,
218
149
  guildId: this.guildId,
219
150
  voiceChannel: channel,
@@ -222,14 +153,14 @@ class Player extends EventEmitter {
222
153
  return this;
223
154
  }
224
155
 
225
- disconnect() {
226
- this.updatePlayer({ track: { encoded: null } });
156
+ disconnect() {
157
+ this.updatePlayer({ track: { encoded: null } });
227
158
  this.connected = false;
228
159
  this.send({ guild_id: this.guildId, channel_id: null });
229
160
  this.aqua.emit("debug", this.guildId, "Player disconnected.");
230
161
  }
231
162
 
232
- shuffle() {
163
+ shuffle() {
233
164
  const len = this.queue.length;
234
165
  for (let i = len - 1; i > 0; i--) {
235
166
  const j = Math.floor(Math.random() * (i + 1));
@@ -242,12 +173,12 @@ class Player extends EventEmitter {
242
173
  return this.queue;
243
174
  }
244
175
 
245
- replay() {
176
+ replay() {
246
177
  return this.seek(0);
247
178
  }
248
179
 
249
- skip() {
250
- this.stop();
180
+ skip() {
181
+ this.stop();
251
182
  return this.playing ? this.play() : undefined;
252
183
  }
253
184
 
@@ -263,16 +194,14 @@ class Player extends EventEmitter {
263
194
  handleEvent = (payload) => {
264
195
  const player = this.aqua.players.get(payload.guildId);
265
196
  if (!player) return;
266
-
267
197
  const track = player.current;
268
198
  const handlerName = Player.EVENT_HANDLERS.get(payload.type);
269
-
270
199
  if (handlerName) {
271
200
  this[handlerName](player, track, payload);
272
201
  } else {
273
202
  this.handleUnknownEvent(player, track, payload);
274
203
  }
275
- }
204
+ };
276
205
 
277
206
  trackStart(player, track) {
278
207
  this.playing = true;
@@ -290,15 +219,13 @@ class Player extends EventEmitter {
290
219
  if (this.shouldDeleteMessage && this.nowPlayingMessage) {
291
220
  try {
292
221
  await this.nowPlayingMessage.delete();
293
- } catch {
294
- // Ignore errors
222
+ } catch (error) {
223
+ // Consider logging specific errors
295
224
  } finally {
296
225
  this.nowPlayingMessage = null;
297
226
  }
298
227
  }
299
-
300
228
  const reason = payload.reason.replace("_", "").toLowerCase();
301
-
302
229
  if (reason === "loadfailed" || reason === "cleanup") {
303
230
  if (player.queue.isEmpty()) {
304
231
  this.aqua.emit("queueEnd", player);
@@ -306,7 +233,6 @@ class Player extends EventEmitter {
306
233
  }
307
234
  return player.play();
308
235
  }
309
-
310
236
  switch (this.loop) {
311
237
  case "track":
312
238
  this.aqua.emit("trackRepeat", player, track);
@@ -317,13 +243,11 @@ class Player extends EventEmitter {
317
243
  player.queue.push(track);
318
244
  break;
319
245
  }
320
-
321
246
  if (player.queue.isEmpty()) {
322
247
  this.playing = false;
323
248
  this.aqua.emit("queueEnd", player);
324
249
  return this.cleanup();
325
250
  }
326
-
327
251
  return player.play();
328
252
  }
329
253
 
@@ -366,7 +290,7 @@ class Player extends EventEmitter {
366
290
  }
367
291
 
368
292
  clearData() {
369
- this.#dataStore = new WeakMap();
293
+ this.#dataStore.delete()
370
294
  return this;
371
295
  }
372
296
 
@@ -384,10 +308,10 @@ class Player extends EventEmitter {
384
308
 
385
309
  async cleanup() {
386
310
  if (!this.playing && !this.paused && this.queue.isEmpty()) {
387
- this.destroy();
311
+ this.destroy();
388
312
  }
389
313
  this.clearData();
390
314
  }
391
315
  }
392
316
 
393
- module.exports = { Player };
317
+ module.exports = { Player };
@@ -29,6 +29,7 @@ class Rest {
29
29
  }
30
30
 
31
31
  const response = await request(`${this.url}${endpoint}`, options);
32
+ if (response.statusCode === 204) return null;
32
33
  this.calls++;
33
34
  const data = await response.body.json();
34
35
  this.aqua.emit("apiResponse", endpoint, {
@@ -5,29 +5,32 @@ const { getImageUrl } = require("../handlers/fetchImage");
5
5
  * @typedef {import("../structures/Player")} Player
6
6
  * @typedef {import("../structures/Node")} Node
7
7
  */
8
+
8
9
  class Track {
9
10
  /**
10
- * @param {{ encoded: string, info: { identifier: string, isSeekable: boolean, author: string, length: number, isStream: boolean, position: number, title: string, uri: string, sourceName: string, artworkUrl: string, track: string }, playlist?: { name: string, selectedTrack: number } }} data
11
+ * @param {Object} data
11
12
  * @param {Player} requester
12
13
  * @param {Node} nodes
13
14
  */
14
15
  constructor(data, requester, nodes) {
15
- const { encoded = null, info = {}, playlist = null } = data;
16
- this.info = Object.freeze({
17
- identifier: info.identifier,
18
- isSeekable: info.isSeekable,
19
- author: info.author,
20
- length: info.length,
21
- isStream: info.isStream,
22
- title: info.title,
23
- uri: info.uri,
24
- sourceName: info.sourceName,
25
- artworkUrl: info.artworkUrl
26
- });
16
+ const info = data?.info || {};
17
+
18
+ this.info = {
19
+ identifier: info.identifier || '',
20
+ isSeekable: !!info.isSeekable,
21
+ author: info.author || '',
22
+ length: ~~info.length,
23
+ isStream: !!info.isStream,
24
+ title: info.title || '',
25
+ uri: info.uri || '',
26
+ sourceName: info.sourceName || '',
27
+ artworkUrl: info.artworkUrl || ''
28
+ };
29
+
30
+ this.track = data?.encoded || null;
31
+ this.playlist = data?.playlist || null;
27
32
  this.requester = requester;
28
33
  this.nodes = nodes;
29
- this.track = encoded;
30
- this.playlist = playlist;
31
34
  }
32
35
 
33
36
  /**
@@ -39,13 +42,16 @@ class Track {
39
42
  return thumbnail.startsWith("http") ? thumbnail : getImageUrl(thumbnail, this.nodes);
40
43
  }
41
44
 
45
+ /**
46
+ * @param {Aqua} aqua
47
+ * @returns {Promise<Track|null>}
48
+ */
42
49
  async resolve(aqua) {
43
50
  if (!aqua?.options?.defaultSearchPlatform) return null;
44
51
 
45
52
  try {
46
- const query = `${this.info.author} - ${this.info.title}`;
47
53
  const result = await aqua.resolve({
48
- query,
54
+ query: this.info.author + ' - ' + this.info.title,
49
55
  source: aqua.options.defaultSearchPlatform,
50
56
  requester: this.requester,
51
57
  node: this.nodes
@@ -53,52 +59,54 @@ class Track {
53
59
 
54
60
  if (!result?.tracks?.length) return null;
55
61
 
56
- const matchedTrack = result.tracks.find(track => this.isTrackMatch(track)) || result.tracks[0];
57
-
58
- if (matchedTrack) {
59
- this.updateTrackInfo(matchedTrack);
60
- return this;
61
- }
62
+ const track = this._findMatchingTrack(result.tracks);
63
+ if (!track) return null;
62
64
 
63
- return null;
64
-
65
- } catch (error) {
66
- console.error('Error resolving track:', error);
65
+ this._updateTrack(track);
66
+ return this;
67
+ } catch {
67
68
  return null;
68
69
  }
69
70
  }
70
71
 
71
- isTrackMatch(track) {
72
+ /**
73
+ * @private
74
+ */
75
+ _findMatchingTrack(tracks) {
72
76
  const { author, title, length } = this.info;
73
- const { author: tAuthor, title: tTitle, length: tLength } = track.info;
74
77
 
75
- return tAuthor === author &&
76
- tTitle === title &&
77
- (!length || Math.abs(tLength - length) <= 2000);
78
+ for (let i = 0; i < tracks.length; i++) {
79
+ const track = tracks[i];
80
+ const tInfo = track.info;
81
+
82
+ if (tInfo.author === author &&
83
+ tInfo.title === title &&
84
+ (!length || Math.abs(tInfo.length - length) <= 2000)) {
85
+ return track;
86
+ }
87
+ }
88
+
89
+ return tracks[0];
78
90
  }
79
91
 
80
92
  /**
81
- * @param {Track} track
93
+ * @private
82
94
  */
83
- updateTrackInfo(track) {
84
- if (!track) return;
85
- this.info = Object.freeze({
86
- ...this.info,
87
- identifier: track.info.identifier
88
- });
89
-
95
+ _updateTrack(track) {
96
+ this.info.identifier = track.info.identifier;
90
97
  this.track = track.track;
91
98
  this.playlist = track.playlist || null;
92
99
  }
93
100
 
94
101
  /**
95
- * Cleanup method to help garbage collection
102
+ * Fast cleanup
96
103
  */
97
104
  destroy() {
98
105
  this.requester = null;
99
106
  this.nodes = null;
100
107
  this.track = null;
101
108
  this.playlist = null;
109
+ this.info = null;
102
110
  }
103
111
  }
104
112
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aqualink",
3
- "version": "1.6.2-beta",
3
+ "version": "1.7.0-beta1",
4
4
  "description": "An Lavalink wrapper, focused in speed, performance, and features, Based in Riffy!",
5
5
  "main": "build/index.js",
6
6
  "scripts": {
@@ -48,8 +48,5 @@
48
48
  "name": "mushroom0162",
49
49
  "url": "https://github.com/ToddyTheNoobDud"
50
50
  }
51
- ],
52
- "devDependencies": {
53
- "discord.js": "^14.16.3"
54
- }
51
+ ]
55
52
  }