aqualink 2.6.1 → 2.6.2

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
@@ -195,18 +195,18 @@ client.login("Yourtokenhere");
195
195
  <div align="center">
196
196
  <table>
197
197
  <tr>
198
- <td align="center" width="50%">
199
- <img width="120" height="120" src="https://img.icons8.com/fluent/240/000000/musical-notes.png"/>
200
- <br/>
201
- <img src="https://img.shields.io/badge/Rive-0061ff?style=for-the-badge&logo=discord&logoColor=white" /><br />
202
- <a href="https://discord.com/oauth2/authorize?client_id=1350601402325405806">Add to Discord</a>
203
- </td>
204
- <td align="center" width="50%">
198
+ <td align="center" width="33%">
205
199
  <img width="120" height="120" src="https://img.icons8.com/fluent/240/000000/water-element.png"/>
206
200
  <br/>
207
201
  <img src="https://img.shields.io/badge/Kenium-00bfff?style=for-the-badge&logo=discord&logoColor=white" /><br />
208
202
  <a href="https://discord.com/oauth2/authorize?client_id=1202232935311495209">Add to Discord</a>
209
203
  </td>
204
+ <td align="center" width="33%">
205
+ <img width="120" height="120" src="https://cdn.discordapp.com/attachments/1347414750463660032/1365654298989690930/soya1.jpg?ex=680e182d&is=680cc6ad&hm=3055de34e2af31a3a430f52b147a00215f8b88c8dcc9363cab5359c50ce8d75f&"/>
206
+ <br/>
207
+ <img src="https://img.shields.io/badge/SoyaMusic-22c55e?style=for-the-badge&logo=discord&logoColor=white" /><br />
208
+ <a href="https://discord.com/oauth2/authorize?client_id=997906613082013868&permissions=281357446481&integration_type=0&scope=bot+applications.commands">Add to Discord</a>
209
+ </td>
210
210
  </tr>
211
211
  </table>
212
212
  </div>
@@ -237,8 +237,8 @@ For detailed usage, API references, and examples, check out our official documen
237
237
 
238
238
  | Bot | Invite Link | Features |
239
239
  |-----|-------------|----------|
240
- | Rive | [Add to Discord](https://discord.com/oauth2/authorize?client_id=1350601402325405806) | Music playback, Queue management |
241
240
  | Kenium | [Add to Discord](https://discord.com/oauth2/authorize?client_id=1202232935311495209) | Audio streaming, Discord integration |
241
+ | Soya Music | [Add to Discord](https://discord.com/oauth2/authorize?client_id=997906613082013868&permissions=281357446481&integration_type=0&scope=bot+applications.commands) | Audio streaming, Discord integration |
242
242
 
243
243
  ## 🛠️ Advanced Features
244
244
 
@@ -347,4 +347,4 @@ Join our thriving community of developers and bot creators!
347
347
 
348
348
  <sub>Built with 💙 by the Aqualink Team</sub>
349
349
 
350
- </div>
350
+ </div>
@@ -1,142 +1,114 @@
1
1
  const https = require('https');
2
2
  const crypto = require('crypto');
3
3
 
4
- const agent = new https.Agent({ keepAlive: true, maxSockets: 10 });
4
+ const agent = new https.Agent({
5
+ keepAlive: true,
6
+ maxSockets: 5,
7
+ maxFreeSockets: 2,
8
+ timeout: 8000,
9
+ freeSocketTimeout: 4000
10
+ });
5
11
 
6
12
  const TOTP_SECRET = Buffer.from("5507145853487499592248630329347", 'utf8');
7
13
 
8
- async function quickFetch(url, options = {}, redirectCount = 0) {
9
- const maxRedirects = 5;
10
-
11
- try {
12
- return await new Promise((resolve, reject) => {
13
- const req = https.get(url, { ...options, agent }, async (res) => {
14
- if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
15
- if (redirectCount >= maxRedirects) {
16
- return reject(new Error('Too many redirects'));
17
- }
18
-
19
- res.resume();
20
- try {
21
- const resolved = await quickFetch(
22
- new URL(res.headers.location, url).toString(),
23
- options,
24
- redirectCount + 1
25
- );
26
- resolve(resolved);
27
- } catch (err) {
28
- reject(err);
29
- }
30
- return;
31
- }
32
-
33
- if (res.statusCode !== 200) {
34
- res.resume();
35
- return reject(new Error(`Request failed. Status code: ${res.statusCode}`));
36
- }
37
-
38
- const chunks = [];
39
- let length = 0;
40
-
41
- res.on('data', (chunk) => {
42
- chunks.push(chunk);
43
- length += chunk.length;
44
- });
45
-
46
- res.on('end', () => {
47
- resolve(Buffer.concat(chunks, length).toString());
48
- });
49
- });
50
-
51
- req.on('error', reject);
52
- req.setTimeout(10000, () => {
53
- req.destroy(new Error('Request timeout'));
54
- });
55
- });
56
- } catch (err) {
57
- throw err;
14
+ const SOUNDCLOUD_REGEX = /<a\s+itemprop="url"\s+href="(\/[^"]+)"/g;
15
+
16
+ const shuffleArray = (arr) => {
17
+ for (let i = arr.length - 1; i > 0; i--) {
18
+ const j = Math.random() * (i + 1) | 0;
19
+ [arr[i], arr[j]] = [arr[j], arr[i]];
58
20
  }
59
- }
21
+ return arr;
22
+ };
23
+
24
+ const fastFetch = (url, options = {}) => {
25
+ return new Promise((resolve, reject) => {
26
+ const req = https.get(url, { ...options, agent }, (res) => {
27
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
28
+ res.resume();
29
+ return fastFetch(new URL(res.headers.location, url).href, options)
30
+ .then(resolve, reject);
31
+ }
32
+
33
+ if (res.statusCode !== 200) {
34
+ res.resume();
35
+ return reject(new Error(`HTTP ${res.statusCode}`));
36
+ }
37
+
38
+ const chunks = [];
39
+ res.on('data', chunk => chunks.push(chunk));
40
+ res.on('end', () => resolve(Buffer.concat(chunks).toString()));
41
+ });
42
+
43
+ req.on('error', reject);
44
+ req.setTimeout(8000, () => req.destroy(new Error('Timeout')));
45
+ });
46
+ };
60
47
 
