aqualink 1.0.5 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -20,6 +20,19 @@ This code is based in riffy, but its an 100% Rewrite made from scratch...
20
20
  # Docs (Wiki)
21
21
  - https://github.com/ToddyTheNoobDud/AquaLink/wiki
22
22
 
23
+ - Example bot: https://github.com/ToddyTheNoobDud/Thorium-Music
24
+
25
+ # Yay, Version 1.2.0 is released ! aqualink so cool
26
+
27
+ + Fixed all the loop system
28
+ + Fixed could not play after ending (weird ahh bug)
29
+ + Improved the general queue handler
30
+ + Remade the REST system (improved speed, ram, and removed useless queue handling)
31
+ + Updated player stuff (Fixed bugs, Added methods: shuffle, getQueue(), restart)
32
+ + Updated the NODE system (misc improvements for speed)
33
+ + Rewrited Aqua system (Fixed playlists support (finnaly), rewrited resolve(), Improved all the code, auto cleanup)
34
+ + Remade the connection system for handling
35
+
23
36
  # How to install
24
37
 
25
38
  `npm install aqualink`
@@ -13,6 +13,7 @@ class Aqua extends EventEmitter {
13
13
  * @param {string} [options.defaultSearchPlatform="ytsearch"] - Default search platform.
14
14
  * @param {string} [options.restVersion="v4"] - Version of the REST API.
15
15
  * @param {Array<Object>} [options.plugins=[]] - Plugins to load.
16
+ * @param {string} [options.shouldDeleteMessage='none'] - Should delete your message? (true, false)
16
17
  */
17
18
  constructor(client, nodes, options) {
18
19
  super();
@@ -27,6 +28,7 @@ class Aqua extends EventEmitter {
27
28
  this.clientId = null;
28
29
  this.initiated = false;
29
30
  this.sessionId = null;
31
+ this.shouldDeleteMessage = options.shouldDeleteMessage || "false";
30
32
  this.defaultSearchPlatform = options.defaultSearchPlatform || "ytmsearch";
31
33
  this.restVersion = options.restVersion || "v3";
32
34
  this.plugins = options.plugins || [];
@@ -51,7 +53,7 @@ class Aqua extends EventEmitter {
51
53
 
52
54
  /**
53
55
  * Initializes Aqua with the provided client ID.
54
- * @param {string} clientId - The client ID.
56
+ * @param {string} clientId - The client ID
55
57
  * @returns {Aqua} The Aqua instance.
56
58
  */
57
59
  init(clientId) {
@@ -98,7 +100,7 @@ class Aqua extends EventEmitter {
98
100
  if (packet.t === "VOICE_SERVER_UPDATE") player.connection.setServerUpdate(packet.d);
99
101
  else if (packet.t === "VOICE_STATE_UPDATE" && packet.d.user_id === this.clientId) player.connection.setStateUpdate(packet.d);
100
102
  }
101
-
103
+
102
104
  /**
103
105
  * Fetches nodes by the specified region.
104
106
  * @param {string} region - The region to filter nodes by.
@@ -155,6 +157,7 @@ class Aqua extends EventEmitter {
155
157
  */
156
158
  destroyPlayer(guildId) {
157
159
  const player = this.players.get(guildId);
160
+ player.clearData();
158
161
  if (!player) return;
159
162
  player.destroy();
160
163
  this.players.delete(guildId);
@@ -167,6 +170,7 @@ class Aqua extends EventEmitter {
167
170
  */
168
171
  removeConnection(guildId) {
169
172
  const player = this.players.get(guildId);
173
+ player.clearData();
170
174
  if (player) {
171
175
  player.destroy();
172
176
  this.players.delete(guildId);
@@ -200,8 +204,7 @@ class Aqua extends EventEmitter {
200
204
  response = await this.handleNoMatches(requestNode.rest, query);
201
205
  }
202
206
 
203
- this.loadTracks(response, requester, requestNode);
204
- return this.constructResponse();
207
+ return this.constructorResponse(response, requester, requestNode);
205
208
  }
206
209
 
207
210
  /**
@@ -223,37 +226,52 @@ class Aqua extends EventEmitter {
223
226
  * @param {Object} response - The response from the track resolution.
224
227
  * @param {Object} requester - The requester of the tracks.
225
228
  * @param {Node} requestNode - The node that handled the request.
229
+ * @returns {Object} The constructed response.
226
230
  */
227
- loadTracks(response, requester, requestNode) {
228
- this.tracks = [];
229
- if (response.loadType === "track") {
230
- if (response.data) {
231
- this.tracks.push(new Track(response.data, requester, requestNode));
232
- }
233
- } else if (response.loadType === "playlist") {
234
- this.tracks = response.data?.tracks.map(track => new Track(track, requester, requestNode)) || [];
235
- this.playlistInfo = response.data?.info || null;
236
- } else if (response.loadType === "search") {
237
- this.tracks = response.data.map(track => new Track(track, requester, requestNode));
231
+ constructorResponse(response, requester, requestNode) {
232
+ switch (response.loadType) {
233
+ case "track":
234
+ if (response.data) {
235
+ return {
236
+ loadType: response.loadType,
237
+ exception: null,
238
+ playlistInfo: null,
239
+ pluginInfo: response.pluginInfo || {},
240
+ tracks: [new Track(response.data, requester, requestNode)],
241
+ };
242
+ }
243
+ break;
244
+ case "playlist":
245
+ return {
246
+ loadType: response.loadType,
247
+ exception: null,
248
+ playlistInfo: {
249
+ name: response.data?.info?.name || response.data?.info?.title,
250
+ ...response.data?.info,
251
+ },
252
+ pluginInfo: response.pluginInfo || {},
253
+ tracks: response.data?.tracks?.map(track => new Track(track, requester, requestNode)) || [],
254
+ };
255
+ case "search":
256
+ return {
257
+ loadType: response.loadType,
258
+ exception: null,
259
+ playlistInfo: null,
260
+ pluginInfo: response.pluginInfo || {},
261
+ tracks: response.data?.map(track => new Track(track, requester, requestNode)),
262
+ };
238
263
  }
239
- this.loadType = response.loadType;
240
- this.pluginInfo = response.pluginInfo || {};
241
- }
242
264
 
243
- /**
244
- * Constructs the response object for the resolved tracks.
245
- * @returns {Object} The constructed response.
246
- */
247
- constructResponse() {
248
265
  return {
249
- loadType: this.loadType,
250
- exception: this.loadType === "error" ? this.loadType.data : (this.loadType === "LOAD_FAILED" ? this.loadType.exception : null),
251
- playlistInfo: this.playlistInfo,
252
- pluginInfo: this.pluginInfo,
253
- tracks: this.tracks.length ? [this.tracks.shift()] : [],
266
+ loadType: response.loadType,
267
+ exception: response.loadType === "error" ? response.loadType.data : (response.loadType === "LOAD_FAILED" ? response.loadType.exception : null),
268
+ playlistInfo: null,
269
+ pluginInfo: response.pluginInfo || {},
270
+ tracks: [],
254
271
  };
255
272
  }
256
273
 
274
+
257
275
  /**
258
276
  * Gets the player associated with the specified guild ID.
259
277
  * @param {string} guildId - The ID of the guild.
@@ -280,4 +298,4 @@ class Aqua extends EventEmitter {
280
298
  }
281
299
  }
282
300
 
283
- module.exports = { Aqua };
301
+ module.exports = { Aqua };
@@ -1,7 +1,3 @@
1
- /**
2
- * Class representing a player connection.
3
- * @param {Player} player The player instance of this connection.
4
- */
5
1
  class Connection {
6
2
  constructor(player) {
7
3
  this.player = player;
@@ -10,42 +6,27 @@ class Connection {
10
6
  this.selfDeaf = false;
11
7
  this.selfMute = false;
12
8
  this.voiceChannel = player.voiceChannel;
9
+ this.lastUpdateTime = 0;
10
+ this.updateThrottle = 1000;
13
11
  }
14
12
 
15
- /**
16
- * Sets the server update data (endpoint and token) for the player.
17
- * @param {object} data The server update data from the VOICE_SERVER_UPDATE packet.
18
- * @param {string} data.endpoint The endpoint URL of the voice server.
19
- * @param {string} data.token The token for the voice server.
20
- */
21
13
  setServerUpdate({ endpoint, token }) {
22
- if (!endpoint) {
23
- throw new Error("Missing 'endpoint' property in VOICE_SERVER_UPDATE packet/payload. Please wait or disconnect the bot from the voice channel and try again.");
24
- }
14
+ if (!endpoint) throw new Error("Missing 'endpoint' property in VOICE_SERVER_UPDATE");
25
15
 
26
16
  const previousVoiceRegion = this.region;
27
17
  this.voice.endpoint = endpoint;
28
18
  this.voice.token = token;
29
19
  this.region = endpoint.split(".")[0].replace(/[0-9]/g, "");
20
+
30
21
  this.player.aqua.emit("debug", `[Player ${this.player.guildId} - CONNECTION] ${previousVoiceRegion ? `Changed Voice Region from ${previousVoiceRegion} to ${this.region}` : `Voice Server: ${this.region}`}`);
31
22
 
32
- if (this.player.paused) {
33
- this.player.pause(false);
34
- }
23
+ if (this.player.paused) this.player.pause(false);
35
24
 
36
25
  this.updatePlayerVoiceData();
37
26
  }
38
27
 
39
- /**
40
- * Sets the state update data (session_id, channel_id, self_deaf, and self_mute) for the player.
41
- * @param {object} data The state update data from the VOICE_STATE_UPDATE packet.
42
- * @param {string} data.session_id The session id of the voice server.
43
- * @param {string} data.channel_id The voice channel id of the player.
44
- * @param {boolean} data.self_deaf The self-deafened status of the player.
45
- * @param {boolean} data.self_mute The self-muted status of the player.
46
- */
47
28
  setStateUpdate({ session_id, channel_id, self_deaf, self_mute }) {
48
- if (channel_id == null || session_id == null) {
29
+ if (!channel_id || !session_id) {
49
30
  this.player.aqua.emit("playerLeave", this.player.voiceChannel);
50
31
  this.player.voiceChannel = null;
51
32
  this.voiceChannel = null;
@@ -67,9 +48,6 @@ class Connection {
67
48
  this.updatePlayerVoiceData();
68
49
  }
69
50
 
70
- /**
71
- * Updates the player voice data.
72
- */
73
51
  updatePlayerVoiceData() {
74
52
  this.player.nodes.rest.updatePlayer({
75
53
  guildId: this.player.guildId,
@@ -1,4 +1,3 @@
1
-
2
1
  const WebSocket = require("ws");
3
2
  const { Rest } = require("./Rest");
4
3
 
@@ -19,22 +18,18 @@ class Node {
19
18
  this.secure = nodes.secure || false;
20
19
  this.sessionId = nodes.sessionId || null;
21
20
  this.rest = new Rest(aqua, this);
22
-
23
21
  this.wsUrl = `ws${this.secure ? 's' : ''}://${this.host}:${this.port}/v4/websocket`;
24
- this.restUrl = `http${this.secure ? 's' : ''}://${this.host}:${this.port}`;
25
-
26
22
  this.ws = null;
27
23
  this.regions = nodes.regions || [];
28
24
  this.info = null;
29
25
  this.connected = false;
30
-
31
26
  this.resumeKey = options.resumeKey || null;
32
27
  this.resumeTimeout = options.resumeTimeout || 60;
33
28
  this.autoResume = options.autoResume || false;
34
-
35
29
  this.reconnectTimeout = options.reconnectTimeout || 5000;
36
30
  this.reconnectTries = options.reconnectTries || 3;
37
31
  this.reconnectAttempted = 0;
32
+ this.lastStatsRequest = 0; // Track the last time stats were requested
38
33
  }
39
34
 
40
35
  initializeStats() {
@@ -61,22 +56,14 @@ class Node {
61
56
  };
62
57
  }
63
58
 
64
- /**
65
- * Fetches the lavalink node's information.
66
- * @param {Object} [options] Options to pass to the rest request.
67
- * @param {boolean} [options.includeHeaders=false] Include headers in the response.
68
- * @returns {Promise<Object>} The lavalink node's information.
69
- */
70
59
  async fetchInfo(options = {}) {
71
60
  return await this.rest.makeRequest("GET", `/v4/info`, null, options.includeHeaders);
72
61
  }
73
-
74
62
 
75
63
  async connect() {
76
64
  if (this.ws) this.ws.close();
77
65
  this.aqua.emit('debug', this.name, `Attempting to connect...`);
78
- const headers = this.constructHeaders();
79
- this.ws = new WebSocket(this.wsUrl, { headers });
66
+ this.ws = new WebSocket(this.wsUrl, { headers: this.constructHeaders() });
80
67
  this.setupWebSocketListeners();
81
68
  }
82
69
 
@@ -100,12 +87,12 @@ class Node {
100
87
  async onOpen() {
101
88
  this.connected = true;
102
89
  this.aqua.emit('debug', this.name, `Connected to Lavalink at ${this.wsUrl}`);
103
-
104
- this.info = await this.fetchInfo()
105
- .catch(err => {
106
- this.aqua.emit('debug', `Failed to fetch info: ${err.message}`);
107
- return null;
108
- });
90
+ try {
91
+ this.info = await this.fetchInfo();
92
+ } catch (err) {
93
+ this.aqua.emit('debug', `Failed to fetch info: ${err.message}`);
94
+ this.info = null;
95
+ }
109
96
 
110
97
  if (!this.info && !this.aqua.bypassChecks.nodeFetchInfo) {
111
98
  throw new Error(`Failed to fetch node info.`);
@@ -114,30 +101,27 @@ class Node {
114
101
  if (this.autoResume) {
115
102
  this.resumePlayers();
116
103
  }
117
-
118
104
  this.lastStats = 0;
119
105
  }
120
106
 
121
107
  async getStats() {
122
- if (Date.now() - this.lastStats < 5000) {
123
- return this.stats;
108
+ const now = Date.now();
109
+ if (now - this.lastStatsRequest < 5000) {
110
+ return this.stats; // Return cached stats if requested too soon
124
111
  }
125
-
126
-
127
- const stats = await this.rest.makeRequest("GET", `/v4/stats`)
128
- .catch(err => {
129
- this.aqua.emit('debug', `Error fetching stats: ${err.message}`);
130
- return null;
131
- });
132
-
133
- if (stats) {
112
+
113
+ try {
114
+ const response = await this.rest.makeRequest("GET", `/v4/stats`);
115
+ const stats = await response.json();
134
116
  this.stats = { ...this.stats, ...stats };
135
- this.lastStats = Date.now();
117
+ this.lastStatsRequest = now; // Update last request time
118
+ return stats;
119
+ } catch (err) {
120
+ this.aqua.emit('debug', `Error fetching stats: ${err.message}`);
121
+ return this.stats; // Return last known stats on error
136
122
  }
137
- return stats;
138
123
  }
139
124
 
140
-
141
125
  resumePlayers() {
142
126
  for (const player of this.aqua.players.values()) {
143
127
  if (player.node === this) {
@@ -153,13 +137,10 @@ class Node {
153
137
  onMessage(msg) {
154
138
  if (Array.isArray(msg)) msg = Buffer.concat(msg);
155
139
  if (msg instanceof ArrayBuffer) msg = Buffer.from(msg);
156
-
157
140
  const payload = JSON.parse(msg.toString());
158
141
  if (!payload.op) return;
159
-
160
142
  this.aqua.emit("raw", "Node", payload);
161
143
  this.aqua.emit("debug", this.name, `Received update: ${JSON.stringify(payload)}`);
162
-
163
144
  this.handlePayload(payload);
164
145
  }
165
146
 
@@ -175,9 +156,7 @@ class Node {
175
156
  break;
176
157
  default:
177
158
  const player = this.aqua.players.get(payload.guildId);
178
- if (payload.guildId && player) {
179
- player.emit(payload.op, payload);
180
- }
159
+ if (player) player.emit(payload.op, payload);
181
160
  break;
182
161
  }
183
162
  }
@@ -196,8 +175,7 @@ class Node {
196
175
  }
197
176
 
198
177
  reconnect() {
199
- this.reconnectAttempted++;
200
- if (this.reconnectAttempted > this.reconnectTries) {
178
+ if (this.reconnectAttempted++ >= this.reconnectTries) {
201
179
  this.aqua.emit("nodeError", this, new Error(`Unable to connect after ${this.reconnectTries} attempts.`));
202
180
  return this.destroy();
203
181
  }
@@ -216,17 +194,13 @@ class Node {
216
194
  this.aqua.nodes.delete(this.name);
217
195
  return;
218
196
  }
219
-
220
197
  if (!this.connected) return;
221
-
222
198
  this.aqua.players.forEach((player) => {
223
199
  if (player.node === this) player.destroy();
224
200
  });
225
-
226
- if (this.ws) this.ws.close(1000, "destroy");
201
+ this.ws?.close(1000, "destroy");
227
202
  this.ws?.removeAllListeners();
228
203
  this.ws = null;
229
-
230
204
  this.aqua.emit("nodeDestroy", this);
231
205
  this.aqua.nodeMap.delete(this.name);
232
206
  this.connected = false;
@@ -255,4 +229,4 @@ class Node {
255
229
  }
256
230
  }
257
231
 
258
- module.exports = { Node };
232
+ module.exports = { Node };
@@ -16,7 +16,7 @@ class Player extends EventEmitter {
16
16
  * @param {number} [options.defaultVolume=100] - The default volume level (0-200).
17
17
  * @param {string} [options.loop='none'] - The loop mode ('none', 'track', 'queue').
18
18
  */
19
- constructor(aqua, nodes, options) {
19
+ constructor(aqua, nodes, options = {}) {
20
20
  super();
21
21
  this.aqua = aqua;
22
22
  this.nodes = nodes;
@@ -33,13 +33,16 @@ class Player extends EventEmitter {
33
33
  this.queue = new Queue();
34
34
  this.position = 0;
35
35
  this.current = null;
36
- this.previousTracks = [];
37
36
  this.playing = false;
38
37
  this.paused = false;
39
38
  this.connected = false;
40
39
  this.timestamp = 0;
41
40
  this.ping = 0;
42
41
  this.isAutoplay = false;
42
+ this.nowPlayingMessage = null;
43
+ this.previousTracks = new Array();
44
+
45
+ this.shouldDeleteMessage = options.shouldDeleteMessage ?? true;
43
46
 
44
47
  this.setupEventListeners();
45
48
  }
@@ -62,9 +65,10 @@ class Player extends EventEmitter {
62
65
  this.position = state.position;
63
66
  this.ping = state.ping;
64
67
  this.timestamp = state.time;
65
-
68
+ this.aqua.emit("playerUpdate", this, packet);
66
69
  }
67
70
 
71
+
68
72
  /**
69
73
  * Gets the previous track.
70
74
  * @returns {Object|null} The previous track or null if none exists.
@@ -83,64 +87,68 @@ class Player extends EventEmitter {
83
87
 
84
88
  /**
85
89
  * Plays the next track in the queue.
90
+ * @param {Object} options - Options for playing the next track.
91
+ * @param {string} options.query - The query to search for the next track.
92
+ * @param {boolean} options.force - Whether to force play the next track even if the queue is empty.
86
93
  * @returns {Promise<Player>} The player instance.
87
94
  * @throws {Error} If the player is not connected.
95
+ * @throws {Error} If the queue is empty and force is not set to true.
96
+ * @description This method plays the next track in the queue.
97
+ * @event play
88
98
  */
89
99
  async play() {
90
- if (!this.connected) throw new Error("Player connection is not established. Please connect first.");
91
- this.current = this.queue.shift();
92
- if (!this.current) return this;
100
+ if (!this.connected) throw new Error("Bro go on and use the connection first");
101
+ if (!this.queue.length) return;
93
102
 
94
- if (!this.current.track) {
95
- this.current = await this.current.resolve(this.aqua);
96
- }
103
+ this.current = this.queue.shift();
104
+ this.current = this.current.track ? this.current : await this.current.resolve(this.aqua);
105
+ this.playing = true;
106
+ this.position = 0;
97
107
 
98
- this.playing = true;
99
- this.position = 0;
108
+ this.aqua.emit("debug", this.guildId, `Playing track: ${this.current.track}`);
109
+ await this.updatePlayer({ track: { encoded: this.current.track } });
110
+ return this;
111
+ }
100
112
 
101
- this.aqua.emit("debug", this.guildId, `Playing track: ${this.current.track}`);
102
- await this.updatePlayer({ track: { encoded: this.current.track } });
103
- return this;
104
- }
105
113
  /**
106
- * Connects the player to the voice channel.
107
- * @param {Object} [options=this] - Connection options.
108
- * @param {string} options.guildId - The ID of the guild.
109
- * @param {string} options.voiceChannel - The ID of the voice channel.
110
- * @param {boolean} [options.deaf=true] - Whether to deaf the player.
111
- * @param {boolean} [options.mute=false] - Whether to mute the player.
114
+ * Connects the player to a voice channel.
115
+ * @param {Object} options - Options for connecting the player.
116
+ * @param {string} options.guildId - The guild ID to connect to.
117
+ * @param {string} options.voiceChannel - The ID of the voice channel to connect to.
118
+ * @param {boolean} [options.deaf=true] - Whether the player should be deafened.
119
+ * @param {boolean} [options.mute=false] - Whether the player should be muted.
112
120
  * @returns {Promise<Player>} The player instance.
121
+ * @throws {Error} If the player is already connected.
122
+ * @description This method connects the player to a voice channel.
123
+ * @event ready
113
124
  */
114
125
  async connect(options = this) {
115
126
  const { guildId, voiceChannel, deaf = true, mute = false } = options;
116
- await this.send({
117
- guild_id: guildId,
118
- channel_id: voiceChannel,
119
- self_deaf: deaf,
120
- self_mute: mute,
121
- });
127
+ await this.send({ guild_id: guildId, channel_id: voiceChannel, self_deaf: deaf, self_mute: mute });
122
128
  this.connected = true;
123
129
  this.aqua.emit("debug", this.guildId, `Player has connected to voice channel: ${voiceChannel}.`);
124
130
  }
125
131
 
126
132
  /**
127
- * Disconnects the player from the voice channel and leaves the channel.
128
- * @returns {Promise<Player>} The player instance.
133
+ * Destroys the player instance.
134
+ * @returns {Promise<void>} The result of the destroy method.
135
+ * @description This method destroys the player instance and clears all data.
136
+ * @event destroy
129
137
  */
130
138
  async destroy() {
131
- await this.updatePlayer({ track: { encoded: null }, });
139
+ await this.updatePlayer({ track: { encoded: null } });
132
140
  this.connected = false;
133
- await this.send({
134
- guild_id: this.guildId,
135
- channel_id: null,
136
- });
137
- this.clearData(); // Clear data when destroyed
141
+ await this.send({ guild_id: this.guildId, channel_id: null });
142
+ this.clearData();
138
143
  this.aqua.emit("debug", this.guildId, "Player has disconnected from voice channel.");
139
144
  }
145
+
140
146
  /**
141
- * Pauses or resumes the player.
142
- * @param {boolean} paused - Whether to pause the player.
147
+ * Pauses the player.
148
+ * @param {boolean} paused - Whether the player should be paused.
143
149
  * @returns {Promise<Player>} The player instance.
150
+ * @description This method pauses the player.
151
+ * @event pause
144
152
  */
145
153
  async pause(paused) {
146
154
  this.paused = paused;
@@ -149,9 +157,11 @@ class Player extends EventEmitter {
149
157
  }
150
158
 
151
159
  /**
152
- * Seeks to a specific position in the current track.
153
- * @param {number} position - The position in milliseconds to seek to.
160
+ * Seeks the player to a specific position.
161
+ * @param {number} position - The position to seek to in milliseconds.
154
162
  * @returns {Promise<Player>} The player instance.
163
+ * @description This method seeks the player to a specific position.
164
+ * @event seek
155
165
  */
156
166
  async seek(position) {
157
167
  this.position = position;
@@ -160,26 +170,27 @@ class Player extends EventEmitter {
160
170
  }
161
171
 
162
172
  /**
163
- * Stops playback and clears the current track.
173
+ * Stops the player.
164
174
  * @returns {Promise<Player>} The player instance.
175
+ * @description This method stops the player.
176
+ * @event stop
165
177
  */
166
178
  async stop() {
167
- if (!this.playing) return this; // If not playing, return early
179
+ if (!this.playing) return this;
168
180
  this.playing = false;
169
181
  this.current = null;
170
182
  this.position = 0;
171
- await this.updatePlayer({ track:
172
- {
173
- encoded: null,
174
- }
175
- });
183
+ await this.updatePlayer({ track: { encoded: null } });
176
184
  return this;
177
185
  }
186
+
178
187
  /**
179
188
  * Sets the volume of the player.
180
- * @param {number} volume - The volume level (0-200).
189
+ * @param {number} volume - The volume to set between 0 and 200.
181
190
  * @returns {Promise<Player>} The player instance.
182
- * @throws {Error} If the volume is out of bounds.
191
+ * @throws {Error} If the volume is not between 0 and 200.
192
+ * @description This method sets the volume of the player.
193
+ * @event volumeChange
183
194
  */
184
195
  async setVolume(volume) {
185
196
  if (volume < 0 || volume > 200) throw new Error("[Volume] Volume must be between 0 and 200.");
@@ -189,10 +200,12 @@ class Player extends EventEmitter {
189
200
  }
190
201
 
191
202
  /**
192
- * Sets the loop mode for the player.
193
- * @param {string} mode - The loop mode ('none', 'track', 'queue').
203
+ * Sets the loop mode of the player.
204
+ * @param {string} mode - The loop mode to set, either 'none', 'track', or 'queue'.
194
205
  * @returns {Promise<Player>} The player instance.
195
- * @throws {Error} If the loop mode is invalid.
206
+ * @throws {Error} If the loop mode is not 'none', 'track', or 'queue'.
207
+ * @description This method sets the loop mode of the player.
208
+ * @event loopChange
196
209
  */
197
210
  async setLoop(mode) {
198
211
  if (!["none", "track", "queue"].includes(mode)) throw new Error("Loop mode must be 'none', 'track', or 'queue'.");
@@ -202,9 +215,11 @@ class Player extends EventEmitter {
202
215
  }
203
216
 
204
217
  /**
205
- * Sends data to the player.
206
- * @param {Object} data - The data to send.
218
+ * Sends an update to the player.
219
+ * @param {Object} data - The data to send to the player.
207
220
  * @returns {Promise<Player>} The player instance.
221
+ * @description This method sends an update to the player.
222
+ * @event update
208
223
  */
209
224
  async send(data) {
210
225
  await this.updatePlayer(data);
@@ -212,9 +227,11 @@ class Player extends EventEmitter {
212
227
  }
213
228
 
214
229
  /**
215
- * Sets the text channel for the player.
216
- * @param {string} channel - The ID of the text channel.
230
+ * Sets the text channel of the player.
231
+ * @param {string} channel - The ID of the text channel to set.
217
232
  * @returns {Promise<Player>} The player instance.
233
+ * @description This method sets the text channel of the player.
234
+ * @event textChannelChange
218
235
  */
219
236
  async setTextChannel(channel) {
220
237
  await this.updatePlayer({ text_channel: channel });
@@ -222,11 +239,11 @@ class Player extends EventEmitter {
222
239
  }
223
240
 
224
241
  /**
225
- * Sets the voice channel for the player.
226
- * @param {string} channel - The ID of the voice channel.
242
+ * Sets the voice channel of the player.
243
+ * @param {string} channel - The ID of the voice channel to set.
227
244
  * @returns {Promise<Player>} The player instance.
228
- * @throws {TypeError} If the channel is not a string.
229
- * @throws {ReferenceError} If the player is already connected to the channel.
245
+ * @description This method sets the voice channel of the player.
246
+ * @event voiceChannelChange
230
247
  */
231
248
  async setVoiceChannel(channel) {
232
249
  if (typeof channel !== "string") throw new TypeError("Channel must be a non-empty string.");
@@ -240,48 +257,77 @@ class Player extends EventEmitter {
240
257
 
241
258
  /**
242
259
  * Disconnects the player from the voice channel.
243
- * @returns {Promise<Player>} The player instance.
260
+ * @returns {Promise<void>} The result of the disconnect method.
261
+ * @description This method disconnects the player from the voice channel.
262
+ * @event disconnect
244
263
  */
245
264
  async disconnect() {
246
- await this.updatePlayer({ track: { encoded: null }});
265
+ await this.updatePlayer({ track: { encoded: null } });
247
266
  await this.send({ guild_id: this.guildId, channel_id: null });
248
267
  this.connected = false;
249
268
  this.aqua.emit("debug", this.guildId, "Player has disconnected from voice channel.");
250
269
  }
251
270
 
252
271
  /**
253
- * Handles events from the player.
254
- * @param {Object} payload - The event payload.
272
+ * Shuffles the queue of the player.
273
+ * @returns {Promise<Player>} The player instance.
274
+ * @description This method shuffles the queue of the player.
275
+ * @event shuffle
255
276
  */
256
- async handleEvent(payload) {
257
- const player = this.aqua.players.get(payload.guildId);
258
- if (!player) return;
259
-
260
- const track = this.current;
261
-
262
- switch (payload.type) {
263
- case "TrackStartEvent":
264
- this.trackStart(player, track, payload);
265
- break;
266
- case "TrackEndEvent":
267
- this.trackEnd(player, track, payload);
268
- break;
269
- case "TrackExceptionEvent":
270
- this.trackError(player, track, payload);
271
- break;
272
- case "TrackStuckEvent":
273
- this.trackStuck(player, track, payload);
274
- break;
275
- case "TrackChangeEvent":
276
- this.trackChange(player, track, payload);
277
- case "WebSocketClosedEvent":
278
- this.socketClosed(player, track, payload);
279
- break;
280
- default:
281
- this.handleUnknownEvent(payload);
282
- break;
283
- }
277
+ async shuffle() {
278
+ this.queue.shuffle();
279
+ return this;
280
+ }
281
+
282
+ /**
283
+ * Gets the queue of the player.
284
+ * @returns {Array<Object>} The queue of the player.
285
+ * @description This method gets the queue of the player.
286
+ * @event getQueue
287
+ */
288
+ async getQueue() {
289
+ return this.queue;
290
+ }
291
+
292
+ /**
293
+ * Replays the current track from the start.
294
+ * @returns {Promise<Player>} The player instance.
295
+ * @description This method replays the current track from the start.
296
+ * @event replay
297
+ */
298
+ async replay() {
299
+ return this.seek(0);
300
+ }
301
+ async handleEvent(payload) {
302
+ const player = this.aqua.players.get(payload.guildId);
303
+ if (!player) return;
304
+
305
+ const track = this.current;
306
+
307
+ switch (payload.type) {
308
+ case "TrackStartEvent":
309
+ this.trackStart(player, track, payload);
310
+ break;
311
+ case "TrackEndEvent":
312
+ this.trackEnd(player, track, payload);
313
+ break;
314
+ case "TrackExceptionEvent":
315
+ this.trackError(player, track, payload);
316
+ break;
317
+ case "TrackStuckEvent":
318
+ this.trackStuck(player, track, payload);
319
+ break;
320
+ case "TrackChangeEvent":
321
+ this.trackChange(player, track, payload);
322
+ break;
323
+ case "WebSocketClosedEvent":
324
+ this.socketClosed(player, track, payload);
325
+ break;
326
+ default:
327
+ this.handleUnknownEvent(payload);
328
+ break;
284
329
  }
330
+ }
285
331
 
286
332
  /**
287
333
  * Handles track start events.
@@ -297,7 +343,7 @@ class Player extends EventEmitter {
297
343
 
298
344
  trackChange(player, track, payload) {
299
345
  this.playing = true;
300
- this.paused = this.player.this.paused
346
+ this.paused = false
301
347
  this.aqua.emit("trackChange", player, track, payload);
302
348
  }
303
349
 
@@ -307,31 +353,27 @@ class Player extends EventEmitter {
307
353
  * @param {Object} payload - The event payload.
308
354
  */
309
355
  trackEnd(player, track, payload) {
310
- this.addToPreviousTrack(this.current);
356
+
357
+ if (this.shouldDeleteMessage && this.nowPlayingMessage) {
358
+ this.nowPlayingMessage.delete();
359
+ this.nowPlayingMessage = null;
360
+
361
+ }
311
362
  if (["loadfailed", "cleanup"].includes(payload.reason.replace("_", "").toLowerCase())) {
312
- if (player.queue.length === 0) {
313
- this.playing = false;
314
- return this.aqua.emit("queueEnd", player);
315
- }
316
- this.aqua.emit("trackEnd", player, payload);
317
- return player.play();
363
+ return player.queue.length === 0 ? this.aqua.emit("queueEnd", player) : player.play();
318
364
  }
365
+ this.addToPreviousTrack(track)
319
366
  if (this.loop === "track") {
320
- player.queue.unshift(player.previous);
321
- this.aqua.emit("trackEnd", player, payload);
322
- return player.play();
323
- } else if (this.loop === "queue") {
324
- player.queue.push(player.previous);
325
- this.aqua.emit("trackEnd", player, payload);
367
+ player.queue.push(this.previous);
326
368
  return player.play();
327
369
  }
328
370
  if (player.queue.length === 0) {
329
371
  this.playing = false;
330
372
  return this.aqua.emit("queueEnd", player);
331
- } else {
332
- this.aqua.emit("trackEnd", player, payload);
333
- return player.play();
334
373
  }
374
+ this.cleanup();
375
+ this.clearData();
376
+ return player.play();
335
377
  }
336
378
 
337
379
  /**
@@ -403,7 +445,7 @@ class Player extends EventEmitter {
403
445
  * @returns {Player} The player instance.
404
446
  */
405
447
  clearData() {
406
- this.data = {} // Clear all custom data efficiently
448
+ this.data = {};
407
449
  return this;
408
450
  }
409
451
 
@@ -439,4 +481,5 @@ class Player extends EventEmitter {
439
481
  }
440
482
  }
441
483
 
442
- module.exports = { Player };
484
+ module.exports = { Player };
485
+
@@ -100,3 +100,4 @@ class Queue extends Array {
100
100
  }
101
101
 
102
102
  module.exports = { Queue };
103
+
@@ -1,5 +1,4 @@
1
1
  const { fetch: undiciFetch } = require("undici");
2
- const nodeUtil = require("node:util");
3
2
 
4
3
  class Rest {
5
4
  constructor(aqua, options) {
@@ -9,10 +8,6 @@ class Rest {
9
8
  this.password = options.password;
10
9
  this.version = options.restVersion;
11
10
  this.calls = 0;
12
- this.queue = [];
13
- this.maxQueueSize = options.maxQueueSize || 100;
14
- this.maxConcurrentRequests = options.maxConcurrentRequests || 5;
15
- this.activeRequests = 0;
16
11
  }
17
12
 
18
13
  setSessionId(sessionId) {
@@ -24,24 +19,22 @@ class Rest {
24
19
  "Content-Type": "application/json",
25
20
  Authorization: this.password,
26
21
  };
27
- const requestOptions = {
22
+
23
+ const response = await undiciFetch(`${this.url}${endpoint}`, {
28
24
  method,
29
25
  headers,
30
- body: body ? JSON.stringify(body) : null,
31
- };
32
- try {
33
- const response = await undiciFetch(`${this.url}${endpoint}`, requestOptions);
34
- this.calls++;
35
- const data = await this.parseResponse(response);
36
- this.aqua.emit("apiResponse", endpoint, response);
37
- this.aqua.emit(
38
- "debug",
39
- `[Rest] ${method} ${endpoint} ${body ? `body: ${JSON.stringify(body)}` : ""} -> Status Code: ${response.status} Response: ${nodeUtil.inspect(data)}`
40
- );
41
- return includeHeaders ? { data, headers: response.headers } : data;
42
- } catch (error) {
43
- throw new Error(`Network error during request: ${method} ${this.url}${endpoint}`, { cause: error });
26
+ body: body && JSON.stringify(body),
27
+ });
28
+
29
+ this.calls++;
30
+
31
+ const data = await response.json();
32
+ this.aqua.emit("apiResponse", endpoint, response);
33
+
34
+ if (includeHeaders) {
35
+ return { data, headers: response.headers };
44
36
  }
37
+ return data;
45
38
  }
46
39
 
47
40
  async getPlayers() {
@@ -50,15 +43,18 @@ class Rest {
50
43
 
51
44
  async updatePlayer(options) {
52
45
  const requestBody = { ...options.data };
46
+
53
47
  if ((requestBody.track && requestBody.track.encoded && requestBody.track.identifier) ||
54
48
  (requestBody.encodedTrack && requestBody.identifier)) {
55
49
  throw new Error(`Cannot provide both 'encoded' and 'identifier' for track in Update Player Endpoint`);
56
50
  }
51
+
57
52
  if (this.version === "v3" && options.data?.track) {
58
53
  const { track } = requestBody;
59
54
  delete requestBody.track;
60
55
  Object.assign(requestBody, track.encoded ? { encodedTrack: track.encoded } : { identifier: track.identifier });
61
56
  }
57
+
62
58
  return this.makeRequest("PATCH", `/${this.version}/sessions/${this.sessionId}/players/${options.guildId}?noReplace=false`, requestBody);
63
59
  }
64
60
 
@@ -70,7 +66,7 @@ class Rest {
70
66
  return this.makeRequest("GET", `/${this.version}/loadtracks?identifier=${encodeURIComponent(identifier)}`);
71
67
  }
72
68
 
73
- async decodeTrack(track, node) {
69
+ async decodeTrack(track) {
74
70
  return this.makeRequest("GET", `/${this.version}/decodetrack?encodedTrack=${encodeURIComponent(track)}`);
75
71
  }
76
72
 
@@ -79,10 +75,7 @@ class Rest {
79
75
  }
80
76
 
81
77
  async getStats() {
82
- if (this.version === "v3") {
83
- return this.makeRequest("GET", `/${this.version}/stats`);
84
- }
85
- return this.makeRequest("GET", `/${this.version}/stats/all`);
78
+ return this.makeRequest("GET", this.version === "v3" ? `/${this.version}/stats` : `/${this.version}/stats/all`);
86
79
  }
87
80
 
88
81
  async getInfo() {
@@ -96,25 +89,7 @@ class Rest {
96
89
  async getRoutePlannerAddress(address) {
97
90
  return this.makeRequest("POST", `/${this.version}/routeplanner/free/address`, { address });
98
91
  }
99
-
100
- async parseResponse(response) {
101
- if (response.status === 204) {
102
- return null;
103
- }
104
- try {
105
- const contentType = response.headers.get("Content-Type");
106
- return await response[contentType.includes("text/plain") ? "text" : "json"]();
107
- } catch (error) {
108
- this.aqua.emit("debug", `[Rest - Error] Failed to process response from ${response.url}: ${error}`);
109
- return null;
110
- }
111
- }
112
- /**
113
- * Cleans up resources related to the queue.
114
- */
115
- cleanupQueue() {
116
- this.queue = [];
117
- }
118
92
  }
119
93
 
120
- module.exports = { Rest };
94
+ module.exports = { Rest };
95
+
@@ -7,7 +7,7 @@ const { getImageUrl } = require("../handlers/fetchImage");
7
7
  */
8
8
  class Track {
9
9
  /**
10
- * @param {{ encoded: string, info: { identifier: string, isSeekable: boolean, author: string, length: number, isStream: boolean, position: number, title: string, uri: string, sourceName: string, thumbnail: string, track: string, tracks: Array<Track>, playlist: { name: string, selectedTrack: number } } }} data
10
+ * @param {{ encoded: string, info: { identifier: string, isSeekable: boolean, author: string, length: number, isStream: boolean, position: number, title: string, uri: string, sourceName: string, artworkUrl: string, track: string, tracks: Array<Track>, playlist: { name: string, selectedTrack: number } } }} data
11
11
  * @param {Player} requester
12
12
  * @param {Node} nodes
13
13
  */
@@ -17,6 +17,7 @@ class Track {
17
17
  this.requester = requester;
18
18
  this.nodes = nodes;
19
19
  this.track = data.encoded || Buffer.from(data.track, "base64").toString("utf8");
20
+ this.playlist = data.playlist || null;
20
21
  }
21
22
 
22
23
  /**
@@ -62,14 +63,18 @@ class Track {
62
63
  updateTrackInfo(track) {
63
64
  this.info.identifier = track.info.identifier;
64
65
  this.track = track.track;
66
+ if (track.playlist) {
67
+ this.playlist = track.playlist;
68
+ }
65
69
  }
66
70
 
67
71
  /**
68
72
  * @private
69
73
  */
70
74
  cleanup() {
71
- this.rawData = this.track = this.info = null;
75
+ this.rawData = this.track = this.info = this.playlist = null;
72
76
  }
73
77
  }
74
78
 
75
79
  module.exports = { Track };
80
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aqualink",
3
- "version": "1.0.5",
3
+ "version": "1.2.0",
4
4
  "description": "An Lavalink wrapper, focused in speed, performance, and features, Based in Riffy!",
5
5
  "main": "build/index.js",
6
6
  "scripts": {