aqualink 1.7.0-beta4 → 1.7.0-beta5

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,8 +4,9 @@ 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
10
  * @param {Object} client - The client instance.
10
11
  * @param {Array<Object>} nodes - An array of node configurations.
11
12
  * @param {Object} options - Configuration options for Aqua.
@@ -47,7 +48,6 @@ class Aqua extends EventEmitter {
47
48
  this.setMaxListeners(0);
48
49
  }
49
50
 
50
-
51
51
  validateInputs(client, nodes, options) {
52
52
  if (!client) throw new Error("Client is required to initialize Aqua");
53
53
  if (!Array.isArray(nodes) || !nodes.length) throw new Error(`Nodes must be a non-empty Array (Received ${typeof nodes})`);
@@ -55,8 +55,7 @@ class Aqua extends EventEmitter {
55
55
  }
56
56
 
57
57
  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) : [];
58
+ return [...this.nodeMap.values()].filter(node => node.connected).sort((a, b) => a.rest.calls - b.rest.calls);
60
59
  }
61
60
 
62
61
  init(clientId) {
@@ -78,41 +77,42 @@ class Aqua extends EventEmitter {
78
77
  this.destroyNode(nodeId); // Ensure no duplicate nodes
79
78
  const node = new Node(this, options, this.options);
80
79
  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
- }
80
+ node.connect()
81
+ .then(() => this.emit("nodeCreate", node))
82
+ .catch(error => {
83
+ this.nodeMap.delete(nodeId);
84
+ throw error;
85
+ });
86
+ return node;
89
87
  }
90
88
 
91
89
  destroyNode(identifier) {
92
90
  const node = this.nodeMap.get(identifier);
93
91
  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
- }
92
+
93
+ node.disconnect()
94
+ .then(() => {
95
+ node.removeAllListeners();
96
+ this.nodeMap.delete(identifier);
97
+ this.emit("nodeDestroy", node);
98
+ })
99
+ .catch(error => console.error(`Error destroying node ${identifier}:`, error));
102
100
  }
103
101
 
104
102
  updateVoiceState({ d, t }) {
105
103
  const player = this.players.get(d.guild_id);
106
- if (player && (t === "VOICE_SERVER_UPDATE" || t === "VOICE_STATE_UPDATE" && d.user_id === this.clientId)) {
104
+ if (player && (t === "VOICE_SERVER_UPDATE" || (t === "VOICE_STATE_UPDATE" && d.user_id === this.clientId))) {
107
105
  player.connection[t === "VOICE_SERVER_UPDATE" ? "setServerUpdate" : "setStateUpdate"](d);
108
106
  if (d.status === "disconnected") this.cleanupPlayer(player);
109
107
  }
110
108
  }
109
+
111
110
  fetchRegion(region) {
112
111
  if (!region) return this.leastUsedNodes;
113
112
  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));
113
+ return [...this.nodeMap.values()]
114
+ .filter(node => node.connected && node.regions?.includes(lowerRegion))
115
+ .sort((a, b) => this.calculateLoad(a) - this.calculateLoad(b));
116
116
  }
117
117
 