61
- async function soundAutoPlay(baseUrl) {
48
+ const soundAutoPlay = async (baseUrl) => {
62
49
  try {
63
- const html = await quickFetch(`${baseUrl}/recommended`);
64
- const links = new Set();
65
- const regex = /<a\s+itemprop="url"\s+href="(\/[^"]+)"/g;
50
+ const html = await fastFetch(`${baseUrl}/recommended`);
66
51
 
52
+ const links = [];
67
53
  let match;
68
- while ((match = regex.exec(html)) !== null) {
69
- links.add(`https://soundcloud.com${match[1]}`);
70
- }
71
-
72
- if (!links.size) {
73
- throw new Error("No recommended tracks found on SoundCloud.");
54
+ while ((match = SOUNDCLOUD_REGEX.exec(html)) && links.length < 50) {
55
+ links.push(`https://soundcloud.com${match[1]}`);
74
56
  }
75
57
 
76
- const urls = Array.from(links);
77
- for (let i = urls.length - 1; i > 0; i--) {
78
- const j = Math.random() * (i + 1) | 0;
79
- [urls[i], urls[j]] = [urls[j], urls[i]];
80
- }
58
+ if (!links.length) throw new Error("No tracks found");
81
59
 
82
- return urls;
60
+ return shuffleArray(links);
83
61
  } catch (err) {
84
- console.error("Error in SoundCloud autoplay:", err);
62
+ console.error("SoundCloud error:", err.message);
85
63
  return [];
86
64
  }
87
- }
65
+ };
88
66
 
89
- function generateToken() {
90
- const timeStep = Math.floor(Date.now() / 30000);
91
- const counter = Buffer.alloc(8);
92
- counter.writeBigInt64BE(BigInt(timeStep));
67
+ const generateToken = () => {
68
+ const timeStep = (Date.now() / 30000) | 0;
69
+ const counter = Buffer.allocUnsafe(8);
70
+ counter.writeBigUInt64BE(BigInt(timeStep), 0);
93
71
 
94
- const hmac = crypto.createHmac('sha1', TOTP_SECRET);
95
- hmac.update(counter);
96
- const hash = hmac.digest();
97
- const offset = hash[hash.length - 1] & 0x0f;
72
+ const hash = crypto.createHmac('sha1', TOTP_SECRET).update(counter).digest();
73
+ const offset = hash[19] & 0x0f;
98
74
 
99
75
  const binCode = (
100
- (hash[offset] << 24) |
101
- (hash[offset + 1] << 16) |
102
- (hash[offset + 2] << 8) |
76
+ (hash[offset] & 0x7f) << 24 |
77
+ hash[offset + 1] << 16 |
78
+ hash[offset + 2] << 8 |
103
79
  hash[offset + 3]
104
- ) & 0x7fffffff;
80
+ );
105
81
 
106
- const token = (binCode % 1000000).toString().padStart(6, '0');
107
- return [token, timeStep * 30000];
108
- }
82
+ return [
83
+ (binCode % 1000000).toString().padStart(6, '0'),
84
+ timeStep * 30000
85
+ ];
86
+ };
109
87
 
