aqualink 2.1.2 → 2.1.3

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.
@@ -0,0 +1,65 @@
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 !== 200) {
7
+ reject(new Error(`Request failed. Status code: ${res.statusCode}`));
8
+ return;
9
+ }
10
+ let data = '';
11
+ res.on('data', (chunk) => data += chunk);
12
+ res.on('end', () => resolve(data));
13
+ });
14
+ req.on('error', reject);
15
+ req.end();
16
+ });
17
+ }
18
+
19
+ async function scAutoPlay(url) {
20
+ try {
21
+ const html = await fetch(`${url}/recommended`);
22
+
23
+ const matches = [...html.matchAll(/<a itemprop="url" href="(\/.*?)"/g)];
24
+
25
+ const hrefs = [...new Set(matches.map(match => `https://soundcloud.com${match[1]}`))];
26
+
27
+ if (hrefs.length === 0) {
28
+ throw new Error("No recommended tracks found on SoundCloud.");
29
+ }
30
+
31
+ const shuffledHrefs = hrefs.sort(() => Math.random() - 0.5);
32
+
33
+ return shuffledHrefs;
34
+ } catch (error) {
35
+ console.error("Error fetching SoundCloud recommendations:", error);
36
+ return [];
37
+ }
38
+ }
39
+
40
+ async function spAutoPlay(track_id) {
41
+ try {
42
+ const tokenResponse = await fetch("https://open.spotify.com/get_access_token?reason=transport&productType=embed");
43
+ const { accessToken } = JSON.parse(tokenResponse);
44
+
45
+ if (!accessToken) throw new Error("Failed to retrieve Spotify access token");
46
+
47
+ const recommendationsResponse = await fetch(`https://api.spotify.com/v1/recommendations?limit=10&seed_tracks=${track_id}`, {
48
+ headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' }
49
+ });
50
+
51
+ const { tracks } = JSON.parse(recommendationsResponse);
52
+
53
+ if (!tracks || tracks.length === 0) {
54
+ throw new Error("No recommended tracks found on Spotify.");
55
+ }
56
+
57
+ // Return a random track ID
58
+ return tracks[Math.floor(Math.random() * tracks.length)].id;
59
+ } catch (error) {
60
+ console.error("Error fetching Spotify recommendations:", error);
61
+ return null;
62
+ }
63
+ }
64
+
65
+ module.exports = { scAutoPlay, spAutoPlay };
@@ -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({
@@ -52,13 +53,25 @@ class Player extends EventEmitter {
52
53
  this.ping = 0;
53
54
  this.nowPlayingMessage = null;
54
55
 
56
+ this.isAutoplay = false;
57
+ this._autoplay = {
58
+ _addToHistory: (track) => {
59
+ if (this.previousTracks.length >= 50) {
60
+ this.previousTracks.pop();
61
+ }
62
+ this.previousTracks.unshift(track);
63
+ },
64
+ _cleanup: () => {
65
+ this.previousTracks = [];
66
+ this.isAutoplay = false;
67
+ }
68
+ };
69
+
55
70
  this._handlePlayerUpdate = this._handlePlayerUpdate.bind(this);
56
71
  this._handleEvent = this._handleEvent.bind(this);
57
72
 
58
73
  this.on("playerUpdate", this._handlePlayerUpdate);
59
74
  this.on("event", this._handleEvent);
60
-
61
- this._dataStore = null;
62
75
  }
63
76
 
64
77
  get previous() {
@@ -69,6 +82,75 @@ class Player extends EventEmitter {
69
82
  return this.current;
70
83
  }
71
84
 
85
+ async autoplay(player) {
86
+ if (!player) throw new Error("Quick Fix: player.autoplay(player)");
87
+
88
+ if (!this.isAutoplayEnabled) {
89
+ this.aqua.emit("debug", this.guildId, "Autoplay is disabled.");
90
+ return this;
91
+ }
92
+
93
+ this.isAutoplay = true;
94
+ if (!this.previous) return this;
95
+
96
+ try {
97
+ const { sourceName, identifier, uri, requester } = this.previous.info;
98
+ this.aqua.emit("debug", this.guildId, `Attempting autoplay for ${sourceName}`);
99
+
100
+ let query, source, response;
101
+
102
+ switch (sourceName) {
103
+ case "youtube":
104
+ query = `https://www.youtube.com/watch?v=${identifier}&list=RD${identifier}`;
105
+ source = "ytmsearch";
106
+ break;
107
+ case "soundcloud":
108
+ const scResults = await scAutoPlay(uri);
109
+ if (!scResults?.length) return this;
110
+ query = scResults[0];
111
+ source = "scsearch";
112
+ break;
113
+ case "spotify":
114
+ const spResult = await spAutoPlay(identifier);
115
+ if (!spResult) return this;
116
+ query = `https://open.spotify.com/track/${spResult}`;
117
+ source = "spotify";
118
+ break;
119
+ default:
120
+ return this;
121
+ }
122
+
123
+ response = await this.aqua.resolve({ query, source, requester });
124
+
125
+ if (!response?.tracks?.length || ["error", "empty", "LOAD_FAILED", "NO_MATCHES"].includes(response.loadType)) {
126
+ return this.stop();
127
+ }
128
+
129
+ const track = response.tracks[Math.floor(Math.random() * response.tracks.length)];
130
+
131
+ if (!track?.info?.title) {
132
+ throw new Error("Invalid track object: missing title or info.");
133
+ }
134
+
135
+ track.requester = this.previous.requester || { id: "Unknown" };
136
+
137
+ this.queue.push(track);
138
+ await this.play();
139
+
140
+ return this;
141
+ } catch (error) {
142
+ console.error("Autoplay error:", error);
143
+ return this.stop();
144
+ }
145
+ }
146
+
147
+ setAutoplay(enabled) {
148
+ this.isAutoplayEnabled = Boolean(enabled);
149
+ this.aqua.emit("debug", this.guildId, `Autoplay has been ${enabled ? "enabled" : "disabled"}.`);
150
+ }
151
+
152
+
153
+
72
154
  addToPreviousTrack(track) {
73
155
  if (!track) return;
74
156
 
@@ -130,6 +212,10 @@ class Player extends EventEmitter {
130
212
 
131
213
  this.nowPlayingMessage?.delete().catch(() => {});
132
214
 
215
+ if (this._autoplay) {
216
+ this._autoplay._cleanup();
217
+ }
218
+
133
219
  this.aqua.destroyPlayer(this.guildId);
134
220
  this.nodes.rest.destroyPlayer(this.guildId);
135
221
  this.clearData();
@@ -165,7 +251,6 @@ class Player extends EventEmitter {
165
251
 
166
252
  stop() {
167
253
  if (!this.playing) return this;
168
-
169
254
  this.playing = false;
170
255
  this.position = 0;
171
256
  this.updatePlayer({ track: { encoded: null } });
@@ -254,15 +339,16 @@ class Player extends EventEmitter {
254
339
  }
255
340
 
256
341
  async trackEnd(player, track, payload) {
342
+ this.addToPreviousTrack(track);
257
343
  if (this.shouldDeleteMessage && this.nowPlayingMessage) {
258
344
  try {
259
345
  await this.nowPlayingMessage.delete();
260
346
  this.nowPlayingMessage = null;
261
- } catch {}
347
+ } catch (error) {
348
+ console.error("Error deleting now playing message:", error);
349
+ }
262
350
  }
263
-
264
351
  const reason = payload.reason?.toLowerCase().replace("_", "");
265
-
266
352
  if (reason === "loadfailed" || reason === "cleanup") {
267
353
  if (!player.queue.length) {
268
354
  this.clearData();
@@ -280,16 +366,20 @@ class Player extends EventEmitter {
280
366
  }
281
367
 
282
368
  if (player.queue.isEmpty()) {
283
- this.playing = false;
284
- if (this.leaveOnEnd) {
285
- this.clearData();
286
- this.cleanup();
369
+ if (this.isAutoplayEnabled) {
370
+ await player.autoplay(player, track);
371
+ } else {
372
+ this.playing = false;
373
+ if (this.leaveOnEnd) {
374
+ this.clearData();
375
+ this.cleanup();
376
+ }
377
+ this.aqua.emit("queueEnd", player);
287
378
  }
288
- this.aqua.emit("queueEnd", player);
289
379
  return;
380
+ } else {
381
+ await player.play();
290
382
  }
291
-
292
- await player.play();
293
383
  }
294
384
 
295
385
  async trackError(player, track, payload) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aqualink",
3
- "version": "2.1.2",
3
+ "version": "2.1.3",
4
4
  "description": "An Lavalink client, focused in pure performance and features",
5
5
  "main": "build/index.js",
6
6
  "types": "index.d.ts",