aqualink 2.1.2 → 2.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
@@ -22,50 +22,47 @@ This code is based in riffy, but its an 100% Rewrite made from scratch...
22
22
  - https://github.com/topi314/LavaLyrics (RECOMMENDED)
23
23
  - https://github.com/DRSchlaubi/lyrics.kt (?)
24
24
  - https://github.com/DuncteBot/java-timed-lyrics (RECOMMENDED)
25
-
26
- # Tralalero Tralala 2.1.0 Released
25
+
26
+
27
+ # Tralalero Tralala 2.2.0 Released
27
28
  ---
28
29
  - Improved the `AQUA` module
29
- - Faster nodes loading
30
- - Faster plugin loading
31
- - Better listeners for player
32
- - Faster resolving system for playlists
33
-
34
- - Remade the `Connection` system
35
- - Less overheard now.
36
- - Faster connections
37
- - Improved the checkings
38
- - Improved error handling
39
- - Fixed creating useless Objects and arrays
40
-
41
- - Fully rewrite the `Node` system
42
- - Way faster connections
43
- - More stable (i think so)
44
- - Faster events / messages / payloads handling
45
- - Better stats handling (reusing, creating, destroyin)
46
- - Some more bug fixes and stuff i forgot.
47
-
48
- - Remade the `Player` module
49
- - Now support Lazy Loading by default
50
- - Better State Updates
51
- - Improved Garbage Collection
52
- - Rewrite to use direct comparasions
30
+ - Added Fast path in getRequestNode ( Reduces unnecessary type checks )
31
+ - Early return in handleNoMatches ( Avoids unnecessary Spotify requests )
32
+ - Rewrite to use manual loops on constructResponse ( faster than Array.prototype.map, makes the playlists and tracks load way faster and less recourses )
33
+ - Pre-allocated arrays ( Avoids dynamic resizing )
34
+ - Also fixed it sending double requests to lavalink.
53
35
 
54
- - Improved the `Rest` module
55
- - Lazy loading of http2
56
- - Faster request chunks
57
- - Some overall upgrades
58
36
 
59
- - Improved `Track` module
60
- - Faster track looking
61
- - More micro optimizations (use Boolean instead of !!)
37
+ - Remade the `Player` module
38
+ - More efficient track addition, void Array re-call
39
+ - faster event handling with direct states
40
+ - Faster autoplay system and more efficient by map()
41
+ - Reduced Object Creation
42
+ - Rewrite destroy() method
43
+ - Also improved Resource Cleanup
44
+ - Now emit TrackEnd and queueEnd correctly
45
+
46
+ - Rewrite the `autoplay` system
47
+ - Added redirect handling
48
+ - More efficient regex processing
49
+ - Set for unique URLs to avoid duplicate
50
+ - Use array chunks for better performance
51
+ - About ~30%-40% faster for resolving now.
52
+
53
+ - Rewrite the Filter system
54
+ - Uses Direct Assigments
55
+ - Avoid recreating objects on each update
56
+ - Property reuse in updateFilters()
57
+ - Uses traditional for loop
62
58
 
63
- - Remade the INDEX.D.TS File: Added more 1000 lines of code. Added autocomplete, options, and documented everything.
64
59
 
65
60
  # Docs (Wiki)
66
- - https://github.com/ToddyTheNoobDud/AquaLink/wiki
61
+ - https://toddythenoobdud.github.io/aqualink.github.io/
62
+
63
+ - Example bot: https://github.com/ToddyTheNoobDud/Kenium-Music
67
64
 
68
- - Example bot: https://github.com/ToddyTheNoobDud/Thorium-Music
65
+ - Discord support: https://discord.com/invite/K4CVv84VBC
69
66
 
70
67
 
71
68
  # How to install
