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.
- package/build/handlers/autoplay.js +65 -0
- package/build/structures/Player.js +103 -13
- package/package.json +1 -1
|
@@ -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.
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
this.
|
|
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) {
|