aqualink 2.3.0 → 2.3.1

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,95 +1,115 @@
1
1
  const https = require('https');
2
+ const crypto = require('crypto');
2
3
 
3
- function fetch(url, options = {}) {
4
+ function quickFetch(url, options = {}) {
4
5
  return new Promise((resolve, reject) => {
5
- const req = https.get(url, options, (res) => {
6
+ const req = https.get(url, options, res => {
6
7
  if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
7
- return fetch(res.headers.location, options).then(resolve).catch(reject);
8
+ return resolve(quickFetch(res.headers.location, options));
8
9
  }
9
-
10
10
  if (res.statusCode !== 200) {
11
- res.resume();
12
- reject(new Error(`Request failed. Status code: ${res.statusCode}`));
13
- return;
11
+ res.resume();
12
+ return reject(new Error(`Request failed. Status code: ${res.statusCode}`));
14
13
  }
15
-
16
14
  const chunks = [];
17
15
  res.on('data', chunk => chunks.push(chunk));
18
16
  res.on('end', () => resolve(Buffer.concat(chunks).toString()));
19
17
  });
20
-
18
+ req.on('error', reject);
21
19
  req.setTimeout(10000, () => {
22
20
  req.destroy();
23
21
  reject(new Error('Request timeout'));
24
22
  });
25
-
26
- req.on('error', err => {
27
- reject(err);
28
- });
29
-
30
23
  req.end();
31
24
  });
32
25
  }
33
26
 
34
- async function scAutoPlay(url) {
27
+ async function soundAutoPlay(baseUrl) {
35
28
  try {
36
- const html = await fetch(`${url}/recommended`);
37
-
38
- const regex = /<a itemprop="url" href="(\/.*?)"/g;
39
- const hrefs = new Set();
29
+ const html = await quickFetch(`${baseUrl}/recommended`);
30
+ const regex = /<a\s+itemprop="url"\s+href="(\/[^"]+)"/g;
31
+ const links = new Set();
40
32
  let match;
41
-
42
33
  while ((match = regex.exec(html)) !== null) {
43
- hrefs.add(`https://soundcloud.com${match[1]}`);
34
+ links.add(`https://soundcloud.com${match[1]}`);
44
35
  }
45
-
46
- if (hrefs.size === 0) {
36
+ if (!links.size) {
47
37
  throw new Error("No recommended tracks found on SoundCloud.");
48
38
  }
49
-
50
- const shuffledHrefs = Array.from(hrefs);
51
- for (let i = shuffledHrefs.length - 1; i > 0; i--) {
39
+ const urls = Array.from(links);
40
+ for (let i = urls.length - 1; i > 0; i--) {
52
41
  const j = Math.floor(Math.random() * (i + 1));
53
- [shuffledHrefs[i], shuffledHrefs[j]] = [shuffledHrefs[j], shuffledHrefs[i]];
42
+ [urls[i], urls[j]] = [urls[j], urls[i]];
54
43
  }
55
-
56
- return shuffledHrefs;
57
- } catch (error) {
58
- console.error("Error fetching SoundCloud recommendations:", error);
44
+ return urls;
45
+ } catch (err) {
46
+ console.error("Error in SoundCloud autoplay:", err);
59
47
  return [];
60
48
  }
61
49
  }
62
50
 
63
- async function spAutoPlay(track_id) {
51
+ function gerenateToKen() {
52
+ const totpSecret = Buffer.from(new Uint8Array([
53
+ 53, 53, 48, 55, 49, 52, 53, 56, 53, 51, 52, 56, 55, 52, 57, 57,
54
+ 53, 57, 50, 50, 52, 56, 54, 51, 48, 51, 50, 57, 51, 52, 55
55
+ ]));
56
+
57
+ // Note for me: Can also be used from Buffer.from("5507145853487499592248630329347", 'utf8');
58
+
59
+ const timeStep = Math.floor(Date.now() / 30000);
60
+ const counter = Buffer.alloc(8);
61
+ counter.writeBigInt64BE(BigInt(timeStep));
62
+
63
+ const hmac = crypto.createHmac('sha1', totpSecret);
64
+ hmac.update(counter);
65
+ const hash = hmac.digest();
66
+ const offset = hash[hash.length - 1] & 0x0f;
67
+ const binCode =
68
+ ((hash[offset] & 0x7f) << 24) |
69
+ ((hash[offset + 1] & 0xff) << 16) |
70
+ ((hash[offset + 2] & 0xff) << 8) |
71
+ (hash[offset + 3] & 0xff);
72
+ const token = (binCode % 1000000).toString().padStart(6, '0');
73
+ return [token, timeStep * 30000];
74
+ }
75
+
76
+ async function spotifyAutoPlay(seedTrackId) {
77
+ const [totp, ts] = gerenateToKen();
78
+ const params = new URLSearchParams({
79
+ reason: "transport",
80
+ productType: "embed",
81
+ totp,
82
+ totpVer: "5",
83
+ ts: ts.toString()
84
+ });
85
+ const tokenUrl = `https://open.spotify.com/get_access_token?${params.toString()}`;
86
+ const tokenData = await quickFetch(tokenUrl);
87
+
88
+ let accessToken;
64
89
  try {
65
- const tokenResponse = await fetch("https://open.spotify.com/get_access_token?reason=transport&productType=embed");
66
- const tokenData = JSON.parse(tokenResponse);
67
- const accessToken = tokenData?.accessToken;
68
-
69
- if (!accessToken) throw new Error("Failed to retrieve Spotify access token");
70
-
71
- const recommendationsResponse = await fetch(
72
- `https://api.spotify.com/v1/recommendations?limit=5&seed_tracks=${track_id}&fields=tracks.id`,
73
- {
74
- headers: {
75
- Authorization: `Bearer ${accessToken}`,
76
- 'Content-Type': 'application/json'
77
- }
78
- }
79
- );
80
-
81
- const data = JSON.parse(recommendationsResponse);
82
- const tracks = data?.tracks || [];
83
-
84
- if (tracks.length === 0) {
85
- throw new Error("No recommended tracks found on Spotify.");
90
+ accessToken = JSON.parse(tokenData).accessToken;
91
+ } catch {
92
+ throw new Error("Failed to retrieve Spotify access token.");
93
+ }
94
+
95
+ const recUrl = `https://api.spotify.com/v1/recommendations?limit=10&seed_tracks=${seedTrackId}`;
96
+ const recData = await quickFetch(recUrl, {
97
+ headers: {
98
+ Authorization: `Bearer ${accessToken}`,
99
+ 'Content-Type': 'application/json'
86
100
  }
87
-
88
- return tracks[Math.floor(Math.random() * tracks.length)].id;
89
- } catch (error) {
90
- console.error("Error fetching Spotify recommendations:", error);
91
- return null;
101
+ });
102
+
103
+ let tracks;
104
+ try {
105
+ tracks = JSON.parse(recData).tracks;
106
+ } catch {
107
+ throw new Error("Failed to parse Spotify recommendations.");
92
108
  }
109
+ return tracks[Math.floor(Math.random() * tracks.length)].id;
93
110
  }
94
111
 
95
- module.exports = { scAutoPlay, spAutoPlay };
112
+ module.exports = {
113
+ scAutoPlay: soundAutoPlay,
114
+ spAutoPlay: spotifyAutoPlay
115
+ };
@@ -26,24 +26,24 @@ class Player extends EventEmitter {
26
26
 
27
27
  constructor(aqua, nodes, options = {}) {
28
28
  super();
29
-
29
+
30
30
  this.aqua = aqua;
31
31
  this.nodes = nodes;
32
32
  this.guildId = options.guildId;
33
33
  this.textChannel = options.textChannel;
34
34
  this.voiceChannel = options.voiceChannel;
35
-
35
+
36
36
  this.connection = new Connection(this);
37
37
  this.filters = new Filters(this);
38
-
38
+
39
39
  this.volume = Math.max(0, Math.min(options.defaultVolume ?? 100, 200));
40
40
  this.loop = Player.validModes.has(options.loop) ? options.loop : Player.LOOP_MODES.NONE;
41
-
41
+
42
42
  this.queue = new Queue();
43
43
  this.previousTracks = new Array(50);
44
44
  this.previousTracksIndex = 0;
45
45
  this.previousTracksCount = 0;
46
-
46
+
47
47
  this.shouldDeleteMessage = Boolean(options.shouldDeleteMessage);
48
48
  this.leaveOnEnd = Boolean(options.leaveOnEnd);
49
49
 
@@ -57,43 +57,46 @@ class Player extends EventEmitter {
57
57
  this.nowPlayingMessage = null;
58
58
  this.isAutoplayEnabled = false;
59
59
  this.isAutoplay = false;
60
-
60
+
61
61
  this._boundHandlers = {
62
62
  playerUpdate: this._handlePlayerUpdate.bind(this),
63
63
  event: this._handleEvent.bind(this)
64
64
  };
65
-
65
+
66
66
  this.on("playerUpdate", this._boundHandlers.playerUpdate);
67
67
  this.on("event", this._boundHandlers.event);
68
-
68
+
69
69
  this._dataStore = null;
70
70
  }
71
71
 
72
72
  get previous() {
73
- return this.previousTracksCount > 0 ? this.previousTracks[this.previousTracksIndex] : null;
73
+ if (this.previousTracksCount === 0) return null;
74
+
75
+ const previousIndex = (this.previousTracksIndex - 1 + 50) % 50;
76
+ return this.previousTracks[previousIndex];
74
77
  }
75
-
78
+
76
79
  get currenttrack() {
77
80
  return this.current;
78
81
  }
79
82
 
80
83
  async autoplay(player) {
81
84
  if (!player) throw new Error("Quick Fix: player.autoplay(player)");
82
-
85
+
83
86
  if (!this.isAutoplayEnabled) {
84
87
  this.aqua.emit("debug", this.guildId, "Autoplay is disabled.");
85
88
  return this;
86
89
  }
87
-
90
+
88
91
  this.isAutoplay = true;
89
92
  if (!this.previous) return this;
90
-
93
+
91
94
  try {
92
95
  const { sourceName, identifier, uri, requester } = this.previous.info;
93
96
  this.aqua.emit("debug", this.guildId, `Attempting autoplay for ${sourceName}`);
94
-
97
+
95
98
  let query, source, response;
96
-
99
+
97
100
  const sourceHandlers = {
98
101
  youtube: async () => {
99
102
  return {
@@ -118,51 +121,51 @@ class Player extends EventEmitter {
118
121
  };
119
122
  }
120
123
  };
121
-
124
+
122
125
  const handler = sourceHandlers[sourceName];
123
126
  if (!handler) return this;
124
-
127
+
125
128
  const result = await handler();
126
129
  if (!result) return this;
127
-
130
+
128
131
  ({ query, source } = result);
129
-
132
+
130
133
  response = await this.aqua.resolve({ query, source, requester });
131
-
134
+
132
135
  if (!response?.tracks?.length || ["error", "empty", "LOAD_FAILED", "NO_MATCHES"].includes(response.loadType)) {
133
136
  return this.stop();
134
137
  }
135
-
138
+
136
139
  const track = response.tracks[Math.floor(Math.random() * response.tracks.length)];
137
-
140
+
138
141
  if (!track?.info?.title) {
139
142
  throw new Error("Invalid track object: missing title or info.");
140
143
  }
141
-
144
+
142
145
  track.requester = this.previous.requester || { id: "Unknown" };
143
-
146
+
144
147
  this.queue.push(track);
145
148
  await this.play();
146
-
149
+
147
150
  return this;
148
151
  } catch (error) {
149
152
  console.error("Autoplay error:", error);
150
153
  return this.stop();
151
154
  }
152
155
  }
153
-
156
+
154
157
  setAutoplay(enabled) {
155
158
  this.isAutoplayEnabled = Boolean(enabled);
156
159
  this.aqua.emit("debug", this.guildId, `Autoplay has been ${enabled ? "enabled" : "disabled"}.`);
157
160
  return this;
158
161
  }
159
-
162
+
160
163
  addToPreviousTrack(track) {
161
164
  if (!track) return;
162
-
165
+
163
166
  this.previousTracks[this.previousTracksIndex] = track;
164
167
  this.previousTracksIndex = (this.previousTracksIndex + 1) % 50;
165
-
168
+
166
169
  if (this.previousTracksCount < 50) {
167
170
  this.previousTracksCount++;
168
171
  }
@@ -171,7 +174,7 @@ class Player extends EventEmitter {
171
174
  _handlePlayerUpdate({ state }) {
172
175
  if (state) {
173
176
  const { position, timestamp, ping } = state;
174
-
177
+
175
178
  if (position !== undefined) this.position = position;
176
179
  if (timestamp !== undefined) this.timestamp = timestamp;
177
180
  if (ping !== undefined) this.ping = ping;
@@ -190,7 +193,7 @@ class Player extends EventEmitter {
190
193
 
191
194
  async play() {
192
195
  if (!this.connected || !this.queue.length) return;
193
-
196
+
194
197
  const item = this.queue.shift();
195
198
  this.current = item.track ? item : await item.resolve(this.aqua);
196
199
  this.playing = true;
@@ -200,18 +203,19 @@ class Player extends EventEmitter {
200
203
  return this.updatePlayer({ track: { encoded: this.current.track } });
201
204
  }
202
205
 
203
- connect({ guildId, voiceChannel, deaf = true, mute = false }) {
204
- if (this.connected) throw new Error("Player is already connected.");
205
-
206
+ connect(options = {}) {
207
+ const { voiceChannel, deaf = true, mute = false } = options;
208
+
209
+
206
210
  const payload = {
207
- guild_id: guildId,
208
- channel_id: voiceChannel,
211
+ guild_id: this.guildId,
212
+ channel_id: this.voiceChannel,
209
213
  self_deaf: deaf,
210
214
  self_mute: mute
211
215
  };
212
-
216
+
213
217
  this.send(payload);
214
-
218
+
215
219
  this.connected = true;
216
220
  this.aqua.emit("debug", this.guildId, `Player connected to voice channel: ${voiceChannel}.`);
217
221
  return this;
@@ -219,41 +223,41 @@ class Player extends EventEmitter {
219
223
 
220
224
  destroy() {
221
225
  if (!this.connected) return this;
222
-
226
+
223
227
  this.disconnect();
224
-
228
+
225
229
  this._cleanupNowPlayingMessage();
226
-
230
+
227
231
  this.isAutoplay = false;
228
232
 
229
233
  this.off("playerUpdate", this._boundHandlers.playerUpdate);
230
234
  this.off("event", this._boundHandlers.event);
231
-
235
+
232
236
  this.aqua.destroyPlayer(this.guildId);
233
237
  this.nodes.rest.destroyPlayer(this.guildId);
234
-
238
+
235
239
  this.clearData();
236
240
  this.removeAllListeners();
237
-
241
+
238
242
  this._boundHandlers = null;
239
243
  this.queue = null;
240
244
  this.previousTracks = null;
241
245
  this.connection = null;
242
246
  this.filters = null;
243
-
247
+
244
248
  return this;
245
249
  }
246
-
250
+
247
251
  _cleanupNowPlayingMessage() {
248
252
  if (this.nowPlayingMessage) {
249
- this.nowPlayingMessage.delete().catch(() => {});
253
+ this.nowPlayingMessage.delete().catch(() => { });
250
254
  this.nowPlayingMessage = null;
251
255
  }
252
256
  }
253
257
 
254
258
  pause(paused) {
255
259
  if (this.paused === paused) return this;
256
-
260
+
257
261
  this.paused = paused;
258
262
  this.updatePlayer({ paused });
259
263
  return this;
@@ -271,7 +275,7 @@ class Player extends EventEmitter {
271
275
 
272
276
  seek(position) {
273
277
  if (!this.playing) return this;
274
-
278
+
275
279
  this.position += position;
276
280
  this.updatePlayer({ position: this.position });
277
281
  return this;
@@ -287,7 +291,7 @@ class Player extends EventEmitter {
287
291
 
288
292
  setVolume(volume) {
289
293
  if (volume < 0 || volume > 200) throw new Error("Volume must be between 0 and 200.");
290
-
294
+
291
295
  this.volume = volume;
292
296
  this.updatePlayer({ volume });
293
297
  return this;
@@ -295,7 +299,7 @@ class Player extends EventEmitter {
295
299
 
296
300
  setLoop(mode) {
297
301
  if (!Player.validModes.has(mode)) throw new Error("Loop mode must be 'none', 'track', or 'queue'.");
298
-
302
+
299
303
  this.loop = mode;
300
304
  this.updatePlayer({ loop: mode });
301
305
  return this;
@@ -309,26 +313,26 @@ class Player extends EventEmitter {
309
313
 
310
314
  setVoiceChannel(channel) {
311
315
  if (!channel?.length) throw new TypeError("Channel must be a non-empty string.");
312
-
316
+
313
317
  if (this.connected && channel === this.voiceChannel) {
314
318
  throw new ReferenceError(`Player already connected to ${channel}.`);
315
319
  }
316
-
320
+
317
321
  this.voiceChannel = channel;
318
-
322
+
319
323
  this.connect({
320
324
  deaf: this.deaf,
321
325
  guildId: this.guildId,
322
326
  voiceChannel: channel,
323
327
  mute: this.mute
324
328
  });
325
-
329
+
326
330
  return this;
327
331
  }
328
332
 
329
333
  disconnect() {
330
334
  if (!this.connected) return this;
331
-
335
+
332
336
  this.connected = false;
333
337
  this.send({ guild_id: this.guildId, channel_id: null });
334
338
  this.voiceChannel = null;
@@ -367,7 +371,7 @@ class Player extends EventEmitter {
367
371
 
368
372
  async trackEnd(player, track, payload) {
369
373
  this.addToPreviousTrack(track);
370
-
374
+
371
375
  if (this.shouldDeleteMessage && this.nowPlayingMessage) {
372
376
  try {
373
377
  await this.nowPlayingMessage.delete();
@@ -376,7 +380,7 @@ class Player extends EventEmitter {
376
380
  console.error("Error deleting now playing message:", error);
377
381
  }
378
382
  }
379
-
383
+
380
384
  const reason = payload.reason;
381
385
  if (reason === "LOAD_FAILED" || reason === "CLEANUP") {
382
386
  if (!player.queue.length) {
@@ -398,7 +402,7 @@ class Player extends EventEmitter {
398
402
  await player.play();
399
403
  }
400
404
  }
401
-
405
+
402
406
  async _handleTrackLooping(player, track) {
403
407
  if (this.loop === Player.LOOP_MODES.TRACK) {
404
408
  player.queue.unshift(track);
@@ -406,7 +410,7 @@ class Player extends EventEmitter {
406
410
  player.queue.push(track);
407
411
  }
408
412
  }
409
-
413
+
410
414
  async _handleEmptyQueue(player) {
411
415
  if (this.isAutoplayEnabled) {
412
416
  await player.autoplay(player);
@@ -432,7 +436,7 @@ class Player extends EventEmitter {
432
436
 
433
437
  async socketClosed(player, payload) {
434
438
  const { code, guildId } = payload || {};
435
-
439
+
436
440
  if (code === 4015 || code === 4009) {
437
441
  this.send({
438
442
  guild_id: guildId,
@@ -441,7 +445,7 @@ class Player extends EventEmitter {
441
445
  self_deaf: this.deaf,
442
446
  });
443
447
  }
444
-
448
+
445
449
  this.aqua.emit("socketClosed", player, payload);
446
450
  this.pause(true);
447
451
  this.aqua.emit("debug", this.guildId, "Player paused due to socket closure.");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aqualink",
3
- "version": "2.3.0",
3
+ "version": "2.3.1",
4
4
  "description": "An Lavalink client, focused in pure performance and features",
5
5
  "main": "build/index.js",
6
6
  "types": "index.d.ts",