@@ -147,7 +144,9 @@ client.on("messageCreate", async (message) => {
147
144
 
148
145
  if (resolve.loadType === 'playlist') {
149
146
  await message.channel.send(`Added ${resolve.tracks.length} songs from ${resolve.playlistInfo.name} playlist.`);
150
- player.queue.add(resolve.tracks);
147
+ for (const track of result.tracks) {
148
+ player.queue.add(track);
149
+ }
151
150
  if (!player.playing && !player.paused) return player.play();
152
151
 
153
152
  } else if (resolve.loadType === 'search' || resolve.loadType === 'track') {
@@ -0,0 +1,95 @@
1
+ const https = require('https');
2
+
3
+ function fetch(url, options = {}) {
4
+ return new Promise((resolve, reject) => {
5
+ const req = https.get(url, options, (res) => {
6
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
7
+ return fetch(res.headers.location, options).then(resolve).catch(reject);
8
+ }
9
+
10
+ if (res.statusCode !== 200) {
11
+ res.resume();
12
+ reject(new Error(`Request failed. Status code: ${res.statusCode}`));
13
+ return;
14
+ }
15
+
16
+ const chunks = [];
17
+ res.on('data', chunk => chunks.push(chunk));
18
+ res.on('end', () => resolve(Buffer.concat(chunks).toString()));
19
+ });
20
+
21
+ req.setTimeout(10000, () => {
22
+ req.destroy();
23
+ reject(new Error('Request timeout'));
24
+ });
25
+
26
+ req.on('error', err => {
27
+ reject(err);
28
+ });
29
+
30
+ req.end();
31
+ });
32
+ }
33
+
34
+ async function scAutoPlay(url) {
35
+ try {
36
+ const html = await fetch(`${url}/recommended`);
37
+
38
+ const regex = /<a itemprop="url" href="(\/.*?)"/g;
39
+ const hrefs = new Set();
40
+ let match;
41
+
42
+ while ((match = regex.exec(html)) !== null) {
43
+ hrefs.add(`https://soundcloud.com${match[1]}`);
44
+ }
45
+
46
+ if (hrefs.size === 0) {
47
+ throw new Error("No recommended tracks found on SoundCloud.");
48
+ }
49
+
50
+ const shuffledHrefs = Array.from(hrefs);
51
+ for (let i = shuffledHrefs.length - 1; i > 0; i--) {
52
+ const j = Math.floor(Math.random() * (i + 1));
53
+ [shuffledHrefs[i], shuffledHrefs[j]] = [shuffledHrefs[j], shuffledHrefs[i]];
54
+ }
55
+
56
+ return shuffledHrefs;
57
+ } catch (error) {
58
+ console.error("Error fetching SoundCloud recommendations:", error);
59
+ return [];
60
+ }
61
+ }
62
+
63
+ async function spAutoPlay(track_id) {
64
+ 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.");
86
+ }
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;
92
+ }
93
+ }
94
+
95
+ module.exports = { scAutoPlay, spAutoPlay };
@@ -21,7 +21,7 @@ class Aqua extends EventEmitter {
21
21
  this.players = new Map();
22
22
  this.clientId = null;
23
23
  this.initiated = false;
24
-
24
+
25
25
  this.shouldDeleteMessage = options.shouldDeleteMessage ?? false;
26
26
  this.defaultSearchPlatform = options.defaultSearchPlatform ?? 'ytsearch';
27
27
  this.leaveOnEnd = options.leaveOnEnd ?? true;
@@ -32,7 +32,7 @@ class Aqua extends EventEmitter {
32
32
  this.autoResume = options.autoResume ?? false;
33
33
  this.infiniteReconnects = options.infiniteReconnects ?? false;
34
34
  this.options = options;
35
-
35
+
36
36
  this.setMaxListeners(0);
37
37
  this._leastUsedCache = { nodes: [], timestamp: 0 };
38
38
  }
@@ -67,11 +67,11 @@ class Aqua extends EventEmitter {
67
67
  if (this.initiated) return this;
68
68
 
69
69
  this.clientId = clientId;
70
-
70
+
71
71
  try {
72
72
  for (let i = 0; i < this.nodes.length; i++) { this.createNode(this.nodes[i]); }
73
73
  if (this.plugins.length > 0) { for (let i = 0; i < this.plugins.length; i++) { this.plugins[i].load(this); } }
74
-
74
+
75
75
  this.initiated = true;
76
76
  } catch (error) {
77
77
  this.initiated = false;
@@ -129,7 +129,7 @@ class Aqua extends EventEmitter {
129
129
  if (!region) return this.leastUsedNodes;
130
130
 
131
131
  const lowerRegion = region.toLowerCase();
132
- const regionNodes = Array.from(this.nodeMap.values()).filter(node =>
132
+ const regionNodes = Array.from(this.nodeMap.values()).filter(node =>
133
133
  node.connected && node.regions?.includes(lowerRegion)
134
134
  );
135
135
  regionNodes.sort((a, b) => this.calculateLoad(a) - this.calculateLoad(b));
@@ -151,7 +151,7 @@ class Aqua extends EventEmitter {
151
151
  const availableNodes = options.region ? this.fetchRegion(options.region) : this.leastUsedNodes;
152
152
  const node = availableNodes[0];
153
153
  if (!node) throw new Error("No nodes are available");
154
-
154
+
155
155
  return this.createPlayer(node, options);
156
156
  }
157
157
 
@@ -159,11 +159,11 @@ class Aqua extends EventEmitter {
159
159
  this.destroyPlayer(options.guildId);
160
160
  const player = new Player(this, node, options);
161
161
  this.players.set(options.guildId, player);
162
-
162
+
163
163
  player.once("destroy", () => {
164
164
  this.players.delete(options.guildId);
165
165
  });
166
-
166
+
167
167
  player.connect(options);
168
168
  this.emit("playerCreate", player);
169
169
  return player;
@@ -193,7 +193,7 @@ class Aqua extends EventEmitter {
193
193
  if (["empty", "NO_MATCHES"].includes(response.loadType)) {
194
194
  return await this.handleNoMatches(requestNode.rest, query);
195
195
  }
196
- return this.constructorResponse(response, requester, requestNode);
196
+ return this.constructResponse(response, requester, requestNode);
197
197
  } catch (error) {
198
198
  if (error.name === "AbortError") {
199
199
  throw new Error("Request timed out");
@@ -203,9 +203,12 @@ class Aqua extends EventEmitter {
203
203
  }
204
204
 
205
205
  getRequestNode(nodes) {
206
- if (nodes && !(typeof nodes === "string" || nodes instanceof Node)) {
206
+ if (!nodes) return this.leastUsedNodes[0];
207
+
208
+ if (!(typeof nodes === "string" || nodes instanceof Node)) {
207
209
  throw new TypeError(`'nodes' must be a string or Node instance, received: ${typeof nodes}`);
208
210
  }
211
+
209
212
  return (typeof nodes === "string" ? this.nodeMap.get(nodes) : nodes) ?? this.leastUsedNodes[0];
210
213
  }
211
214
 
@@ -221,17 +224,20 @@ class Aqua extends EventEmitter {
221
224
  try {
222
225
  const ytIdentifier = `/v4/loadtracks?identifier=https://www.youtube.com/watch?v=${query}`;
223
226
  const youtubeResponse = await rest.makeRequest("GET", ytIdentifier);
224
- if (["empty", "NO_MATCHES"].includes(youtubeResponse.loadType)) {
225
- const spotifyIdentifier = `/v4/loadtracks?identifier=https://open.spotify.com/track/${query}`;
226
- return await rest.makeRequest("GET", spotifyIdentifier);
227
+
228
+ if (!["empty", "NO_MATCHES"].includes(youtubeResponse.loadType)) {
229
+ return youtubeResponse;
227
230
  }
228
- return youtubeResponse;
231
+
232
+ const spotifyIdentifier = `/v4/loadtracks?identifier=https://open.spotify.com/track/${query}`;
233
+ return await rest.makeRequest("GET", spotifyIdentifier);
229
234
  } catch (error) {
230
235
  console.error(`Failed to resolve track: ${error.message}`);
236
+ throw error;
231
237
  }
232
238
  }
233
239
 
234
- constructorResponse(response, requester, requestNode) {
240
+ constructResponse(response, requester, requestNode) {
235
241
  const baseResponse = {
236
242
  loadType: response.loadType,
237
243
  exception: null,
@@ -245,8 +251,7 @@ class Aqua extends EventEmitter {
245
251
  return baseResponse;
246
252
  }
247
253
 
248
- // Inline the track factory for better performance
249
- const trackFactory = trackData => new Track(trackData, requester, requestNode);
254
+ const trackFactory = (trackData) => new Track(trackData, requester, requestNode);
250
255
 
251
256
  switch (response.loadType) {
252
257
  case "track":
@@ -254,6 +259,7 @@ class Aqua extends EventEmitter {
254
259
  baseResponse.tracks.push(trackFactory(response.data));
255
260
  }
256
261
  break;
262
+
257
263
  case "playlist":
258
264
  if (response.data?.info) {
259
265
  baseResponse.playlistInfo = {
@@ -261,26 +267,32 @@ class Aqua extends EventEmitter {
261
267
  ...response.data.info
262
268
  };
263
269
  }
270
+
264
271
  const tracks = response.data?.tracks;
265
272
  if (tracks?.length) {
266
- baseResponse.tracks = new Array(tracks.length);
267
- for (let i = 0; i < tracks.length; i++) {
273
+ const len = tracks.length;
274
+ baseResponse.tracks = new Array(len);
275
+ for (let i = 0; i < len; i++) {
268
276
  baseResponse.tracks[i] = trackFactory(tracks[i]);
269
277
  }
270
278
  }
271
279
  break;
280
+
272
281
  case "search":
273
282
  const searchData = response.data ?? [];
274
283
  if (searchData.length) {
275
- baseResponse.tracks = new Array(searchData.length);
276
- for (let i = 0; i < searchData.length; i++) {
284
+ const len = searchData.length;
285
+ baseResponse.tracks = new Array(len);
286
+ for (let i = 0; i < len; i++) {
277
287
  baseResponse.tracks[i] = trackFactory(searchData[i]);
278
288
  }
279
289
  }
280
290
  break;
281
291
  }
292
+
282
293
  return baseResponse;
283
294
  }
295
+
284
296
  get(guildId) {
285
297
  const player = this.players.get(guildId);
286
298
  if (!player) throw new Error(`Player not found for guild ID: ${guildId}`);
@@ -3,21 +3,49 @@
3
3
  class Filters {
4
4
  constructor(player, options = {}) {
5
5
  this.player = player;
6
- this.volume = options.volume ?? 1;
7
- this.equalizer = options.equalizer ?? [];
8
- this.karaoke = options.karaoke ?? null;
9
- this.timescale = options.timescale ?? null;
10
- this.tremolo = options.tremolo ?? null;
11
- this.vibrato = options.vibrato ?? null;
12
- this.rotation = options.rotation ?? null;
13
- this.distortion = options.distortion ?? null;
14
- this.channelMix = options.channelMix ?? null;
15
- this.lowPass = options.lowPass ?? null;
16
- this.bassboost = options.bassboost ?? null;
17
- this.slowmode = options.slowmode ?? null;
18
- this.nightcore = options.nightcore ?? null;
19
- this.vaporwave = options.vaporwave ?? null;
20
- this._8d = options._8d ?? null;
6
+ this.volume = options.volume || 1;
7
+ this.equalizer = options.equalizer || [];
8
+ this.karaoke = options.karaoke || null;
9
+ this.timescale = options.timescale || null;
10
+ this.tremolo = options.tremolo || null;
11
+ this.vibrato = options.vibrato || null;
12
+ this.rotation = options.rotation || null;
13
+ this.distortion = options.distortion || null;
14
+ this.channelMix = options.channelMix || null;
15
+ this.lowPass = options.lowPass || null;
16
+ this.bassboost = options.bassboost || null;
17
+ this.slowmode = options.slowmode || null;
18
+ this.nightcore = options.nightcore || null;
19
+ this.vaporwave = options.vaporwave || null;
20
+ this._8d = options._8d || null;
21
+
22
+ this._filterDataTemplate = {
23
+ volume: this.volume,
24
+ equalizer: this.equalizer,
25
+ karaoke: null,
26
+ timescale: null,
27
+ tremolo: null,
28
+ vibrato: null,
29
+ rotation: null,
30
+ distortion: null,
31
+ channelMix: null,
32
+ lowPass: null
33
+ };
34
+ }
35
+
36
+ _setFilter(filterName, enabled, options, defaults) {
37
+ if (!enabled) {
38
+ this[filterName] = null;
39
+ return this.updateFilters();
40
+ }
41
+
42
+ const filterObj = {};
43
+ for (const [key, defaultValue] of Object.entries(defaults)) {
44
+ filterObj[key] = options[key] !== undefined ? options[key] : defaultValue;
45
+ }
46
+
47
+ this[filterName] = filterObj;
48
+ return this.updateFilters();
21
49
  }
22
50
 
23
51
  setEqualizer(bands) {
@@ -26,127 +54,137 @@ class Filters {
26
54
  }
27
55
 
28
56
  setKaraoke(enabled, options = {}) {
29
- this.karaoke = enabled ? {
30
- level: options.level ?? 1.0,
31
- monoLevel: options.monoLevel ?? 1.0,
32
- filterBand: options.filterBand ?? 220.0,
33
- filterWidth: options.filterWidth ?? 100.0
34
- } : null;
35
- return this.updateFilters();
57
+ return this._setFilter('karaoke', enabled, options, {
58
+ level: 1.0,
59
+ monoLevel: 1.0,
60
+ filterBand: 220.0,
61
+ filterWidth: 100.0
62
+ });
36
63
  }
37
64
 
38
65
  setTimescale(enabled, options = {}) {
39
- this.timescale = enabled ? {
40
- speed: options.speed ?? 1.0,
41
- pitch: options.pitch ?? 1.0,
42
- rate: options.rate ?? 1.0
43
- } : null;
44
- return this.updateFilters();
66
+ return this._setFilter('timescale', enabled, options, {
67
+ speed: 1.0,
68
+ pitch: 1.0,
69
+ rate: 1.0
70
+ });
45
71
  }
46
72
 
47
73
  setTremolo(enabled, options = {}) {
48
- this.tremolo = enabled ? {
49
- frequency: options.frequency ?? 2.0,
50
- depth: options.depth ?? 0.5
51
- } : null;
52
- return this.updateFilters();
74
+ return this._setFilter('tremolo', enabled, options, {
75
+ frequency: 2.0,
76
+ depth: 0.5
77
+ });
53
78
  }
54
79
 
55
80
  setVibrato(enabled, options = {}) {
56
- this.vibrato = enabled ? {
57
- frequency: options.frequency ?? 2.0,
58
- depth: options.depth ?? 0.5
59
- } : null;
60
- return this.updateFilters();
81
+ return this._setFilter('vibrato', enabled, options, {
82
+ frequency: 2.0,
83
+ depth: 0.5
84
+ });
61
85
  }
62
86
 
63
87
  setRotation(enabled, options = {}) {
64
- this.rotation = enabled ? {
65
- rotationHz: options.rotationHz ?? 0.0
66
- } : null;
67
- return this.updateFilters();
88
+ return this._setFilter('rotation', enabled, options, {
89
+ rotationHz: 0.0
90
+ });
68
91
  }
69
92
 
70
93
  setDistortion(enabled, options = {}) {
71
- this.distortion = enabled ? {
72
- sinOffset: options.sinOffset ?? 0.0,
73
- sinScale: options.sinScale ?? 1.0,
74
- cosOffset: options.cosOffset ?? 0.0,
75
- cosScale: options.cosScale ?? 1.0,
76
- tanOffset: options.tanOffset ?? 0.0,
77
- tanScale: options.tanScale ?? 1.0,
78
- offset: options.offset ?? 0.0,
79
- scale: options.scale ?? 1.0
80
- } : null;
81
- return this.updateFilters();
94
+ return this._setFilter('distortion', enabled, options, {
95
+ sinOffset: 0.0,
96
+ sinScale: 1.0,
97
+ cosOffset: 0.0,
98
+ cosScale: 1.0,
99
+ tanOffset: 0.0,
100
+ tanScale: 1.0,
101
+ offset: 0.0,
102
+ scale: 1.0
103
+ });
82
104
  }
83
105
 
84
106
  setChannelMix(enabled, options = {}) {
85
- this.channelMix = enabled ? {
86
- leftToLeft: options.leftToLeft ?? 1.0,
87
- leftToRight: options.leftToRight ?? 0.0,
88
- rightToLeft: options.rightToLeft ?? 0.0,
89
- rightToRight: options.rightToRight ?? 1.0
90
- } : null;
91
- return this.updateFilters();
107
+ return this._setFilter('channelMix', enabled, options, {
108
+ leftToLeft: 1.0,
109
+ leftToRight: 0.0,
110
+ rightToLeft: 0.0,
111
+ rightToRight: 1.0
112
+ });
92
113
  }
93
114
 
94
115
  setLowPass(enabled, options = {}) {
95
- this.lowPass = enabled ? {
96
- smoothing: options.smoothing ?? 20.0
97
- } : null;
98
- return this.updateFilters();
116
+ return this._setFilter('lowPass', enabled, options, {
117
+ smoothing: 20.0
118
+ });
99
119
  }
100
120
 
101
121
  setBassboost(enabled, options = {}) {
102
- if (enabled) {
103
- const value = options.value ?? 5;
104
- if (value < 0 || value > 5) throw new Error("Bassboost value must be between 0 and 5");
105
- this.bassboost = value;
106
- const num = (value - 1) * (1.25 / 9) - 0.25;
107
- return this.setEqualizer(Array(13).fill(0).map((_, i) => ({
108
- band: i,
109
- gain: num
110
- })));
122
+ if (!enabled) {
123
+ this.bassboost = null;
124
+ return this.setEqualizer([]);
111
125
  }
112
- this.bassboost = null;
113
- return this.setEqualizer([]);
126
+
127
+ const value = options.value || 5;
128
+ if (value < 0 || value > 5) throw new Error("Bassboost value must be between 0 and 5");
129
+
130
+ this.bassboost = value;
131
+ const num = (value - 1) * (1.25 / 9) - 0.25;
132
+
133
+ const eq = new Array(13);
134
+ for (let i = 0; i < 13; i++) {
135
+ eq[i] = { band: i, gain: num };
136
+ }
137
+
138
+ return this.setEqualizer(eq);
114
139
  }
115
140
 
116
141
  setSlowmode(enabled, options = {}) {
117
142
  this.slowmode = enabled;
118
- return this.setTimescale(enabled, { rate: enabled ? options.rate ?? 0.8 : 1.0 });
143
+ return this.setTimescale(enabled, { rate: enabled ? (options.rate || 0.8) : 1.0 });
119
144
  }
120
145
 
121
146
  setNightcore(enabled, options = {}) {
122
147
  this.nightcore = enabled;
123
- if (enabled) {
124
- return this.setTimescale(true, { rate: options.rate ?? 1.5 });
125
- }
126
- return this.setTimescale(false);
148
+ return this.setTimescale(enabled, { rate: enabled ? (options.rate || 1.5) : 1.0 });
127
149
  }
128
150
 
129
151
  setVaporwave(enabled, options = {}) {
130
152
  this.vaporwave = enabled;
131
- if (enabled) {
132
- return this.setTimescale(true, { pitch: options.pitch ?? 0.5 });
133
- }
134
- return this.setTimescale(false);
153
+ return this.setTimescale(enabled, { pitch: enabled ? (options.pitch || 0.5) : 1.0 });
135
154
  }
136
155
 
137
156
  set8D(enabled, options = {}) {
138
157
  this._8d = enabled;
139
- return this.setRotation(enabled, { rotationHz: enabled ? options.rotationHz ?? 0.2 : 0.0 });
158
+ return this.setRotation(enabled, { rotationHz: enabled ? (options.rotationHz || 0.2) : 0.0 });
140
159
  }
141
160
 
142
161
  async clearFilters() {
143
- Object.assign(this, new Filters(this.player));
162
+ this.volume = 1;
163
+ this.equalizer = [];
164
+ this.karaoke = null;
165
+ this.timescale = null;
166
+ this.tremolo = null;
167
+ this.vibrato = null;
168
+ this.rotation = null;
169
+ this.distortion = null;
170
+ this.channelMix = null;
171
+ this.lowPass = null;
172
+ this.bassboost = null;
173
+ this.slowmode = null;
174
+ this.nightcore = null;
175
+ this.vaporwave = null;
176
+ this._8d = null;
177
+
178
+ this._filterDataTemplate.volume = 1;
179
+ this._filterDataTemplate.equalizer = [];
180
+
144
181
  await this.updateFilters();
145
182
  return this;
146
183
  }
147
184
 
148
185
  async updateFilters() {
149
186
  const filterData = {
187
+ ...this._filterDataTemplate,
150
188
  volume: this.volume,
151
189
  equalizer: this.equalizer,
152
190
  karaoke: this.karaoke,
@@ -159,6 +197,8 @@ class Filters {
159
197
  lowPass: this.lowPass
160
198
  };
161
199
 
200
+ this._filterDataTemplate = { ...filterData };
201
+
162
202
  await this.player.nodes.rest.updatePlayer({
163
203
  guildId: this.player.guildId,
164
204
  data: { filters: filterData }
@@ -168,4 +208,4 @@ class Filters {
168
208
  }
169
209
  }
170
210
 
171
- module.exports = Filters
211
+ module.exports = Filters;
@@ -4,6 +4,7 @@ const { EventEmitter } = require("events");
4
4
  const Connection = require("./Connection");
5
5
  const Queue = require("./Queue");
6
6
  const Filters = require("./Filters");
7
+ const { spAutoPlay, scAutoPlay } = require('../handlers/autoplay');
7
8
 
8
9
  class Player extends EventEmitter {
9
10
  static LOOP_MODES = Object.freeze({
@@ -25,6 +26,7 @@ class Player extends EventEmitter {
25
26
 
26
27
  constructor(aqua, nodes, options = {}) {
27
28
  super();
29
+
28
30
  this.aqua = aqua;
29
31
  this.nodes = nodes;
30
32
  this.guildId = options.guildId;
@@ -34,55 +36,145 @@ class Player extends EventEmitter {
34
36
  this.connection = new Connection(this);
35
37
  this.filters = new Filters(this);
36
38
 
37
- const defaultVolume = options.defaultVolume ?? 100;
38
- this.volume = defaultVolume > 200 ? 200 : (defaultVolume < 0 ? 0 : defaultVolume);
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
- this.previousTracks = [];
44
- this.shouldDeleteMessage = !!options.shouldDeleteMessage;
45
- this.leaveOnEnd = !!options.leaveOnEnd;
43
+ this.previousTracks = Array(50);
44
+ this.previousTracksCount = 0;
45
+ this.shouldDeleteMessage = Boolean(options.shouldDeleteMessage);
46
+ this.leaveOnEnd = Boolean(options.leaveOnEnd);
46
47
 
47
48
  this.playing = false;
48
49
  this.paused = false;
49
50
  this.connected = false;
50
51
  this.current = null;
52
+ this.position = 0;
51
53
  this.timestamp = 0;
52
54
  this.ping = 0;
53
55
  this.nowPlayingMessage = null;
54
-
56
+ this.isAutoplayEnabled = false;
57
+ this.isAutoplay = false;
58
+
55
59
  this._handlePlayerUpdate = this._handlePlayerUpdate.bind(this);
56
60
  this._handleEvent = this._handleEvent.bind(this);
57
61
 
58
62
  this.on("playerUpdate", this._handlePlayerUpdate);
59
63
  this.on("event", this._handleEvent);
60
-
61
- this._dataStore = null;
62
64
  }
63
65
 
64
66
  get previous() {
65
- return this.previousTracks[0] || null;
67
+ return this.previousTracksCount > 0 ? this.previousTracks[0] : null;
66
68
  }
67
69
 
68
70
  get currenttrack() {
69
71
  return this.current;
70
72
  }
71
73
 
74
+ async autoplay(player) {
75
+ if (!player) throw new Error("Quick Fix: player.autoplay(player)");
76
+
77
+ if (!this.isAutoplayEnabled) {
78
+ this.aqua.emit("debug", this.guildId, "Autoplay is disabled.");
79
+ return this;
80
+ }
81
+
82
+ this.isAutoplay = true;
83
+ if (!this.previous) return this;
84
+
85
+ try {
86
+ const { sourceName, identifier, uri, requester } = this.previous.info;
87
+ this.aqua.emit("debug", this.guildId, `Attempting autoplay for ${sourceName}`);
88
+
89
+ let query, source, response;
90
+
91
+ const sourceHandlers = {
92
+ youtube: async () => {
93
+ return {
94
+ query: `https://www.youtube.com/watch?v=${identifier}&list=RD${identifier}`,
95
+ source: "ytmsearch"
96
+ };
97
+ },
98
+ soundcloud: async () => {
99
+ const scResults = await scAutoPlay(uri);
100
+ if (!scResults?.length) return null;
101
+ return {
102
+ query: scResults[0],
103
+ source: "scsearch"
104
+ };
105
+ },
106
+ spotify: async () => {
107
+ const spResult = await spAutoPlay(identifier);
108
+ if (!spResult) return null;
109
+ return {
110
+ query: `https://open.spotify.com/track/${spResult}`,
111
+ source: "spotify"
112
+ };
113
+ }
114
+ };
115
+
116
+ const handler = sourceHandlers[sourceName];
117
+ if (!handler) return this;
118
+
119
+ const result = await handler();
120
+ if (!result) return this;
121
+
122
+ ({ query, source } = result);
123
+
124
+ response = await this.aqua.resolve({ query, source, requester });
125
+
126
+ if (!response?.tracks?.length || ["error", "empty", "LOAD_FAILED", "NO_MATCHES"].includes(response.loadType)) {
127
+ return this.stop();
128
+ }
129
+
130
+ const track = response.tracks[Math.floor(Math.random() * response.tracks.length)];
131
+
132
+ if (!track?.info?.title) {
133
+ throw new Error("Invalid track object: missing title or info.");
134
+ }
135
+
136
+ track.requester = this.previous.requester || { id: "Unknown" };
137
+
138
+ this.queue.push(track);
139
+ await this.play();
140
+
141
+ return this;
142
+ } catch (error) {
143
+ console.error("Autoplay error:", error);
144
+ return this.stop();
145
+ }
146
+ }
147
+
148
+ setAutoplay(enabled) {
149
+ this.isAutoplayEnabled = Boolean(enabled);
150
+ this.aqua.emit("debug", this.guildId, `Autoplay has been ${enabled ? "enabled" : "disabled"}.`);
151
+ return this;
152
+ }
153
+
72
154
  addToPreviousTrack(track) {
73
155
  if (!track) return;
74
156
 
75
- if (this.previousTracks.length >= 50) {
76
- this.previousTracks.pop();
157
+ if (this.previousTracksCount < 50) {
158
+ for (let i = this.previousTracksCount; i > 0; i--) {
159
+ this.previousTracks[i] = this.previousTracks[i-1];
160
+ }
161
+ this.previousTracks[0] = track;
162
+ this.previousTracksCount++;
163
+ } else {
164
+ for (let i = 49; i > 0; i--) {
165
+ this.previousTracks[i] = this.previousTracks[i-1];
166
+ }
167
+ this.previousTracks[0] = track;
77
168
  }
78
- this.previousTracks.unshift(track);
79
169
  }
80
170
 
81
171
  _handlePlayerUpdate({ state }) {
82
172
  if (state) {
83
- if (state.position !== undefined) this.position = state.position;
84
- if (state.timestamp !== undefined) this.timestamp = state.timestamp;
85
- if (state.ping !== undefined) this.ping = state.ping;
173
+ const { position, timestamp, ping } = state;
174
+
175
+ if (position !== undefined) this.position = position;
176
+ if (timestamp !== undefined) this.timestamp = timestamp;
177
+ if (ping !== undefined) this.ping = ping;
86
178
  }
87
179
  this.aqua.emit("playerUpdate", this, { state });
88
180
  }
@@ -111,12 +203,14 @@ class Player extends EventEmitter {
111
203
  connect({ guildId, voiceChannel, deaf = true, mute = false }) {
112
204
  if (this.connected) throw new Error("Player is already connected.");
113
205
 
114
- this.send({
206
+ const payload = {
115
207
  guild_id: guildId,
116
208
  channel_id: voiceChannel,
117
209
  self_deaf: deaf,
118
210
  self_mute: mute
119
- });
211
+ };
212
+
213
+ this.send(payload);
120
214
 
121
215
  this.connected = true;
122
216
  this.aqua.emit("debug", this.guildId, `Player connected to voice channel: ${voiceChannel}.`);
@@ -128,8 +222,13 @@ class Player extends EventEmitter {
128
222
 
129
223
  this.disconnect();
130
224
 
131
- this.nowPlayingMessage?.delete().catch(() => {});
225
+ if (this.nowPlayingMessage) {
226
+ this.nowPlayingMessage.delete().catch(() => {});
227
+ this.nowPlayingMessage = null;
228
+ }
132
229
 
230
+ this.isAutoplay = false;
231
+
133
232
  this.aqua.destroyPlayer(this.guildId);
134
233
  this.nodes.rest.destroyPlayer(this.guildId);
135
234
  this.clearData();
@@ -165,7 +264,6 @@ class Player extends EventEmitter {
165
264
 
166
265
  stop() {
167
266
  if (!this.playing) return this;
168
-
169
267
  this.playing = false;
170
268
  this.position = 0;
171
269
  this.updatePlayer({ track: { encoded: null } });
@@ -202,6 +300,7 @@ class Player extends EventEmitter {
202
300
  }
203
301
 
204
302
  this.voiceChannel = channel;
303
+
205
304
  this.connect({
206
305
  deaf: this.deaf,
207
306
  guildId: this.guildId,
@@ -244,30 +343,30 @@ class Player extends EventEmitter {
244
343
  }
245
344
 
246
345
  async trackStart(player, track) {
247
- this.updateTrackState(true, false);
346
+ this.playing = true;
347
+ this.paused = false;
248
348
  this.aqua.emit("trackStart", player, track);
249
349
  }
250
350
 
251
- async trackChange(player, track) {
252
- this.updateTrackState(true, false);
253
- this.aqua.emit("trackChange", player, track);
254
- }
255
-
256
351
  async trackEnd(player, track, payload) {
352
+ this.addToPreviousTrack(track);
353
+
257
354
  if (this.shouldDeleteMessage && this.nowPlayingMessage) {
258
355
  try {
259
356
  await this.nowPlayingMessage.delete();
260
357
  this.nowPlayingMessage = null;
261
- } catch {}
358
+ } catch (error) {
359
+ console.error("Error deleting now playing message:", error);
360
+ }
262
361
  }
263
-
264
- const reason = payload.reason?.toLowerCase().replace("_", "");
265
362
 
266
- if (reason === "loadfailed" || reason === "cleanup") {
363
+ const reason = payload.reason;
364
+ if (reason === "LOAD_FAILED" || reason === "CLEANUP") {
267
365
  if (!player.queue.length) {
268
366
  this.clearData();
269
367
  this.aqua.emit("queueEnd", player);
270
368
  } else {
369
+ this.aqua.emit("trackEnd", player, track, reason);
271
370
  await player.play();
272
371
  }
273
372
  return;
@@ -280,16 +379,20 @@ class Player extends EventEmitter {
280
379
  }
281
380
 
282
381
  if (player.queue.isEmpty()) {
283
- this.playing = false;
284
- if (this.leaveOnEnd) {
285
- this.clearData();
286
- this.cleanup();
382
+ if (this.isAutoplayEnabled) {
383
+ await player.autoplay(player);
384
+ } else {
385
+ this.playing = false;
386
+ if (this.leaveOnEnd) {
387
+ this.clearData();
388
+ this.cleanup();
389
+ }
390
+ this.aqua.emit("queueEnd", player);
287
391
  }
288
- this.aqua.emit("queueEnd", player);
289
- return;
392
+ } else {
393
+ this.aqua.emit("trackEnd", player, track, reason);
394
+ await player.play();
290
395
  }
291
-
292
- await player.play();
293
396
  }
294
397
 
295
398
  async trackError(player, track, payload) {
@@ -303,11 +406,11 @@ class Player extends EventEmitter {
303
406
  }
304
407
 
305
408
  async socketClosed(player, payload) {
306
- const code = payload && payload.code;
409
+ const { code, guildId } = payload || {};
307
410
 
308
411
  if (code === 4015 || code === 4009) {
309
412
  this.send({
310
- guild_id: payload.guildId,
413
+ guild_id: guildId,
311
414
  channel_id: this.voiceChannel,
312
415
  self_mute: this.mute,
313
416
  self_deaf: this.deaf,
@@ -325,7 +428,7 @@ class Player extends EventEmitter {
325
428
 
326
429
  set(key, value) {
327
430
  if (!this._dataStore) {
328
- this._dataStore = new WeakMap();
431
+ this._dataStore = new Map();
329
432
  }
330
433
  this._dataStore.set(key, value);
331
434
  }
@@ -335,7 +438,9 @@ class Player extends EventEmitter {
335
438
  }
336
439
 
337
440
  clearData() {
338
- if (this.previousTracks) this.previousTracks.length = 0;
441
+ if (this.previousTracks) {
442
+ this.previousTracksCount = 0;
443
+ }
339
444
  this._dataStore = null;
340
445
  return this;
341
446
  }
@@ -354,11 +459,6 @@ class Player extends EventEmitter {
354
459
  this.destroy();
355
460
  }
356
461
  }
357
-
358
- updateTrackState(playing, paused) {
359
- this.playing = playing;
360
- this.paused = paused;
361
- }
362
462
  }
363
463
 
364
464
  module.exports = Player;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aqualink",
3
- "version": "2.1.2",
3
+ "version": "2.2.0",
4
4
  "description": "An Lavalink client, focused in pure performance and features",
5
5
  "main": "build/index.js",
6
6
  "types": "index.d.ts",