118
118
  calculateLoad(node) {
@@ -125,6 +125,7 @@ class Aqua extends EventEmitter {
125
125
  this.ensureInitialized();
126
126
  const player = this.players.get(options.guildId);
127
127
  if (player && player.voiceChannel) return player;
128
+
128
129
  const node = options.region ? this.fetchRegion(options.region)[0] : this.leastUsedNodes[0];
129
130
  if (!node) throw new Error("No nodes are available");
130
131
  return this.createPlayer(node, options);
@@ -140,22 +141,21 @@ class Aqua extends EventEmitter {
140
141
  return player;
141
142
  }
142
143
 
143
- async destroyPlayer(guildId) {
144
- const player = this.players.get(guildId);
145
- if (!player) return;
144
+ async destroyPlayer(guildId) {
145
+ const player = this.players.get(guildId);
146
+ if (!player) return;
146
147
 
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);
148
+ try {
149
+ await player.clearData(); // Assuming clearData is an async function
150
+ player.removeAllListeners();
151
+ this.players.delete(guildId);
152
+ this.emit("playerDestroy", player);
153
+ } catch (error) {
154
+ console.error(`Error destroying player for guild ${guildId}:`, error);
155
+ }
155
156
  }
156
- }
157
157
 
158
- async resolve({ query, source = this.defaultSearchPlatform , requester, nodes }) {
158
+ async resolve({ query, source = this.defaultSearchPlatform, requester, nodes }) {
159
159
  this.ensureInitialized();
160
160
  const requestNode = this.getRequestNode(nodes);
161
161
  const formattedQuery = this.formatQuery(query, source);
@@ -188,11 +188,11 @@ async destroyPlayer(guildId) {
188
188
  return URL_REGEX.test(query) ? query : `${source}:${query}`;
189
189
  }
190
190
 
191
- handleNoMatches(rest, query) {
191
+ async handleNoMatches(rest, query) {
192
192
  try {
193
- const youtubeResponse = rest.makeRequest("GET", `/v4/loadtracks?identifier=https://www.youtube.com/watch?v=${query}`, { signal: controller.signal });
193
+ const youtubeResponse = await rest.makeRequest("GET", `/v4/loadtracks?identifier=https://www.youtube.com/watch?v=${query}`);
194
194
  if (["empty", "NO_MATCHES"].includes(youtubeResponse.loadType)) {
195
- return rest.makeRequest("GET", `/v4/loadtracks?identifier=https://open.spotify.com/track/${query}`, { signal: controller.signal });
195
+ return await rest.makeRequest("GET", `/v4/loadtracks?identifier=https://open.spotify.com/track/${query}`);
196
196
  }
197
197
  return youtubeResponse;
198
198
  } catch (error) {
@@ -208,10 +208,12 @@ async destroyPlayer(guildId) {
208
208
  pluginInfo: response.pluginInfo ?? {},
209
209
  tracks: [],
210
210
  };
211
+
211
212
  if (response.loadType === "error" || response.loadType === "LOAD_FAILED") {
212
213
  baseResponse.exception = response.data ?? response.exception;
213
214
  return baseResponse;
214
215
  }
216
+
215
217
  const trackFactory = (trackData) => new Track(trackData, requester, requestNode);
216
218
  switch (response.loadType) {
217
219
  case "track":
@@ -283,4 +285,4 @@ async destroyPlayer(guildId) {
283
285
  }
284
286
  }
285
287
 
286
- module.exports = { Aqua };
288
+ 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 };
@@ -6,6 +6,8 @@ class Node {
6
6
  #statsCache = {};
7
7
  #lastStatsRequest = 0;
8
8
  #reconnectAttempted = 0;
9
+ #reconnectTimeoutId = null;
10
+
9
11
  constructor(aqua, nodes, options) {
10
12
  const {
11
13
  name,
@@ -43,25 +45,9 @@ 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: null, nulled: null, deficit: null },
65
51
  ping: 0
66
52
  };
67
53
  }
@@ -83,14 +69,9 @@ class Node {
83
69
 
84
70
  #cleanup() {
85
71
  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
- }
72
+ this.#ws.removeAllListeners();
73
+ this.#ws.terminate();
74
+ this.#ws = null;
94
75
  }
95
76
  }
96
77
 
@@ -107,11 +88,10 @@ class Node {
107
88
 
108
89
  #setupWebSocketListeners() {
109
90
  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));
91
+ this.#ws.once("open", this.#onOpen.bind(this));
92
+ this.#ws.once("error", this.#onError.bind(this));
93
+ this.#ws.on("message", this.#onMessage.bind(this));
94
+ this.#ws.once("close", this.#onClose.bind(this));
115
95
  }
116
96
 
117
97
  async #onOpen() {
@@ -119,11 +99,12 @@ class Node {
119
99
  this.aqua.emit('debug', this.name, `Connected to ${this.wsUrl.href}`);
120
100
  try {
121
101
  this.info = await this.rest.makeRequest("GET", "/v4/info");
122
- this.autoResume && await this.resumePlayers();
102
+ if (this.autoResume) await this.resumePlayers();
123
103
  } catch (err) {
124
104
  this.info = null;
125
- !this.aqua.bypassChecks?.nodeFetchInfo &&
105
+ if (!this.aqua.bypassChecks?.nodeFetchInfo) {
126
106
  this.aqua.emit('error', `Failed to fetch node info: ${err.message}`);
107
+ }
127
108
  }
128
109
  }
129
110
 
@@ -147,7 +128,7 @@ class Node {
147
128
 
148
129
  #updateStats(payload) {
149
130
  if (!payload) return;
150
- const newStats = {
131
+ this.stats = Object.freeze({
151
132
  players: payload.players ?? 0,
152
133
  playingPlayers: payload.playingPlayers ?? 0,
153
134
  uptime: payload.uptime ?? 0,
@@ -155,8 +136,7 @@ class Node {
155
136
  memory: this.#updateMemoryStats(payload.memory),
156
137
  cpu: this.#updateCpuStats(payload.cpu),
157
138
  frameStats: this.#updateFrameStats(payload.frameStats)
158
- };
159
- this.stats = Object.freeze(newStats);
139
+ });
160
140
  }
161
141
 
162
142
  #updateMemoryStats(memory = {}) {
@@ -191,11 +171,6 @@ class Node {
191
171
  deficit: 0
192
172
  });
193
173
  }