110
- async function spotifyAutoPlay(seedTrackId) {
88
+ const spotifyAutoPlay = async (seedTrackId) => {
111
89
  const [totp, ts] = generateToken();
112
- const params = new URLSearchParams({
113
- reason: "transport",
114
- productType: "embed",
115
- totp,
116
- totpVer: "5",
117
- ts: ts.toString()
118
- });
119
-
90
+
120
91
  try {
121
- const tokenData = await quickFetch(`https://open.spotify.com/get_access_token?${params}`);
122
- const { accessToken } = JSON.parse(tokenData);
92
+ const tokenUrl = `https://open.spotify.com/api/token?reason=init&productType=embed&totp=${totp}&totpVer=5&ts=${ts}`;
93
+ const tokenResponse = await fastFetch(tokenUrl);
94
+ const { accessToken } = JSON.parse(tokenResponse);
123
95
 
124
- if (!accessToken) throw new Error("Invalid access token");
96
+ if (!accessToken) throw new Error("No access token");
125
97
 
126
- const recData = await quickFetch(
127
- `https://api.spotify.com/v1/recommendations?limit=10&seed_tracks=${seedTrackId}`,
128
- { headers: { Authorization: `Bearer ${accessToken}` } }
129
- );
98
+ const recUrl = `https://api.spotify.com/v1/recommendations?limit=10&seed_tracks=${seedTrackId}`;
99
+ const recResponse = await fastFetch(recUrl, {
100
+ headers: { Authorization: `Bearer ${accessToken}` }
101
+ });
130
102
 
131
- const { tracks } = JSON.parse(recData);
132
- if (!tracks?.length) throw new Error("No tracks found");
103
+ const { tracks } = JSON.parse(recResponse);
104
+ if (!tracks?.length) throw new Error("No tracks");
133
105
 
134
106
  return tracks[Math.random() * tracks.length | 0].id;
135
107
  } catch (err) {
136
- console.error("Spotify autoplay error:", err);
108
+ console.error("Spotify error:", err.message);
137
109
  throw err;
138
110
  }
139
- }
111
+ };
140
112
 