194
- return Object.freeze({
195
- sent: frameStats.sent ?? 0,
196
- nulled: frameStats.nulled ?? 0,
197
- deficit: frameStats.deficit ?? 0
198
- });
199
174
  }
200
175
 
201
176
  #onMessage(msg) {
@@ -242,23 +217,18 @@ class Node {
242
217
 
243
218
  #reconnect() {
244
219
  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);
220
+ this.aqua.emit("nodeReconnect", this, "Experimental infinite reconnects enabled, will be trying non-stop...");
221
+ setTimeout(() => this.connect(), 10000);
249
222
  return;
250
223
  }
251
224
 
252
- if (this.connected) {
253
- clearTimeout(this.reconnectTimeoutId);
254
- }
225
+ clearTimeout(this.#reconnectTimeoutId);
255
226
  if (++this.#reconnectAttempted >= this.reconnectTries) {
256
227
  this.aqua.emit("nodeError", this, new Error(`Max reconnection attempts reached (${this.reconnectTries})`));
257
- clearTimeout(this.reconnectTimeoutId);
258
228
  return this.destroy();
259
229
  }
260
- clearTimeout(this.reconnectTimeoutId);
261
- this.reconnectTimeoutId = setTimeout(() => {
230
+
231
+ this.#reconnectTimeoutId = setTimeout(() => {
262
232
  this.aqua.emit("nodeReconnect", this, this.#reconnectAttempted);
263
233
  this.connect();
264
234
  }, this.reconnectTimeout * Math.pow(2, this.#reconnectAttempted)); // Exponential backoff
@@ -271,8 +241,7 @@ class Node {
271
241
  penalties += Math.round(Math.pow(1.05, 100 * this.stats.cpu.systemLoad) * 10 - 10);
272
242
  }
273
243
  if (this.stats.frameStats) {
274
- penalties += this.stats.frameStats.deficit;
275
- penalties += this.stats.frameStats.nulled * 2;
244
+ penalties += this.stats.frameStats.deficit + this.stats.frameStats.nulled * 2;
276
245
  }
277
246
  return penalties;
278
247
  }
@@ -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;
@@ -30,23 +35,23 @@ class Player extends EventEmitter {
30
35
  this.previousTracks = [];
31
36
  this.shouldDeleteMessage = options.shouldDeleteMessage ?? true;
32
37
 
33
- this.on("playerUpdate", this.onPlayerUpdate.bind(this));
34
- this.on("event", this.handleEvent.bind(this));
38
+ this.boundHandleEvent = this.handleEvent.bind(this);
39
+ this.boundOnPlayerUpdate = this.onPlayerUpdate.bind(this);
40
+ this.on("playerUpdate", this.boundOnPlayerUpdate);
41
+ this.on("event", this.boundHandleEvent);
35
42
  }
36
43
 
37
- onPlayerUpdate(packet) {
38
- if (!packet?.state) return;
39
- const { state } = packet;
44
+ onPlayerUpdate({ state } = {}) {
45
+ if (!state) return;
46
+
40
47
  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);
48
+ Object.assign(this, { connected, position, ping, timestamp: time });
49
+
50
+ this.aqua.emit("playerUpdate", this, { state });
46
51
  }
47
52
 
48
53
  get previous() {
49
- return this.previousTracks.length ? this.previousTracks[0] : null;
54
+ return this.previousTracks[0] || null;
50
55
  }
51
56
 
52
57
  get currenttrack() {
@@ -60,25 +65,28 @@ class Player extends EventEmitter {
60
65
  this.previousTracks.unshift(track);
61
66
  }
62
67
 
63
-
64
68
  async play() {
65
- if (!this.connected) throw new Error("Player must be connected first.");
69
+ if (!this.connected) {
70
+ throw new Error("Player must be connected first.");
71
+ }
66
72
  if (!this.queue.length) return;
67
-
73
+
68
74
  const track = this.queue.shift();
69
-
70
75
  this.current = track.track ? track : await track.resolve(this.aqua);
71
76
 
72
- this.playing = true;
73
- this.position = 0;
77
+ Object.assign(this, {
78
+ playing: true,
79
+ position: 0
80
+ });
81
+
74
82
  this.aqua.emit("debug", this.guildId, `Playing track: ${this.current.track}`);
75
- this.updatePlayer({ track: { encoded: this.current.track } });
76
- return this;
83
+ return this.updatePlayer({ track: { encoded: this.current.track } });
77
84
  }
78
85
 
79
- connect(options) {
80
- if (this.connected) throw new Error("Player is already connected.");
81
- const { guildId, voiceChannel, deaf = true, mute = false } = options;
86
+ connect({ guildId, voiceChannel, deaf = true, mute = false }) {
87
+ if (this.connected) {
88
+ throw new Error("Player is already connected.");
89
+ }
82
90
  this.send({
83
91
  guild_id: guildId,
84
92
  channel_id: voiceChannel,
@@ -93,7 +101,7 @@ class Player extends EventEmitter {
93
101
  destroy() {
94
102
  if (!this.connected) return this;
95
103
  this.disconnect();
96
- this.nowPlayingMessage?.delete().catch(() => { }); // ignore the error
104
+ this.nowPlayingMessage?.delete().catch(() => { });
97
105
  this.aqua.destroyPlayer(this.guildId);
98
106
  this.nodes.rest.destroyPlayer(this.guildId);
99
107
  return this;
@@ -107,30 +115,29 @@ class Player extends EventEmitter {
107
115
 
108
116
  async searchLyrics(query) {
109
117
  if (!query) return null;
110
-
111
- const response = await this.nodes.rest.getLyrics({
112
- track: {
118
+ const response = await this.nodes.rest.getLyrics({
119
+ track: {
113
120
  encoded: { info: { title: query } },
114
121
  guild_id: this.guildId,
115
122
  search: true
116
- }
123
+ }
117
124
  });
118
-
119
125
  return response || null;
120
126
  }
127
+
121
128
  async lyrics() {
122
129
  if (!this.playing) return null;
123
130
  const response = await this.nodes.rest.getLyrics({
124
131
  track: {
125
- encoded: this.current.track,
126
- guild_id: this.guildId
132
+ encoded: this.current.track,
133
+ guild_id: this.guildId
127
134
  },
128
- });
135
+ });
129
136
  return response || null;
130
137
  }
131
138
 
132
139
  seek(position) {
133
- if (!this.playing) return this;
140
+ if (!this.playing) return this;
134
141
  const newPosition = this.position + position;
135
142
  if (newPosition < 0) {
136
143
  throw new Error("Seek position cannot be negative.");
@@ -155,8 +162,7 @@ class Player extends EventEmitter {
155
162
  return this;
156
163
  }
157
164
 
158
- static validModes = new Set(["none", "track", "queue"]);
159
-
165
+ static validModes = new Set(["none", "track", "queue"]);
160
166
  setLoop(mode) {
161
167
  if (!Player.validModes.has(mode)) throw new Error("Loop mode must be 'none', 'track', or 'queue'.");
162
168
  this.loop = mode;
@@ -192,10 +198,10 @@ class Player extends EventEmitter {
192
198
  }
193
199
 
194
200
  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]];
201
+ const arr = this.queue;
202
+ for (let i = arr.length - 1; i > 0; i--) {
203
+ const j = (Math.random() * (i + 1)) | 0;
204
+ [arr[i], arr[j]] = [arr[j], arr[i]];
199
205
  }
200
206
  return this;
201
207
  }
@@ -213,30 +219,29 @@ class Player extends EventEmitter {
213
219
  return this.playing ? this.play() : undefined;
214
220
  }
215
221
 
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
- ]);
222
+ static EVENT_HANDLERS = Object.freeze({
223
+ TrackStartEvent: "trackStart",
224
+ TrackEndEvent: "trackEnd",
225
+ TrackExceptionEvent: "trackError",
226
+ TrackStuckEvent: "trackStuck",
227
+ TrackChangeEvent: "trackChange",
228
+ WebSocketClosedEvent: "socketClosed"
229
+ });
224
230
 
225
- handleEvent = (payload) => {
231
+ async handleEvent(payload) {
226
232
  const player = this.aqua.players.get(payload.guildId);
227
233
  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);
234
+
235
+ const handler = Player.EVENT_HANDLERS[payload.type];
236
+ if (handler) {
237
+ await this[handler](player, this.current, payload);
232
238
  } else {
233
- this.handleUnknownEvent(player, track, payload);
239
+ this.handleUnknownEvent(payload);
234
240
  }
235
- };
241
+ }
236
242
 
237
243
  trackStart(player, track) {
238
- this.playing = true;
239
- this.paused = false;
244
+ Object.assign(this, { playing: true, paused: false });
240
245
  this.aqua.emit("trackStart", player, track);
241
246
  }
242
247
 
@@ -248,36 +253,34 @@ class Player extends EventEmitter {
248
253
 
249
254
  async trackEnd(player, track, payload) {
250
255
  if (this.shouldDeleteMessage && this.nowPlayingMessage) {
251
- try {
252
- await this.nowPlayingMessage.delete();
253
- } catch (error) {
254
- } finally {
255
- this.nowPlayingMessage = null;
256
- }
256
+ await this.nowPlayingMessage?.delete().catch(() => { });
257
+ this.nowPlayingMessage = null;
257
258
  }
259
+
258
260
  const reason = payload.reason.replace("_", "").toLowerCase();
259
261
  if (reason === "loadfailed" || reason === "cleanup") {
260
- if (player.queue.isEmpty()) {
261
- this.aqua.emit("queueEnd", player);
262
- return;
263
- }
264
- return player.play();
262
+ return player.queue.isEmpty() ?
263
+ this.aqua.emit("queueEnd", player) :
264
+ player.play();
265
265
  }
266
+
266
267
  switch (this.loop) {
267
- case "track":
268
+ case Player.LOOP_MODES.TRACK:
268
269
  this.aqua.emit("trackRepeat", player, track);
269
270
  player.queue.unshift(track);
270
271
  break;
271
- case "queue":
272
+ case Player.LOOP_MODES.QUEUE:
272
273
  this.aqua.emit("queueRepeat", player, track);
273
274
  player.queue.push(track);
274
275
  break;
275
276
  }
277
+
276
278
  if (player.queue.isEmpty()) {
277
279
  this.playing = false;
278
280
  this.aqua.emit("queueEnd", player);
279
281
  return this.cleanup();
280
282
  }
283
+
281
284
  return player.play();
282
285
  }
283
286
 
@@ -309,9 +312,8 @@ class Player extends EventEmitter {
309
312
  this.aqua.send({ op: 4, d: data });
310
313
  }
311
314
 
312
- #dataStore = new WeakMap();
313
-
314
- set(key, value) {
315
+ #dataStore = new Map();
316
+ set(key, value) {
315
317
  this.#dataStore.set(key, value);
316
318
  }
317
319
 
@@ -320,7 +322,7 @@ class Player extends EventEmitter {
320
322
  }
321
323
 
322
324
  clearData() {
323
- this.#dataStore.delete()
325
+ this.#dataStore.clear();
324
326
  return this;
325
327
  }
326
328
 
@@ -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-beta5",
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": {