141
113
  module.exports = {
142
114
  scAutoPlay: soundAutoPlay,
@@ -4,16 +4,17 @@ const Player = require("./Player");
4
4
  const Track = require("./Track");
5
5
  const { version: pkgVersion } = require("../../package.json");
6
6
  const { EventEmitter } = require('tseep');
7
+ const fs = require('fs-extra');
7
8
 
8
9
  const URL_REGEX = /^https?:\/\//;
9
10
  const DEFAULT_OPTIONS = Object.freeze({
10
- shouldDeleteMessage: false,
11
- defaultSearchPlatform: 'ytsearch',
12
- leaveOnEnd: true,
13
- restVersion: 'v4',
14
- plugins: [],
15
- autoResume: false,
16
- infiniteReconnects: false
11
+ shouldDeleteMessage: false,
12
+ defaultSearchPlatform: 'ytsearch',
13
+ leaveOnEnd: true,
14
+ restVersion: 'v4',
15
+ plugins: [],
16
+ autoResume: false,
17
+ infiniteReconnects: false
17
18
  });
18
19
  const LEAST_USED_CACHE_TTL = 50;
19
20
 
@@ -32,9 +33,9 @@ class Aqua extends EventEmitter {
32
33
  this.clientId = null;
33
34
  this.initiated = false;
34
35
  this.version = pkgVersion;
35
-
36
+
36
37
  this.options = Object.assign({}, DEFAULT_OPTIONS, options);
37
-
38
+
38
39
  const {
39
40
  shouldDeleteMessage,
40
41
  defaultSearchPlatform,
@@ -45,7 +46,7 @@ class Aqua extends EventEmitter {
45
46
  infiniteReconnects,
46
47
  send
47
48
  } = this.options;
48
-
49
+
49
50
  this.shouldDeleteMessage = shouldDeleteMessage;
50
51
  this.defaultSearchPlatform = defaultSearchPlatform;
51
52
  this.leaveOnEnd = leaveOnEnd;
@@ -53,9 +54,9 @@ class Aqua extends EventEmitter {
53
54
  this.plugins = plugins;
54
55
  this.autoResume = autoResume;
55
56
  this.infiniteReconnects = infiniteReconnects;
56
-
57
+
57
58
  this.send = send || this.defaultSendFunction.bind(this);
58
-
59
+
59
60
  this._leastUsedCache = { nodes: [], timestamp: 0 };
60
61
  }
61
62
 
@@ -69,14 +70,14 @@ class Aqua extends EventEmitter {
69
70
  if (now - this._leastUsedCache.timestamp < LEAST_USED_CACHE_TTL) {
70
71
  return this._leastUsedCache.nodes;
71
72
  }
72
-
73
+
73
74
  const connectedNodes = [];
74
75
  for (const node of this.nodeMap.values()) {
75
76
  if (node.connected) connectedNodes.push(node);
76
77
  }
77
-
78
+
78
79
  connectedNodes.sort((a, b) => a.rest.calls - b.rest.calls);
79
-
80
+
80
81
  this._leastUsedCache = { nodes: connectedNodes, timestamp: now };
81
82
  return connectedNodes;
82
83
  }
@@ -84,35 +85,35 @@ class Aqua extends EventEmitter {
84
85
  async init(clientId) {
85
86
  if (this.initiated) return this;
86
87
  this.clientId = clientId;
87
-
88
+
88
89
  try {
89
90
  const nodePromises = [];
90
91
  for (const node of this.nodes) {
91
92
  nodePromises.push(this.createNode(node));
92
93
  }
93
94
  await Promise.all(nodePromises);
94
-
95
+
95
96
  for (const plugin of this.plugins) {
96
97
  plugin.load(this);
97
98
  }
98
-
99
+
99
100
  this.initiated = true;
100
101
  } catch (error) {
101
102
  this.initiated = false;
102
103
  throw error;
103
104
  }
104
-
105
+
105
106
  return this;
106
107
  }
107
108
 
108
109
  async createNode(options) {
109
110
  const nodeId = options.name || options.host;
110
111
  this.destroyNode(nodeId);
111
-
112
+
112
113
  const node = new Node(this, options, this.options);
113
114
  this.nodeMap.set(nodeId, node);
114
115
  this._leastUsedCache.timestamp = 0;
115
-
116
+
116
117
  try {
117
118
  await node.connect();
118
119
  this.emit("nodeCreate", node);
@@ -127,7 +128,7 @@ class Aqua extends EventEmitter {
127
128
  destroyNode(identifier) {
128
129
  const node = this.nodeMap.get(identifier);
129
130
  if (!node) return;
130
-
131
+
131
132
  node.destroy();
132
133
  this.nodeMap.delete(identifier);
133
134
  this._leastUsedCache.timestamp = 0;
@@ -137,14 +138,14 @@ class Aqua extends EventEmitter {
137
138
  updateVoiceState({ d, t }) {
138
139
  const player = this.players.get(d.guild_id);
139
140
  if (!player) return;
140
-
141
+
141
142
  if (t === "VOICE_SERVER_UPDATE" || (t === "VOICE_STATE_UPDATE" && d.user_id === this.clientId)) {
142
143
  if (t === "VOICE_SERVER_UPDATE") {
143
144
  player.connection?.setServerUpdate?.(d);
144
145
  } else {
145
146
  player.connection?.setStateUpdate?.(d);
146
147
  }
147
-
148
+
148
149
  if (d.channel_id === null) {
149
150
  this.cleanupPlayer(player);
150
151
  }
@@ -153,23 +154,23 @@ class Aqua extends EventEmitter {
153
154
 
154
155
  fetchRegion(region) {
155
156
  if (!region) return this.leastUsedNodes;
156
-
157
+
157
158
  const lowerRegion = region.toLowerCase();
158
159
  const regionNodes = [];
159
-
160
+
160
161
  for (const node of this.nodeMap.values()) {
161
162
  if (node.connected && node.regions?.includes(lowerRegion)) {
162
163
  regionNodes.push(node);
163
164
  }
164
165
  }
165
-
166
+
166
167
  const loadCache = new Map();
167
168
  regionNodes.sort((a, b) => {
168
169
  if (!loadCache.has(a)) loadCache.set(a, this.calculateLoad(a));
169
170
  if (!loadCache.has(b)) loadCache.set(b, this.calculateLoad(b));
170
171
  return loadCache.get(a) - loadCache.get(b);
171
172
  });
172
-
173
+
173
174
  return regionNodes;
174
175
  }
175
176
 
@@ -182,27 +183,28 @@ class Aqua extends EventEmitter {
182
183
 
183
184
  createConnection(options) {
184
185
  if (!this.initiated) throw new Error("Aqua must be initialized before this operation");
185
-
186
+
186
187
  const existingPlayer = this.players.get(options.guildId);
187
188
  if (existingPlayer && existingPlayer.voiceChannel) return existingPlayer;
188
-
189
+
189
190
  const availableNodes = options.region ? this.fetchRegion(options.region) : this.leastUsedNodes;
190
191
  const node = availableNodes[0];
191
192
  if (!node) throw new Error("No nodes are available");
192
-
193
+
193
194
  return this.createPlayer(node, options);
194
195
  }
195
196
 
196
197
  createPlayer(node, options) {
197
198
  this.destroyPlayer(options.guildId);
198
-
199
+
199
200
  const player = new Player(this, node, options);
200
201
  this.players.set(options.guildId, player);
201
-
202
+
202
203
  player.on("destroy", () => {
203
204
  this.players.delete(options.guildId);
205
+ this.emit("playerDestroy", player);
204
206
  });
205
-
207
+
206
208
  player.connect(options);
207
209
  this.emit("playerCreate", player);
208
210
  return player;
@@ -211,7 +213,7 @@ class Aqua extends EventEmitter {
211
213
  async destroyPlayer(guildId) {
212
214
  const player = this.players.get(guildId);
213
215
  if (!player) return;
214
-
216
+
215
217
  try {
216
218
  await player.clearData();
217
219
  player.removeAllListeners();
@@ -224,18 +226,18 @@ class Aqua extends EventEmitter {
224
226
 
225
227
  async resolve({ query, source = this.defaultSearchPlatform, requester, nodes }) {
226
228
  if (!this.initiated) throw new Error("Aqua must be initialized before this operation");
227
-
229
+
228
230
  const requestNode = this.getRequestNode(nodes);
229
231
  const formattedQuery = URL_REGEX.test(query) ? query : `${source}:${query}`;
230
-
232
+
231
233
  try {
232
234
  const endpoint = `/v4/loadtracks?identifier=${encodeURIComponent(formattedQuery)}`;
233
235
  const response = await requestNode.rest.makeRequest("GET", endpoint);
234
-
236
+
235
237
  if (["empty", "NO_MATCHES"].includes(response.loadType)) {
236
238
  return await this.handleNoMatches(query);
237
239
  }
238
-
240
+
239
241
  return this.constructResponse(response, requester, requestNode);
240
242
  } catch (error) {
241
243
  if (error.name === "AbortError") {
@@ -244,19 +246,19 @@ class Aqua extends EventEmitter {
244
246
  throw new Error(`Failed to resolve track: ${error.message}`);
245
247
  }
246
248
  }
247
-
249
+
248
250
  getRequestNode(nodes) {
249
251
  if (!nodes) return this.leastUsedNodes[0];
250
-
252
+
251
253
  if (nodes instanceof Node) return nodes;
252
254
  if (typeof nodes === "string") {
253
255
  const mappedNode = this.nodeMap.get(nodes);
254
256
  return mappedNode || this.leastUsedNodes[0];
255
257
  }
256
-
258
+
257
259
  throw new TypeError(`'nodes' must be a string or Node instance, received: ${typeof nodes}`);
258
260
  }
259
-
261
+
260
262
  async handleNoMatches(query) {
261
263
  return {
262
264
  loadType: "empty",
@@ -266,8 +268,8 @@ class Aqua extends EventEmitter {
266
268
  tracks: []
267
269
  };
268
270
  }
269
-
270
- constructResponse(response, requester, requestNode) {
271
+
272
+ async constructResponse(response, requester, requestNode) {
271
273
  const baseResponse = {
272
274
  loadType: response.loadType,
273
275
  exception: null,
@@ -275,28 +277,29 @@ class Aqua extends EventEmitter {
275
277
  pluginInfo: response.pluginInfo ?? {},
276
278
  tracks: []
277
279
  };
278
-
280
+
279
281
  if (response.loadType === "error" || response.loadType === "LOAD_FAILED") {
280
282
  baseResponse.exception = response.data ?? response.exception;
281
283
  return baseResponse;
282
284
  }
283
-
285
+
284
286
  const trackFactory = (trackData) => new Track(trackData, requester, requestNode);
285
-
287
+
286
288
  switch (response.loadType) {
287
289
  case "track":
288
290
  if (response.data) {
289
291
  baseResponse.tracks.push(trackFactory(response.data));
290
292
  }
291
293
  break;
292
-
293
294
  case "playlist": {
294
295
  const info = response.data?.info;
295
296
  if (info) {
296
- baseResponse.playlistInfo = {
297
+ const playlistInfo = {
297
298
  name: info.name ?? info.title,
299
+ thumbnail: response.data.pluginInfo?.artworkUrl ?? (response.data.tracks?.[0]?.info?.artworkUrl || null),
298
300
  ...info
299
301
  };
302
+ baseResponse.playlistInfo = playlistInfo;
300
303
  }
301
304
 
302
305
  const tracks = response.data?.tracks;
@@ -322,7 +325,7 @@ class Aqua extends EventEmitter {
322
325
  break;
323
326
  }
324
327
  }
325
-
328
+
326
329
  return baseResponse;
327
330
  }
328
331
 
@@ -334,7 +337,7 @@ class Aqua extends EventEmitter {
334
337
 
335
338
  async search(query, requester, source = this.defaultSearchPlatform) {
336
339
  if (!query || !requester) return null;
337
-
340
+
338
341
  try {
339
342
  const { tracks } = await this.resolve({ query, source, requester });
340
343
  return tracks || null;
@@ -344,21 +347,94 @@ class Aqua extends EventEmitter {
344
347
  }
345
348
  }
346
349
 
347
- async cleanupPlayer(player) {
348
- if (!player) return;
349
-
350
+ async savePlayer(filePath = "./AquaPlayers.json") {
351
+ const data = Array.from(this.players.values()).map(player => ({
352
+ guildId: player.guildId,
353
+ textChannel: player.textChannel,
354
+ voiceChannel: player.voiceChannel,
355
+ track: player.current ? {
356
+ identifier: player.current.identifier,
357
+ author: player.current.author,
358
+ title: player.current.title,
359
+ uri: player.current.uri,
360
+ sourceName: player.current.sourceName,
361
+ artworkUrl: player.current.artworkUrl,
362
+ duration: player.current.duration,
363
+ position: player.position,
364
+ } : null,
365
+ requester: player.requester || player.current?.requester,
366
+ volume: player.volume,
367
+ paused: player.paused
368
+ }));
369
+ console.log(`Saving ${data.length} players to ${filePath}`);
370
+ await fs.writeJSON(filePath, data, { spaces: 2 });
371
+ this.emit("debug", "Aqua", `Saved players to ${filePath}`);
372
+ }
373
+
374
+ async waitForFirstNode() {
375
+ if (this.leastUsedNodes.length > 0) return;
376
+ return new Promise(resolve => {
377
+ const check = () => {
378
+ if (this.leastUsedNodes.length > 0) {
379
+ resolve();
380
+ } else {
381
+ setTimeout(check, 100);
382
+ }
383
+ };
384
+ check();
385
+ });
386
+ }
387
+
388
+ async loadPlayers(filePath = "./AquaPlayers.json") {
389
+ if (!fs.existsSync(filePath)) {
390
+ this.emit("debug", "Aqua", `No player data found at ${filePath}`);
391
+ return;
392
+ }
350
393
  try {
351
- if (player.connection) {
352
- try {
353
- await player.destroy()
354
- player.connection = null;
355
- } catch (error) {
356
- console.error(`Error disconnecting player connection: ${error.message}`);
394
+ await this.waitForFirstNode();
395
+ const data = await fs.readJSON(filePath);
396
+ for (const playerData of data) {
397
+ const { guildId, textChannel, voiceChannel, track, volume, paused, requester } = playerData;
398
+ let player = this.players.get(guildId);
399
+
400
+ if (!player) {
401
+ player = await this.createConnection({
402
+ guildId: guildId,
403
+ textChannel: textChannel,
404
+ voiceChannel: voiceChannel,
405
+ defaultVolume: volume || 65,
406
+ deaf: true
407
+ });
408
+ }
409
+
410
+ if (track && player) {
411
+ const resolved = await this.resolve({ query: track.uri, requester });
412
+ if (resolved.tracks && resolved.tracks.length > 0) {
413
+ player.queue.add(resolved.tracks[0]);
414
+ player.position = track.position || 0;
415
+ } else {
416
+ this.emit("debug", "Aqua", `Could not resolve track for guild ${guildId}: ${track.uri}`);
417
+ }
418
+ }
419
+
420
+ if (player) {
421
+ player.paused = paused || false;
422
+ if (!player.playing && !player.paused && player.queue.size > 0) {
423
+ player.play();
424
+ }
357
425
  }
358
426
  }
359
-
360
- this.players.delete(player.guildId);
361
- this.emit("playerCleanup", player.guildId);
427
+ this.emit("debug", "Aqua", `Loaded players from ${filePath}`);
428
+ } catch (error) {
429
+ console.error(`Failed to load players from ${filePath}:`, error);
430
+ }
431
+ }
432
+
433
+ async cleanupPlayer(player) {
434
+ if (!player) return;
435
+
436
+ try {
437
+ await player.destroy();
362
438
  } catch (error) {
363
439
  console.error(`Error during player cleanup: ${error.message}`);
364
440
  }
@@ -96,7 +96,7 @@ class Node {
96
96
  this.aqua.emit("nodeConnected", this);
97
97
 
98
98
  if (this.autoResume && this.sessionId) {
99
- await this.resumePlayers();
99
+ await this.aqua.loadPlayers();
100
100
  }
101
101
  } catch (err) {
102
102
  this.info = null;
@@ -245,13 +245,33 @@ class Node {
245
245
  }
246
246
 
247
247
  async getStats() {
248
- if (this.connected) {
248
+
249
+ if (this.connected && this.stats) {
249
250
  return this.stats;
250
251
  }
251
252
 
252
253
  try {
253
254
  const newStats = await this.rest.getStats();
254
- Object.assign(this.stats, newStats);
255
+ if (newStats && this.stats) {
256
+ this.stats.players = newStats.players ?? this.stats.players;
257
+ this.stats.playingPlayers = newStats.playingPlayers ?? this.stats.playingPlayers;
258
+ this.stats.uptime = newStats.uptime ?? this.stats.uptime;
259
+ this.stats.ping = newStats.ping ?? this.stats.ping;
260
+
261
+ if (newStats.memory) {
262
+ Object.assign(this.stats.memory, newStats.memory);
263
+ this._calculateMemoryPercentages();
264
+ }
265
+
266
+ if (newStats.cpu) {
267
+ Object.assign(this.stats.cpu, newStats.cpu);
268
+ this._calculateCpuPercentages();
269
+ }
270
+
271
+ if (newStats.frameStats) {
272
+ Object.assign(this.stats.frameStats, newStats.frameStats);
273
+ }
274
+ }
255
275
  return this.stats;
256
276
  } catch (err) {
257
277
  this.emitError(`Failed to fetch node stats: ${err.message}`);
@@ -269,13 +289,12 @@ class Node {
269
289
 
270
290
  if (payload.memory) {
271
291
  Object.assign(this.stats.memory, payload.memory);
272
- this.stats.memory.freePercentage = (this.stats.memory.free / this.stats.memory.allocated) * 100;
273
- this.stats.memory.usedPercentage = (this.stats.memory.used / this.stats.memory.allocated) * 100;
292
+ this._calculateMemoryPercentages();
274
293
  }
275
294
 
276
295
  if (payload.cpu) {
277
296
  Object.assign(this.stats.cpu, payload.cpu);
278
- this.stats.cpu.lavalinkLoadPercentage = (this.stats.cpu.lavalinkLoad / this.stats.cpu.cores) * 100;
297
+ this._calculateCpuPercentages();
279
298
  }
280
299
 
281
300
  if (payload.frameStats) {
@@ -283,6 +302,21 @@ class Node {
283
302
  }
284
303
  }
285
304
 
305
+ _calculateMemoryPercentages() {
306
+ const { memory } = this.stats;
307
+ if (memory.allocated > 0) {
308
+ memory.freePercentage = (memory.free / memory.allocated) * 100;
309
+ memory.usedPercentage = (memory.used / memory.allocated) * 100;
310
+ }
311
+ }
312
+
313
+ _calculateCpuPercentages() {
314
+ const { cpu } = this.stats;
315
+ if (cpu.cores > 0) {
316
+ cpu.lavalinkLoadPercentage = (cpu.lavalinkLoad / cpu.cores) * 100;
317
+ }
318
+ }
319
+
286
320
  _handleReadyOp(payload) {
287
321
  if (!payload.sessionId) {
288
322
  this.emitError("Ready payload missing sessionId");
@@ -297,10 +331,7 @@ class Node {
297
331
 
298
332
  async resumePlayers() {
299
333
  try {
300
- await this.rest.makeRequest("PATCH", `/v4/sessions/${this.sessionId}`, {
301
- resuming: true,
302
- timeout: this.resumeTimeout
303
- });
334
+ await this.aqua.loadPlayers();
304
335
 
305
336
  this.aqua.emit("debug", this.name, "Session resumed successfully");
306
337
  } catch (err) {
@@ -58,13 +58,13 @@ class Player extends EventEmitter {
58
58
  this._updateTimeout = null;
59
59
  this._dataStore = new Map();
60
60
 
61
- this.on("playerUpdate", (state) => {
62
- if (state) {
63
- this.position = state.position ?? this.position;
64
- this.timestamp = state.timestamp ?? this.timestamp;
65
- this.ping = state.ping ?? this.ping;
66
- this.aqua.emit("playerUpdate", this, { state });
67
- }
61
+ this.on("playerUpdate", (packet) => {
62
+ this.position = packet.state.position;
63
+ this.connected = packet.state.connected;
64
+ this.ping = packet.state.ping;
65
+ this.timestamp = packet.state.timestamp;
66
+
67
+ this.aqua.emit("playerUpdate", this, packet);
68
68
  });
69
69
 
70
70
  this.on("event", async (payload) => {
@@ -185,18 +185,18 @@ class Player extends EventEmitter {
185
185
  return this.batchUpdatePlayer({ track: { encoded: this.current.track } }, true);
186
186
  }
187
187
 
188
- connect({ deaf = true, mute = false } = {}) {
188
+ connect(options = this) {
189
+ const { guildId, voiceChannel, deaf = true, mute = false } = options;
189
190
  this.deaf = deaf;
190
191
  this.mute = mute;
191
- const payload = {
192
- guild_id: this.guildId,
193
- channel_id: this.voiceChannel,
192
+ this.send({
193
+ guild_id: guildId,
194
+ channel_id: voiceChannel,
194
195
  self_deaf: deaf,
195
- self_mute: mute
196
- };
197
- this.send(payload);
196
+ self_mute: mute,
197
+ });
198
198
  this.connected = true;
199
- this.aqua.emit("debug", this.guildId, `Player connected to voice channel: ${this.voiceChannel}.`);
199
+ this.aqua.emit("debug", guildId, `Player connected to voice channel: ${voiceChannel}.`);
200
200
  return this;
201
201
  }
202
202
 
@@ -289,7 +289,13 @@ class Player extends EventEmitter {
289
289
  if (!channel?.length) throw new TypeError("Channel must be a non-empty string.");
290
290
  if (this.connected && channel === this.voiceChannel) throw new ReferenceError(`Player already connected to ${channel}.`);
291
291
  this.voiceChannel = channel;
292
- this.connect({ deaf: this.deaf, mute: this.mute });
292
+ this.connect({
293
+ deaf: this.deaf,
294
+ guildId: this.guildId,
295
+ voiceChannel: this.voiceChannel,
296
+ textChannel: this.textChannel,
297
+ mute: this.mute,
298
+ });
293
299
  return this;
294
300
  }
295
301
 
@@ -21,7 +21,7 @@ class Rest {
21
21
  this.secure = secure;
22
22
  this.timeout = timeout;
23
23
 
24
- this.client = secure ? http2 || https : http;
24
+ this.client = secure ? https || http2 : http;
25
25
  }
26
26
 
27
27
  setSessionId(sessionId) {
@@ -10,6 +10,7 @@ class Track {
10
10
  this.identifier = info.identifier || '';
11
11
  this.isSeekable = Boolean(info.isSeekable);
12
12
  this.author = info.author || '';
13
+ this.position = info.position || 0;
13
14
  this.length = info.length || 0;
14
15
  this.duration = info.length || 0;
15
16
  this.isStream = Boolean(info.isStream);
@@ -29,6 +30,7 @@ class Track {
29
30
  return {
30
31
  identifier: this.identifier,
31
32
  isSeekable: this.isSeekable,
33
+ position: this.position,
32
34
  author: this.author,
33
35
  length: this.length,
34
36
  isStream: this.isStream,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aqualink",
3
- "version": "2.6.1",
3
+ "version": "2.6.2",
4
4
  "description": "An Lavalink client, focused in pure performance and features",
5
5
  "main": "build/index.js",
6
6
  "types": "index.d.ts",
@@ -42,6 +42,7 @@
42
42
  },
43
43
  "dependencies": {
44
44
  "ws": "^8.18.2",
45
+ "fs-extra": "^11.3.0",
45
46
  "tseep": "^1.3.1"
46
47
  },
47
48
  "maintainers": [