aqualink 1.6.2-beta → 1.7.0-beta1
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 +44 -34
- package/build/handlers/fetchImage.js +38 -26
- package/build/structures/Aqua.js +45 -25
- package/build/structures/Connection.js +93 -72
- package/build/structures/Node.js +44 -62
- package/build/structures/Player.js +52 -128
- package/build/structures/Rest.js +1 -0
- package/build/structures/Track.js +49 -41
- package/package.json +2 -5
package/README.md
CHANGED
|
@@ -25,41 +25,51 @@ This code is based in riffy, but its an 100% Rewrite made from scratch...
|
|
|
25
25
|
|
|
26
26
|
- Example bot: https://github.com/ToddyTheNoobDud/Thorium-Music
|
|
27
27
|
|
|
28
|
-
#
|
|
28
|
+
# Real changelog for 1.7.0-beta1
|
|
29
|
+
Note: Not features are widely tested / Not fully Complete
|
|
30
|
+
|
|
31
|
+
- Reformated the `PLAYER` System (removed Documentation for now)
|
|
32
|
+
- Notable Changes:
|
|
33
|
+
- New WeakMap System (Properly handling, deleting, setting)
|
|
34
|
+
- Around 2x faster (by my tests, taked 0ms to resolve an song)
|
|
35
|
+
- Uses less recourses (reduced by around ~0,5mb, also less cpu instensive)
|
|
36
|
+
|
|
37
|
+
- Fix Some errors in `REST`, Now destroyPlayer, etc, should work as expected.
|
|
38
|
+
- Rewrited out the `TRACK` System
|
|
39
|
+
- Reduced object creation
|
|
40
|
+
- Use direct acess
|
|
41
|
+
- use direct destroy() instead of Object.assing()
|
|
42
|
+
- Separate _findMatchingTrack()
|
|
43
|
+
- Rewrite the search system, Removed useless caching, Improved speed, use traditional for ... of instead of find() - Experimental
|
|
44
|
+
|
|
45
|
+
- Rewrited out the `NODE` System
|
|
46
|
+
- Implement the InfiniteReconnects Option (this will make the code try to connect to an node non-stop.)
|
|
47
|
+
- WeakMap has been replaced with statsCache (experimental)
|
|
48
|
+
- Optimized by using free, used and allocated direct.
|
|
49
|
+
- Backoff in reconnect logic (by using Math)
|
|
50
|
+
- Clear reconnectTimeoutId (prevent memory leaks)
|
|
51
|
+
- Improve the overall speed by a bit
|
|
52
|
+
|
|
53
|
+
- Rewrited the `CONNECTION` System
|
|
54
|
+
- Improved the Connecting, Resolving, Reconnecting Speed (around 1,5x faster now)
|
|
55
|
+
- Improved checking
|
|
56
|
+
- Cached frequently used Code
|
|
57
|
+
- Object.assign Implemented for Batch updates
|
|
58
|
+
- Still in testing, pls report bugs
|
|
59
|
+
|
|
60
|
+
- Some Additions for `AQUA`
|
|
61
|
+
- Implement the InfiniteReconnects Options
|
|
62
|
+
- Re-added our DOCS (Now autocomplete works again!)
|
|
63
|
+
- Add platforms + search system on DefaultPlatform
|
|
64
|
+
|
|
65
|
+
- Rewrited the updateVoiceState System
|
|
66
|
+
- Misc changes to createConnection
|
|
67
|
+
- Document + fix destroyPlayer
|
|
68
|
+
|
|
69
|
+
- Remade some stuff in `FetchImage`
|
|
70
|
+
- Use promise.race since only first sucess is required. (will be tested, may revert to promise.any)
|
|
71
|
+
- Use map cuz its faster and more efficient than Objects.
|
|
29
72
|
|
|
30
|
-
Version 1.6.2
|
|
31
|
-
- Fixed the rejected error (connection, for example using destroy returns an error)
|
|
32
|
-
- Added bun support (now it should work with bun too)
|
|
33
|
-
|
|
34
|
-
Version 1.6.1:
|
|
35
|
-
|
|
36
|
-
- Many fixes related to caching and mapping.
|
|
37
|
-
- Rewrited `AQUA` again, Should fix a lot of stuff related to speed, async memory leaks, remade some methods, and others stuff;
|
|
38
|
-
- Rewrited `CONNECTION` manager again, should be an better cleanup system less memory leaks, correct updating.
|
|
39
|
-
- Fixed various stuff + Various speed improvements, and way less recourses used on `NODE` manager.
|
|
40
|
-
- Updated `REST` Manager to use better checkings, speed, and dumping system.
|
|
41
|
-
- Rewrote the `TRACK` System, fixed small memory leaks, improved speed by wayyy more
|
|
42
|
-
- Some misc changes on player, small optimziations, use Fisher-Yates algorithm, and remove useless asyncs.
|
|
43
|
-
- Improved the internal code Garbage collection.
|
|
44
|
-
- Use more WeakSet and WeakMaps for memory friendly
|
|
45
|
-
- Extra: Also fixed requesting every 1 sec, reducing the requests system and memory usage by a lot i think;
|
|
46
|
-
-- Im working on new features, ex: autoplay, lyrics system, and more to come... its hard to me as an solo dev
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
Version 1.6.0:
|
|
50
|
-
|
|
51
|
-
- Reworked the `TRACK` Manager (This improves the speed by wayyy more, also uses objects, removed useless code)
|
|
52
|
-
- Improved the `REST` Manager (This improves the garbage collector, an faster code, and more optimized)
|
|
53
|
-
- Added enqueue to `QUEUE` (this gets the previous, made for dev), removed addMultiple (useless)
|
|
54
|
-
- Fully Rewrite the `PLAYER` Manager (Way faster resolving, way less recourse intensive, more responsive, better error handling)
|
|
55
|
-
|
|
56
|
-
^^ Now uses the WeakMap and WeakSet for an garbage collector, making it with an better memory management.
|
|
57
|
-
|
|
58
|
-
- Rewrite the `NODE` Manager (reconnect speeds improved, various methods improved, Rewrite the cache and status handler, improve the performance) - Also fixed player resuming.
|
|
59
|
-
- Remade some stuff in `CONNECTION` (this improves error handling, cleaning up, and speed)
|
|
60
|
-
- Rewrite `AQUA` Manager (remade every single method, improved the resolve, made the code dynamic, fixed lots of bugs, uses weakMap too.) - Added autoResume option (true false)
|
|
61
|
-
|
|
62
|
-
- There are way more stuff that i forgot to add on changelog. pls report bugs on my github !
|
|
63
73
|
# How to install
|
|
64
74
|
|
|
65
75
|
`npm install aqualink`
|
|
@@ -1,47 +1,59 @@
|
|
|
1
1
|
const { request } = require("undici");
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
(
|
|
5
|
-
(
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
const sourceHandlers = new Map([
|
|
4
|
+
['spotify', (uri) => fetchThumbnail(`https://open.spotify.com/oembed?url=${uri}`)],
|
|
5
|
+
['youtube', (identifier) => fetchYouTubeThumbnail(identifier)]
|
|
6
|
+
]);
|
|
7
|
+
|
|
8
|
+
const YOUTUBE_URL_TEMPLATE = (quality) =>
|
|
9
|
+
(id) => `https://img.youtube.com/vi/${id}/${quality}.jpg`;
|
|
10
|
+
|
|
11
|
+
const YOUTUBE_QUALITIES = [
|
|
12
|
+
'maxresdefault',
|
|
13
|
+
'hqdefault',
|
|
14
|
+
'mqdefault',
|
|
15
|
+
'default'
|
|
16
|
+
].map(YOUTUBE_URL_TEMPLATE);
|
|
9
17
|
|
|
10
18
|
async function getImageUrl(info) {
|
|
11
|
-
if (!info
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
if (!info?.sourceName || !info?.uri) return null;
|
|
20
|
+
|
|
21
|
+
const handler = sourceHandlers.get(info.sourceName.toLowerCase());
|
|
22
|
+
if (!handler) return null;
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
return await handler(info.uri);
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
20
28
|
}
|
|
21
29
|
}
|
|
22
30
|
|
|
23
31
|
async function fetchThumbnail(url) {
|
|
24
32
|
try {
|
|
25
|
-
const { body } = await request(url, {
|
|
33
|
+
const { body } = await request(url, {
|
|
34
|
+
method: "GET",
|
|
35
|
+
headers: {
|
|
36
|
+
'Accept': 'application/json'
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
26
40
|
const json = await body.json();
|
|
27
|
-
await body.dump();
|
|
28
41
|
return json.thumbnail_url || null;
|
|
29
|
-
} catch
|
|
30
|
-
console.error(`Error fetching ${url}:`, error);
|
|
42
|
+
} catch {
|
|
31
43
|
return null;
|
|
32
44
|
}
|
|
33
45
|
}
|
|
34
46
|
|
|
35
47
|
async function fetchYouTubeThumbnail(identifier) {
|
|
36
|
-
const fetchPromises =
|
|
37
|
-
|
|
48
|
+
const fetchPromises = YOUTUBE_QUALITIES.map(urlFunc =>
|
|
49
|
+
fetchThumbnail(urlFunc(identifier))
|
|
50
|
+
);
|
|
38
51
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
52
|
+
try {
|
|
53
|
+
return await Promise.race(fetchPromises);
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
43
56
|
}
|
|
44
|
-
return null;
|
|
45
57
|
}
|
|
46
58
|
|
|
47
59
|
module.exports = { getImageUrl };
|
package/build/structures/Aqua.js
CHANGED
|
@@ -5,6 +5,27 @@ const { Track } = require("./Track");
|
|
|
5
5
|
const { version: pkgVersion } = require("../../package.json");
|
|
6
6
|
const URL_REGEX = /^https?:\/\//;
|
|
7
7
|
class Aqua extends EventEmitter {
|
|
8
|
+
/**
|
|
9
|
+
* @param {Object} client - The client instance.
|
|
10
|
+
* @param {Array<Object>} nodes - An array of node configurations.
|
|
11
|
+
* @param {Object} options - Configuration options for Aqua.
|
|
12
|
+
* @param {Function} options.send - Function to send data.
|
|
13
|
+
* @param {string} [options.defaultSearchPlatform="ytsearch"] - Default search platform. Options include:
|
|
14
|
+
* - "youtube music": "ytmsearch"
|
|
15
|
+
* - "youtube": "ytsearch"
|
|
16
|
+
* - "spotify": "spsearch"
|
|
17
|
+
* - "jiosaavn": "jssearch"
|
|
18
|
+
* - "soundcloud": "scsearch"
|
|
19
|
+
* - "deezer": "dzsearch"
|
|
20
|
+
* - "tidal": "tdsearch"
|
|
21
|
+
* - "applemusic": "amsearch"
|
|
22
|
+
* - "bandcamp": "bcsearch"
|
|
23
|
+
* @param {string} [options.restVersion="v4"] - Version of the REST API.
|
|
24
|
+
* @param {Array<Object>} [options.plugins=[]] - Plugins to load.
|
|
25
|
+
* @param {string} [options.shouldDeleteMessage='none'] - Should delete your message? (true, false)
|
|
26
|
+
* @param {boolean} [options.autoResume=false] - Automatically resume tracks on reconnect.
|
|
27
|
+
* @param {boolean} [options.infiniteReconnects=false] - Reconnect infinitely (default: false).
|
|
28
|
+
*/
|
|
8
29
|
constructor(client, nodes, options) {
|
|
9
30
|
super();
|
|
10
31
|
this.validateInputs(client, nodes, options);
|
|
@@ -22,9 +43,11 @@ class Aqua extends EventEmitter {
|
|
|
22
43
|
this.options = options;
|
|
23
44
|
this.send = options.send;
|
|
24
45
|
this.autoResume = options.autoResume || false;
|
|
46
|
+
this.infiniteReconnects = options.infiniteReconnects || false;
|
|
25
47
|
this.setMaxListeners(0);
|
|
26
48
|
}
|
|
27
49
|
|
|
50
|
+
|
|
28
51
|
validateInputs(client, nodes, options) {
|
|
29
52
|
if (!client) throw new Error("Client is required to initialize Aqua");
|
|
30
53
|
if (!Array.isArray(nodes) || !nodes.length) throw new Error(`Nodes must be a non-empty Array (Received ${typeof nodes})`);
|
|
@@ -78,17 +101,13 @@ class Aqua extends EventEmitter {
|
|
|
78
101
|
}
|
|
79
102
|
}
|
|
80
103
|
|
|
81
|
-
updateVoiceState(
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
player.connection[updateType](packet.d);
|
|
87
|
-
if (packet.d.status === "disconnected") {
|
|
88
|
-
this.cleanupPlayer(player);
|
|
104
|
+
updateVoiceState({ d, t }) {
|
|
105
|
+
const player = this.players.get(d.guild_id);
|
|
106
|
+
if (player && (t === "VOICE_SERVER_UPDATE" || t === "VOICE_STATE_UPDATE" && d.user_id === this.clientId)) {
|
|
107
|
+
player.connection[t === "VOICE_SERVER_UPDATE" ? "setServerUpdate" : "setStateUpdate"](d);
|
|
108
|
+
if (d.status === "disconnected") this.cleanupPlayer(player);
|
|
89
109
|
}
|
|
90
110
|
}
|
|
91
|
-
|
|
92
111
|
fetchRegion(region) {
|
|
93
112
|
if (!region) return this.leastUsedNodes;
|
|
94
113
|
const lowerRegion = region.toLowerCase();
|
|
@@ -104,8 +123,8 @@ class Aqua extends EventEmitter {
|
|
|
104
123
|
|
|
105
124
|
createConnection(options) {
|
|
106
125
|
this.ensureInitialized();
|
|
107
|
-
const
|
|
108
|
-
if (
|
|
126
|
+
const player = this.players.get(options.guildId);
|
|
127
|
+
if (player && player.voiceChannel) return player;
|
|
109
128
|
const node = options.region ? this.fetchRegion(options.region)[0] : this.leastUsedNodes[0];
|
|
110
129
|
if (!node) throw new Error("No nodes are available");
|
|
111
130
|
return this.createPlayer(node, options);
|
|
@@ -121,21 +140,22 @@ class Aqua extends EventEmitter {
|
|
|
121
140
|
return player;
|
|
122
141
|
}
|
|
123
142
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
143
|
+
async destroyPlayer(guildId) {
|
|
144
|
+
const player = this.players.get(guildId);
|
|
145
|
+
if (!player) return;
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
// Ensure that clearData and destroy are awaited if they are async
|
|
149
|
+
await player.clearData(); // Assuming clearData is an async function
|
|
150
|
+
player.removeAllListeners(); // This should not cause an infinite loop
|
|
151
|
+
this.players.delete(guildId);
|
|
152
|
+
this.emit("playerDestroy", player);
|
|
153
|
+
} catch (error) {
|
|
154
|
+
console.error(`Error destroying player for guild ${guildId}:`, error);
|
|
136
155
|
}
|
|
156
|
+
}
|
|
137
157
|
|
|
138
|
-
async resolve({ query, source = this.defaultSearchPlatform, requester, nodes }) {
|
|
158
|
+
async resolve({ query, source = this.defaultSearchPlatform , requester, nodes }) {
|
|
139
159
|
this.ensureInitialized();
|
|
140
160
|
const requestNode = this.getRequestNode(nodes);
|
|
141
161
|
const formattedQuery = this.formatQuery(query, source);
|
|
@@ -263,4 +283,4 @@ class Aqua extends EventEmitter {
|
|
|
263
283
|
}
|
|
264
284
|
}
|
|
265
285
|
|
|
266
|
-
module.exports = { Aqua };
|
|
286
|
+
module.exports = { Aqua };
|
|
@@ -1,85 +1,106 @@
|
|
|
1
1
|
class Connection {
|
|
2
2
|
constructor(player) {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
Object.assign(this, {
|
|
4
|
+
player,
|
|
5
|
+
voice: { sessionId: null, endpoint: null, token: null },
|
|
6
|
+
region: null,
|
|
7
|
+
selfDeaf: false,
|
|
8
|
+
selfMute: false,
|
|
9
|
+
voiceChannel: player.voiceChannel,
|
|
10
|
+
_guildId: player.guildId,
|
|
11
|
+
_aqua: player.aqua,
|
|
12
|
+
_nodes: player.nodes
|
|
13
|
+
});
|
|
11
14
|
}
|
|
12
|
-
|
|
13
|
-
setServerUpdate(
|
|
14
|
-
|
|
15
|
+
|
|
16
|
+
setServerUpdate(data) {
|
|
17
|
+
const endpoint = data.endpoint;
|
|
18
|
+
if (!endpoint) throw new Error("Missing 'endpoint' property");
|
|
19
|
+
|
|
20
|
+
const dotIndex = endpoint.indexOf('.');
|
|
21
|
+
if (dotIndex === -1) return;
|
|
22
|
+
|
|
23
|
+
const newRegion = endpoint.substring(0, dotIndex).replace(/[0-9]/g, '');
|
|
24
|
+
|
|
25
|
+
if (this.region !== newRegion) {
|
|
26
|
+
const prevRegion = this.region;
|
|
15
27
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
this.updatePlayerVoiceData();
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
updateRegion(newRegion, endpoint, token) {
|
|
24
|
-
const previousVoiceRegion = this.region;
|
|
28
|
+
Object.assign(this.voice, {
|
|
29
|
+
endpoint,
|
|
30
|
+
token: data.token
|
|
31
|
+
});
|
|
25
32
|
this.region = newRegion;
|
|
26
|
-
|
|
27
|
-
this.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
|
|
34
|
+
this._aqua.emit(
|
|
35
|
+
"debug",
|
|
36
|
+
`[Player ${this._guildId} - CONNECTION] ${
|
|
37
|
+
prevRegion
|
|
38
|
+
? `Changed Voice Region from ${prevRegion} to ${newRegion}`
|
|
39
|
+
: `Voice Server: ${newRegion}`
|
|
40
|
+
}`
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
this._updatePlayerVoiceData();
|
|
34
45
|
}
|
|
46
|
+
|
|
35
47
|
|
|
36
48
|
setStateUpdate(data) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
49
|
+
const channelId = data.channel_id;
|
|
50
|
+
const sessionId = data.session_id;
|
|
51
|
+
|
|
52
|
+
if (!channelId || !sessionId) {
|
|
53
|
+
this._cleanup();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
41
56
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
57
|
+
if (this.voiceChannel !== channelId) {
|
|
58
|
+
this._aqua.emit("playerMove", this.voiceChannel, channelId);
|
|
59
|
+
this.voiceChannel = channelId;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
Object.assign(this, {
|
|
63
|
+
selfDeaf: data.self_deaf,
|
|
64
|
+
selfMute: data.self_mute
|
|
65
|
+
});
|
|
66
|
+
this.voice.sessionId = sessionId;
|
|
50
67
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
voice: this.voice,
|
|
59
|
-
volume: this.player.volume,
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
this.player.nodes.rest.updatePlayer({
|
|
63
|
-
guildId: this.player.guildId,
|
|
64
|
-
data,
|
|
65
|
-
}).catch(err => {
|
|
66
|
-
this.player.aqua.emit("apiError", "updatePlayer", err);
|
|
67
|
-
});
|
|
68
|
+
|
|
69
|
+
_updatePlayerVoiceData() {
|
|
70
|
+
this._nodes.rest.updatePlayer({
|
|
71
|
+
guildId: this._guildId,
|
|
72
|
+
data: {
|
|
73
|
+
voice: this.voice,
|
|
74
|
+
volume: this.player.volume
|
|
68
75
|
}
|
|
76
|
+
}).catch(err => {
|
|
77
|
+
this._aqua.emit("apiError", "updatePlayer", err);
|
|
78
|
+
});
|
|
69
79
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
|
|
81
|
+
_cleanup() {
|
|
82
|
+
const aqua = this._aqua;
|
|
83
|
+
const channel = this.player.voiceChannel;
|
|
84
|
+
|
|
85
|
+
aqua.emit("playerLeave", channel);
|
|
86
|
+
|
|
87
|
+
this.player.voiceChannel = null;
|
|
88
|
+
this.voiceChannel = null;
|
|
89
|
+
this.player.destroy();
|
|
90
|
+
|
|
91
|
+
aqua.emit("playerDestroy", this.player);
|
|
92
|
+
|
|
93
|
+
Object.assign(this, {
|
|
94
|
+
player: null,
|
|
95
|
+
voice: null,
|
|
96
|
+
region: null,
|
|
97
|
+
selfDeaf: null,
|
|
98
|
+
selfMute: null,
|
|
99
|
+
_guildId: null,
|
|
100
|
+
_aqua: null,
|
|
101
|
+
_nodes: null
|
|
102
|
+
});
|
|
82
103
|
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
module.exports = { Connection };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = { Connection };
|
package/build/structures/Node.js
CHANGED
|
@@ -2,24 +2,21 @@ const WebSocket = require("ws");
|
|
|
2
2
|
const { Rest } = require("./Rest");
|
|
3
3
|
|
|
4
4
|
class Node {
|
|
5
|
-
// Private fields using # for better encapsulation
|
|
6
5
|
#ws = null;
|
|
7
|
-
#statsCache =
|
|
6
|
+
#statsCache = {};
|
|
8
7
|
#lastStatsRequest = 0;
|
|
9
8
|
#reconnectAttempted = 0;
|
|
10
|
-
|
|
11
9
|
constructor(aqua, nodes, options) {
|
|
12
|
-
const {
|
|
13
|
-
name,
|
|
14
|
-
host = "localhost",
|
|
15
|
-
port = 2333,
|
|
10
|
+
const {
|
|
11
|
+
name,
|
|
12
|
+
host = "localhost",
|
|
13
|
+
port = 2333,
|
|
16
14
|
password = "youshallnotpass",
|
|
17
15
|
secure = false,
|
|
18
16
|
sessionId = null,
|
|
19
17
|
regions = []
|
|
20
18
|
} = nodes;
|
|
21
19
|
|
|
22
|
-
// Core properties
|
|
23
20
|
this.aqua = aqua;
|
|
24
21
|
this.name = name || host;
|
|
25
22
|
this.host = host;
|
|
@@ -28,23 +25,16 @@ class Node {
|
|
|
28
25
|
this.secure = secure;
|
|
29
26
|
this.sessionId = sessionId;
|
|
30
27
|
this.regions = regions;
|
|
31
|
-
|
|
32
|
-
// Configuration
|
|
33
28
|
this.wsUrl = new URL(`ws${secure ? 's' : ''}://${host}:${port}/v4/websocket`);
|
|
34
29
|
this.rest = new Rest(aqua, this);
|
|
35
|
-
|
|
36
|
-
// Connection options
|
|
37
30
|
this.resumeKey = options?.resumeKey ?? null;
|
|
38
31
|
this.resumeTimeout = options?.resumeTimeout ?? 60;
|
|
39
32
|
this.autoResume = options?.autoResume ?? false;
|
|
40
33
|
this.reconnectTimeout = options?.reconnectTimeout ?? 2000;
|
|
41
34
|
this.reconnectTries = options?.reconnectTries ?? 3;
|
|
42
|
-
|
|
43
|
-
// State
|
|
35
|
+
this.infiniteReconnects = options?.infiniteReconnects ?? false;
|
|
44
36
|
this.connected = false;
|
|
45
37
|
this.info = null;
|
|
46
|
-
|
|
47
|
-
// Initialize stats with frozen objects to prevent modifications
|
|
48
38
|
this.stats = Object.freeze(this.#createStats());
|
|
49
39
|
}
|
|
50
40
|
|
|
@@ -78,16 +68,13 @@ class Node {
|
|
|
78
68
|
|
|
79
69
|
async connect() {
|
|
80
70
|
this.#cleanup();
|
|
81
|
-
|
|
82
71
|
try {
|
|
83
72
|
this.#ws = new WebSocket(this.wsUrl.href, {
|
|
84
73
|
headers: this.#constructHeaders(),
|
|
85
|
-
perMessageDeflate: false
|
|
74
|
+
perMessageDeflate: false
|
|
86
75
|
});
|
|
87
|
-
|
|
88
76
|
this.#setupWebSocketListeners();
|
|
89
77
|
this.aqua.emit('debug', this.name, 'Connecting...');
|
|
90
|
-
|
|
91
78
|
} catch (err) {
|
|
92
79
|
this.aqua.emit('debug', this.name, `Connection failed: ${err.message}`);
|
|
93
80
|
this.#reconnect();
|
|
@@ -98,9 +85,10 @@ class Node {
|
|
|
98
85
|
if (this.#ws) {
|
|
99
86
|
try {
|
|
100
87
|
this.#ws.removeAllListeners();
|
|
101
|
-
this.#ws.terminate();
|
|
102
|
-
} catch {
|
|
103
|
-
|
|
88
|
+
this.#ws.terminate();
|
|
89
|
+
} catch (err) {
|
|
90
|
+
this.aqua.emit('debug', `Cleanup error: ${err.message}`);
|
|
91
|
+
} finally {
|
|
104
92
|
this.#ws = null;
|
|
105
93
|
}
|
|
106
94
|
}
|
|
@@ -112,18 +100,14 @@ class Node {
|
|
|
112
100
|
"User-Id": this.aqua.clientId,
|
|
113
101
|
"Client-Name": `Aqua/${this.aqua.version}`,
|
|
114
102
|
};
|
|
115
|
-
|
|
116
103
|
if (this.sessionId) headers["Session-Id"] = this.sessionId;
|
|
117
104
|
if (this.resumeKey) headers["Resume-Key"] = this.resumeKey;
|
|
118
|
-
|
|
119
105
|
return Object.freeze(headers);
|
|
120
106
|
}
|
|
121
107
|
|
|
122
108
|
#setupWebSocketListeners() {
|
|
123
109
|
if (!this.#ws) return;
|
|
124
|
-
|
|
125
110
|
const ws = this.#ws;
|
|
126
|
-
|
|
127
111
|
ws.once("open", this.#onOpen.bind(this));
|
|
128
112
|
ws.once("error", this.#onError.bind(this));
|
|
129
113
|
ws.on("message", this.#onMessage.bind(this));
|
|
@@ -133,30 +117,27 @@ class Node {
|
|
|
133
117
|
async #onOpen() {
|
|
134
118
|
this.connected = true;
|
|
135
119
|
this.aqua.emit('debug', this.name, `Connected to ${this.wsUrl.href}`);
|
|
136
|
-
|
|
137
120
|
try {
|
|
138
121
|
this.info = await this.rest.makeRequest("GET", "/v4/info");
|
|
139
122
|
this.autoResume && await this.resumePlayers();
|
|
140
123
|
} catch (err) {
|
|
141
124
|
this.info = null;
|
|
142
|
-
!this.aqua.bypassChecks?.nodeFetchInfo &&
|
|
125
|
+
!this.aqua.bypassChecks?.nodeFetchInfo &&
|
|
143
126
|
this.aqua.emit('error', `Failed to fetch node info: ${err.message}`);
|
|
144
127
|
}
|
|
145
128
|
}
|
|
146
129
|
|
|
147
130
|
async getStats() {
|
|
148
131
|
const now = Date.now();
|
|
149
|
-
const STATS_COOLDOWN = 60000;
|
|
150
|
-
|
|
132
|
+
const STATS_COOLDOWN = 60000;
|
|
151
133
|
if (now - this.#lastStatsRequest < STATS_COOLDOWN) {
|
|
152
|
-
return this.#statsCache.
|
|
134
|
+
return this.#statsCache[this.name] ?? this.stats;
|
|
153
135
|
}
|
|
154
|
-
|
|
155
136
|
try {
|
|
156
137
|
const stats = await this.rest.makeRequest("GET", "/v4/stats");
|
|
157
138
|
this.#updateStats(stats);
|
|
158
139
|
this.#lastStatsRequest = now;
|
|
159
|
-
this.#statsCache.
|
|
140
|
+
this.#statsCache[this.name] = this.stats;
|
|
160
141
|
return this.stats;
|
|
161
142
|
} catch (err) {
|
|
162
143
|
this.aqua.emit('debug', `Stats fetch error: ${err.message}`);
|
|
@@ -166,7 +147,6 @@ class Node {
|
|
|
166
147
|
|
|
167
148
|
#updateStats(payload) {
|
|
168
149
|
if (!payload) return;
|
|
169
|
-
|
|
170
150
|
const newStats = {
|
|
171
151
|
players: payload.players ?? 0,
|
|
172
152
|
playingPlayers: payload.playingPlayers ?? 0,
|
|
@@ -176,19 +156,20 @@ class Node {
|
|
|
176
156
|
cpu: this.#updateCpuStats(payload.cpu),
|
|
177
157
|
frameStats: this.#updateFrameStats(payload.frameStats)
|
|
178
158
|
};
|
|
179
|
-
|
|
180
159
|
this.stats = Object.freeze(newStats);
|
|
181
160
|
}
|
|
182
161
|
|
|
183
162
|
#updateMemoryStats(memory = {}) {
|
|
184
163
|
const allocated = memory.allocated ?? 0;
|
|
164
|
+
const free = memory.free ?? 0;
|
|
165
|
+
const used = memory.used ?? 0;
|
|
185
166
|
return Object.freeze({
|
|
186
|
-
free
|
|
187
|
-
used
|
|
167
|
+
free,
|
|
168
|
+
used,
|
|
188
169
|
allocated,
|
|
189
170
|
reservable: memory.reservable ?? 0,
|
|
190
|
-
freePercentage: allocated ? (
|
|
191
|
-
usedPercentage: allocated ? (
|
|
171
|
+
freePercentage: allocated ? (free / allocated) * 100 : 0,
|
|
172
|
+
usedPercentage: allocated ? (used / allocated) * 100 : 0
|
|
192
173
|
});
|
|
193
174
|
}
|
|
194
175
|
|
|
@@ -202,20 +183,25 @@ class Node {
|
|
|
202
183
|
});
|
|
203
184
|
}
|
|
204
185
|
|
|
205
|
-
#updateFrameStats() {
|
|
186
|
+
#updateFrameStats(frameStats = {}) {
|
|
187
|
+
if (!frameStats) {
|
|
188
|
+
return Object.freeze({
|
|
189
|
+
sent: 0,
|
|
190
|
+
nulled: 0,
|
|
191
|
+
deficit: 0
|
|
192
|
+
});
|
|
193
|
+
}
|
|
206
194
|
return Object.freeze({
|
|
207
|
-
sent: 0,
|
|
208
|
-
nulled: 0,
|
|
209
|
-
deficit: 0
|
|
195
|
+
sent: frameStats.sent ?? 0,
|
|
196
|
+
nulled: frameStats.nulled ?? 0,
|
|
197
|
+
deficit: frameStats.deficit ?? 0
|
|
210
198
|
});
|
|
211
199
|
}
|
|
212
200
|
|
|
213
|
-
// More optimized message handling
|
|
214
201
|
#onMessage(msg) {
|
|
215
202
|
try {
|
|
216
203
|
const payload = JSON.parse(msg.toString());
|
|
217
204
|
if (!payload?.op) return;
|
|
218
|
-
|
|
219
205
|
switch (payload.op) {
|
|
220
206
|
case "stats":
|
|
221
207
|
this.#updateStats(payload);
|
|
@@ -255,32 +241,33 @@ class Node {
|
|
|
255
241
|
}
|
|
256
242
|
|
|
257
243
|
#reconnect() {
|
|
244
|
+
if (this.infiniteReconnects) {
|
|
245
|
+
this.aqua.emit("nodeReconnect", this, console.log("Experimental infinite reconnects enabled, will be trying non-stop..."));
|
|
246
|
+
this.connect();
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
258
249
|
if (++this.#reconnectAttempted >= this.reconnectTries) {
|
|
259
250
|
this.aqua.emit("nodeError", this, new Error(`Max reconnection attempts reached (${this.reconnectTries})`));
|
|
251
|
+
clearTimeout(this.reconnectTimeoutId);
|
|
260
252
|
return this.destroy();
|
|
261
253
|
}
|
|
262
|
-
|
|
263
|
-
setTimeout(() => {
|
|
264
|
-
this.aqua.emit("nodeReconnect", this);
|
|
254
|
+
clearTimeout(this.reconnectTimeoutId);
|
|
255
|
+
this.reconnectTimeoutId = setTimeout(() => {
|
|
256
|
+
this.aqua.emit("nodeReconnect", this, this.#reconnectAttempted);
|
|
265
257
|
this.connect();
|
|
266
|
-
}, this.reconnectTimeout * this.#reconnectAttempted); // Exponential backoff
|
|
258
|
+
}, this.reconnectTimeout * Math.pow(2, this.#reconnectAttempted)); // Exponential backoff
|
|
267
259
|
}
|
|
268
260
|
|
|
269
|
-
// Performance optimized penalties calculation
|
|
270
261
|
get penalties() {
|
|
271
262
|
if (!this.connected) return Number.MAX_SAFE_INTEGER;
|
|
272
|
-
|
|
273
263
|
let penalties = this.stats.players;
|
|
274
|
-
|
|
275
264
|
if (this.stats.cpu?.systemLoad) {
|
|
276
265
|
penalties += Math.round(Math.pow(1.05, 100 * this.stats.cpu.systemLoad) * 10 - 10);
|
|
277
266
|
}
|
|
278
|
-
|
|
279
267
|
if (this.stats.frameStats) {
|
|
280
268
|
penalties += this.stats.frameStats.deficit;
|
|
281
269
|
penalties += this.stats.frameStats.nulled * 2;
|
|
282
270
|
}
|
|
283
|
-
|
|
284
271
|
return penalties;
|
|
285
272
|
}
|
|
286
273
|
|
|
@@ -290,8 +277,6 @@ class Node {
|
|
|
290
277
|
this.aqua.nodes.delete(this.name);
|
|
291
278
|
return;
|
|
292
279
|
}
|
|
293
|
-
|
|
294
|
-
// Cleanup all players using this node
|
|
295
280
|
if (this.connected) {
|
|
296
281
|
for (const player of this.aqua.players.values()) {
|
|
297
282
|
if (player.node === this) {
|
|
@@ -299,17 +284,14 @@ class Node {
|
|
|
299
284
|
}
|
|
300
285
|
}
|
|
301
286
|
}
|
|
302
|
-
|
|
303
287
|
this.#cleanup();
|
|
304
288
|
this.connected = false;
|
|
305
289
|
this.aqua.nodeMap.delete(this.name);
|
|
306
290
|
this.aqua.emit("nodeDestroy", this);
|
|
307
|
-
|
|
308
|
-
// Clear references
|
|
309
291
|
this.info = null;
|
|
310
|
-
this.#statsCache =
|
|
292
|
+
this.#statsCache = {};
|
|
311
293
|
this.stats = Object.freeze(this.#createStats());
|
|
312
294
|
}
|
|
313
295
|
}
|
|
314
296
|
|
|
315
|
-
module.exports = { Node };
|
|
297
|
+
module.exports = { Node };
|
|
@@ -30,11 +30,8 @@ class Player extends EventEmitter {
|
|
|
30
30
|
this.previousTracks = [];
|
|
31
31
|
this.shouldDeleteMessage = options.shouldDeleteMessage ?? true;
|
|
32
32
|
|
|
33
|
-
this.
|
|
34
|
-
this.
|
|
35
|
-
|
|
36
|
-
this.on("playerUpdate", this._boundPlayerUpdate);
|
|
37
|
-
this.on("event", this._boundHandleEvent);
|
|
33
|
+
this.on("playerUpdate", this.onPlayerUpdate.bind(this));
|
|
34
|
+
this.on("event", this.handleEvent.bind(this));
|
|
38
35
|
}
|
|
39
36
|
|
|
40
37
|
onPlayerUpdate(packet) {
|
|
@@ -58,162 +55,96 @@ class Player extends EventEmitter {
|
|
|
58
55
|
}
|
|
59
56
|
this.previousTracks.unshift(track);
|
|
60
57
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
* @throws {Error} If the player is not connected.
|
|
65
|
-
* @returns {Promise<Player>} The player instance.
|
|
66
|
-
*/
|
|
67
|
-
play() {
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
async play() {
|
|
68
61
|
if (!this.connected) throw new Error("Player must be connected first.");
|
|
69
62
|
if (!this.queue.length) return;
|
|
70
|
-
|
|
63
|
+
|
|
71
64
|
const track = this.queue.shift();
|
|
72
|
-
|
|
73
|
-
|
|
65
|
+
|
|
66
|
+
this.current = track.track ? track : await track.resolve(this.aqua);
|
|
67
|
+
|
|
74
68
|
this.playing = true;
|
|
75
69
|
this.position = 0;
|
|
76
|
-
|
|
77
70
|
this.aqua.emit("debug", this.guildId, `Playing track: ${this.current.track}`);
|
|
78
|
-
|
|
71
|
+
this.updatePlayer({ track: { encoded: this.current.track } });
|
|
79
72
|
return this;
|
|
80
73
|
}
|
|
81
|
-
/**
|
|
82
|
-
* Connects the player to a specified voice channel.
|
|
83
|
-
*
|
|
84
|
-
* @param {Object} options - Options for connecting the player.
|
|
85
|
-
* @param {string} options.guildId - The ID of the guild.
|
|
86
|
-
* @param {string} options.voiceChannel - The ID of the voice channel to connect to.
|
|
87
|
-
* @param {boolean} [options.deaf=true] - Whether the player should be self-deafened.
|
|
88
|
-
* @param {boolean} [options.mute=false] - Whether the player should be self-muted.
|
|
89
|
-
* @throws {Error} If the player is already connected.
|
|
90
|
-
* @returns {Promise<Player>} The player instance.
|
|
91
|
-
*/
|
|
92
|
-
connect(options) {
|
|
93
|
-
if (this.connected) throw new Error("Player is already connected.");
|
|
94
|
-
|
|
95
|
-
const {
|
|
96
|
-
guildId,
|
|
97
|
-
voiceChannel,
|
|
98
|
-
deaf = true,
|
|
99
|
-
mute = false
|
|
100
|
-
} = options;
|
|
101
74
|
|
|
75
|
+
connect(options) {
|
|
76
|
+
if (this.connected) throw new Error("Player is already connected.");
|
|
77
|
+
const { guildId, voiceChannel, deaf = true, mute = false } = options;
|
|
102
78
|
this.send({
|
|
103
79
|
guild_id: guildId,
|
|
104
80
|
channel_id: voiceChannel,
|
|
105
81
|
self_deaf: deaf,
|
|
106
82
|
self_mute: mute
|
|
107
83
|
});
|
|
108
|
-
|
|
109
84
|
this.connected = true;
|
|
110
85
|
this.aqua.emit("debug", this.guildId, `Player connected to voice channel: ${voiceChannel}.`);
|
|
111
86
|
return this;
|
|
112
87
|
}
|
|
113
88
|
|
|
114
|
-
|
|
89
|
+
destroy() {
|
|
115
90
|
if (!this.connected) return this;
|
|
116
|
-
|
|
117
|
-
this.
|
|
118
|
-
this.
|
|
119
|
-
this.
|
|
120
|
-
this.playing = false;
|
|
121
|
-
this.position = 0;
|
|
122
|
-
this.send({ guild_id: this.guildId, channel_id: null });
|
|
123
|
-
this.connected = false;
|
|
124
|
-
|
|
125
|
-
this.removeListener("playerUpdate", this._boundPlayerUpdate);
|
|
126
|
-
this.removeListener("event", this._boundHandleEvent);
|
|
127
|
-
|
|
91
|
+
this.disconnect();
|
|
92
|
+
this.nowPlayingMessage?.delete().catch(() => { }); // ignore the error
|
|
93
|
+
this.aqua.destroyPlayer(this.guildId);
|
|
94
|
+
this.nodes.rest.destroyPlayer(this.guildId);
|
|
128
95
|
return this;
|
|
129
96
|
}
|
|
130
|
-
/**
|
|
131
|
-
* Pauses or resumes the player.
|
|
132
|
-
*
|
|
133
|
-
* @param {boolean} paused - If true, the player will be paused; if false, it will resume.
|
|
134
|
-
* @returns {Promise<Player>} The player instance.
|
|
135
|
-
*/
|
|
136
97
|
|
|
137
|
-
|
|
98
|
+
pause(paused) {
|
|
138
99
|
this.paused = paused;
|
|
139
|
-
|
|
100
|
+
this.updatePlayer({ paused });
|
|
140
101
|
return this;
|
|
141
102
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
*
|
|
145
|
-
* @param {number} position - The position in milliseconds to seek to.
|
|
146
|
-
* @throws {Error} If the position is negative.
|
|
147
|
-
* @returns {Promise<Player>} The player instance.
|
|
148
|
-
*/
|
|
149
|
-
seek(position) {
|
|
103
|
+
|
|
104
|
+
seek(position) {
|
|
150
105
|
if (position < 0) throw new Error("Seek position cannot be negative.");
|
|
151
106
|
if (!this.playing) return this;
|
|
152
|
-
|
|
153
107
|
this.position = position;
|
|
154
|
-
|
|
108
|
+
this.updatePlayer({ position });
|
|
155
109
|
return this;
|
|
156
110
|
}
|
|
157
|
-
|
|
111
|
+
|
|
112
|
+
stop() {
|
|
158
113
|
if (!this.playing) return this;
|
|
159
|
-
|
|
114
|
+
this.updatePlayer({ track: { encoded: null } });
|
|
160
115
|
this.playing = false;
|
|
161
116
|
this.position = 0;
|
|
162
117
|
return this;
|
|
163
118
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
*
|
|
167
|
-
* @param {number} volume - The volume to set, between 0 and 200.
|
|
168
|
-
* @throws {Error} If the volume is out of range.
|
|
169
|
-
* @returns {Promise<Player>} The player instance.
|
|
170
|
-
*/
|
|
171
|
-
setVolume(volume) {
|
|
119
|
+
|
|
120
|
+
setVolume(volume) {
|
|
172
121
|
if (volume < 0 || volume > 200) throw new Error("Volume must be between 0 and 200.");
|
|
173
122
|
this.volume = volume;
|
|
174
|
-
|
|
123
|
+
this.updatePlayer({ volume });
|
|
175
124
|
return this;
|
|
176
125
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
* @returns {Promise<Player>} The player instance.
|
|
183
|
-
*/
|
|
184
|
-
setLoop(mode) {
|
|
185
|
-
const validModes = new Set(["none", "track", "queue"]);
|
|
186
|
-
if (!validModes.has(mode)) throw new Error("Loop mode must be 'none', 'track', or 'queue'.");
|
|
126
|
+
|
|
127
|
+
static validModes = new Set(["none", "track", "queue"]);
|
|
128
|
+
|
|
129
|
+
setLoop(mode) {
|
|
130
|
+
if (!Player.validModes.has(mode)) throw new Error("Loop mode must be 'none', 'track', or 'queue'.");
|
|
187
131
|
this.loop = mode;
|
|
188
|
-
|
|
132
|
+
this.updatePlayer({ loop: mode });
|
|
189
133
|
return this;
|
|
190
134
|
}
|
|
191
|
-
/**
|
|
192
|
-
* Sets the text channel for the player.
|
|
193
|
-
*
|
|
194
|
-
* @param {string} channel - The ID of the text channel to set.
|
|
195
|
-
* @returns {Promise<Player>} The player instance.
|
|
196
|
-
*/
|
|
197
135
|
|
|
198
|
-
|
|
199
|
-
|
|
136
|
+
setTextChannel(channel) {
|
|
137
|
+
this.updatePlayer({ text_channel: channel });
|
|
200
138
|
return this;
|
|
201
139
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
*
|
|
205
|
-
* @param {string} channel - The ID of the voice channel to set.
|
|
206
|
-
* @throws {TypeError} If the channel is not a non-empty string.
|
|
207
|
-
* @throws {ReferenceError} If the player is already connected to the channel.
|
|
208
|
-
* @returns {Promise<Player>} The player instance.
|
|
209
|
-
*/
|
|
210
|
-
setVoiceChannel(channel) {
|
|
140
|
+
|
|
141
|
+
setVoiceChannel(channel) {
|
|
211
142
|
if (!channel?.length) throw new TypeError("Channel must be a non-empty string.");
|
|
212
143
|
if (this.connected && channel === this.voiceChannel) {
|
|
213
144
|
throw new ReferenceError(`Player already connected to ${channel}.`);
|
|
214
145
|
}
|
|
215
146
|
this.voiceChannel = channel;
|
|
216
|
-
|
|
147
|
+
this.connect({
|
|
217
148
|
deaf: this.deaf,
|
|
218
149
|
guildId: this.guildId,
|
|
219
150
|
voiceChannel: channel,
|
|
@@ -222,14 +153,14 @@ class Player extends EventEmitter {
|
|
|
222
153
|
return this;
|
|
223
154
|
}
|
|
224
155
|
|
|
225
|
-
|
|
226
|
-
|
|
156
|
+
disconnect() {
|
|
157
|
+
this.updatePlayer({ track: { encoded: null } });
|
|
227
158
|
this.connected = false;
|
|
228
159
|
this.send({ guild_id: this.guildId, channel_id: null });
|
|
229
160
|
this.aqua.emit("debug", this.guildId, "Player disconnected.");
|
|
230
161
|
}
|
|
231
162
|
|
|
232
|
-
|
|
163
|
+
shuffle() {
|
|
233
164
|
const len = this.queue.length;
|
|
234
165
|
for (let i = len - 1; i > 0; i--) {
|
|
235
166
|
const j = Math.floor(Math.random() * (i + 1));
|
|
@@ -242,12 +173,12 @@ class Player extends EventEmitter {
|
|
|
242
173
|
return this.queue;
|
|
243
174
|
}
|
|
244
175
|
|
|
245
|
-
|
|
176
|
+
replay() {
|
|
246
177
|
return this.seek(0);
|
|
247
178
|
}
|
|
248
179
|
|
|
249
|
-
|
|
250
|
-
|
|
180
|
+
skip() {
|
|
181
|
+
this.stop();
|
|
251
182
|
return this.playing ? this.play() : undefined;
|
|
252
183
|
}
|
|
253
184
|
|
|
@@ -263,16 +194,14 @@ class Player extends EventEmitter {
|
|
|
263
194
|
handleEvent = (payload) => {
|
|
264
195
|
const player = this.aqua.players.get(payload.guildId);
|
|
265
196
|
if (!player) return;
|
|
266
|
-
|
|
267
197
|
const track = player.current;
|
|
268
198
|
const handlerName = Player.EVENT_HANDLERS.get(payload.type);
|
|
269
|
-
|
|
270
199
|
if (handlerName) {
|
|
271
200
|
this[handlerName](player, track, payload);
|
|
272
201
|
} else {
|
|
273
202
|
this.handleUnknownEvent(player, track, payload);
|
|
274
203
|
}
|
|
275
|
-
}
|
|
204
|
+
};
|
|
276
205
|
|
|
277
206
|
trackStart(player, track) {
|
|
278
207
|
this.playing = true;
|
|
@@ -290,15 +219,13 @@ class Player extends EventEmitter {
|
|
|
290
219
|
if (this.shouldDeleteMessage && this.nowPlayingMessage) {
|
|
291
220
|
try {
|
|
292
221
|
await this.nowPlayingMessage.delete();
|
|
293
|
-
} catch {
|
|
294
|
-
//
|
|
222
|
+
} catch (error) {
|
|
223
|
+
// Consider logging specific errors
|
|
295
224
|
} finally {
|
|
296
225
|
this.nowPlayingMessage = null;
|
|
297
226
|
}
|
|
298
227
|
}
|
|
299
|
-
|
|
300
228
|
const reason = payload.reason.replace("_", "").toLowerCase();
|
|
301
|
-
|
|
302
229
|
if (reason === "loadfailed" || reason === "cleanup") {
|
|
303
230
|
if (player.queue.isEmpty()) {
|
|
304
231
|
this.aqua.emit("queueEnd", player);
|
|
@@ -306,7 +233,6 @@ class Player extends EventEmitter {
|
|
|
306
233
|
}
|
|
307
234
|
return player.play();
|
|
308
235
|
}
|
|
309
|
-
|
|
310
236
|
switch (this.loop) {
|
|
311
237
|
case "track":
|
|
312
238
|
this.aqua.emit("trackRepeat", player, track);
|
|
@@ -317,13 +243,11 @@ class Player extends EventEmitter {
|
|
|
317
243
|
player.queue.push(track);
|
|
318
244
|
break;
|
|
319
245
|
}
|
|
320
|
-
|
|
321
246
|
if (player.queue.isEmpty()) {
|
|
322
247
|
this.playing = false;
|
|
323
248
|
this.aqua.emit("queueEnd", player);
|
|
324
249
|
return this.cleanup();
|
|
325
250
|
}
|
|
326
|
-
|
|
327
251
|
return player.play();
|
|
328
252
|
}
|
|
329
253
|
|
|
@@ -366,7 +290,7 @@ class Player extends EventEmitter {
|
|
|
366
290
|
}
|
|
367
291
|
|
|
368
292
|
clearData() {
|
|
369
|
-
this.#dataStore
|
|
293
|
+
this.#dataStore.delete()
|
|
370
294
|
return this;
|
|
371
295
|
}
|
|
372
296
|
|
|
@@ -384,10 +308,10 @@ class Player extends EventEmitter {
|
|
|
384
308
|
|
|
385
309
|
async cleanup() {
|
|
386
310
|
if (!this.playing && !this.paused && this.queue.isEmpty()) {
|
|
387
|
-
|
|
311
|
+
this.destroy();
|
|
388
312
|
}
|
|
389
313
|
this.clearData();
|
|
390
314
|
}
|
|
391
315
|
}
|
|
392
316
|
|
|
393
|
-
module.exports = { Player };
|
|
317
|
+
module.exports = { Player };
|
package/build/structures/Rest.js
CHANGED
|
@@ -5,29 +5,32 @@ const { getImageUrl } = require("../handlers/fetchImage");
|
|
|
5
5
|
* @typedef {import("../structures/Player")} Player
|
|
6
6
|
* @typedef {import("../structures/Node")} Node
|
|
7
7
|
*/
|
|
8
|
+
|
|
8
9
|
class Track {
|
|
9
10
|
/**
|
|
10
|
-
* @param {
|
|
11
|
+
* @param {Object} data
|
|
11
12
|
* @param {Player} requester
|
|
12
13
|
* @param {Node} nodes
|
|
13
14
|
*/
|
|
14
15
|
constructor(data, requester, nodes) {
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
16
|
+
const info = data?.info || {};
|
|
17
|
+
|
|
18
|
+
this.info = {
|
|
19
|
+
identifier: info.identifier || '',
|
|
20
|
+
isSeekable: !!info.isSeekable,
|
|
21
|
+
author: info.author || '',
|
|
22
|
+
length: ~~info.length,
|
|
23
|
+
isStream: !!info.isStream,
|
|
24
|
+
title: info.title || '',
|
|
25
|
+
uri: info.uri || '',
|
|
26
|
+
sourceName: info.sourceName || '',
|
|
27
|
+
artworkUrl: info.artworkUrl || ''
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
this.track = data?.encoded || null;
|
|
31
|
+
this.playlist = data?.playlist || null;
|
|
27
32
|
this.requester = requester;
|
|
28
33
|
this.nodes = nodes;
|
|
29
|
-
this.track = encoded;
|
|
30
|
-
this.playlist = playlist;
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
/**
|
|
@@ -39,13 +42,16 @@ class Track {
|
|
|
39
42
|
return thumbnail.startsWith("http") ? thumbnail : getImageUrl(thumbnail, this.nodes);
|
|
40
43
|
}
|
|
41
44
|
|
|
45
|
+
/**
|
|
46
|
+
* @param {Aqua} aqua
|
|
47
|
+
* @returns {Promise<Track|null>}
|
|
48
|
+
*/
|
|
42
49
|
async resolve(aqua) {
|
|
43
50
|
if (!aqua?.options?.defaultSearchPlatform) return null;
|
|
44
51
|
|
|
45
52
|
try {
|
|
46
|
-
const query = `${this.info.author} - ${this.info.title}`;
|
|
47
53
|
const result = await aqua.resolve({
|
|
48
|
-
query,
|
|
54
|
+
query: this.info.author + ' - ' + this.info.title,
|
|
49
55
|
source: aqua.options.defaultSearchPlatform,
|
|
50
56
|
requester: this.requester,
|
|
51
57
|
node: this.nodes
|
|
@@ -53,52 +59,54 @@ class Track {
|
|
|
53
59
|
|
|
54
60
|
if (!result?.tracks?.length) return null;
|
|
55
61
|
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
if (matchedTrack) {
|
|
59
|
-
this.updateTrackInfo(matchedTrack);
|
|
60
|
-
return this;
|
|
61
|
-
}
|
|
62
|
+
const track = this._findMatchingTrack(result.tracks);
|
|
63
|
+
if (!track) return null;
|
|
62
64
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
} catch
|
|
66
|
-
console.error('Error resolving track:', error);
|
|
65
|
+
this._updateTrack(track);
|
|
66
|
+
return this;
|
|
67
|
+
} catch {
|
|
67
68
|
return null;
|
|
68
69
|
}
|
|
69
70
|
}
|
|
70
71
|
|
|
71
|
-
|
|
72
|
+
/**
|
|
73
|
+
* @private
|
|
74
|
+
*/
|
|
75
|
+
_findMatchingTrack(tracks) {
|
|
72
76
|
const { author, title, length } = this.info;
|
|
73
|
-
const { author: tAuthor, title: tTitle, length: tLength } = track.info;
|
|
74
77
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
+
for (let i = 0; i < tracks.length; i++) {
|
|
79
|
+
const track = tracks[i];
|
|
80
|
+
const tInfo = track.info;
|
|
81
|
+
|
|
82
|
+
if (tInfo.author === author &&
|
|
83
|
+
tInfo.title === title &&
|
|
84
|
+
(!length || Math.abs(tInfo.length - length) <= 2000)) {
|
|
85
|
+
return track;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return tracks[0];
|
|
78
90
|
}
|
|
79
91
|
|
|
80
92
|
/**
|
|
81
|
-
* @
|
|
93
|
+
* @private
|
|
82
94
|
*/
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
this.info = Object.freeze({
|
|
86
|
-
...this.info,
|
|
87
|
-
identifier: track.info.identifier
|
|
88
|
-
});
|
|
89
|
-
|
|
95
|
+
_updateTrack(track) {
|
|
96
|
+
this.info.identifier = track.info.identifier;
|
|
90
97
|
this.track = track.track;
|
|
91
98
|
this.playlist = track.playlist || null;
|
|
92
99
|
}
|
|
93
100
|
|
|
94
101
|
/**
|
|
95
|
-
*
|
|
102
|
+
* Fast cleanup
|
|
96
103
|
*/
|
|
97
104
|
destroy() {
|
|
98
105
|
this.requester = null;
|
|
99
106
|
this.nodes = null;
|
|
100
107
|
this.track = null;
|
|
101
108
|
this.playlist = null;
|
|
109
|
+
this.info = null;
|
|
102
110
|
}
|
|
103
111
|
}
|
|
104
112
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aqualink",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0-beta1",
|
|
4
4
|
"description": "An Lavalink wrapper, focused in speed, performance, and features, Based in Riffy!",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -48,8 +48,5 @@
|
|
|
48
48
|
"name": "mushroom0162",
|
|
49
49
|
"url": "https://github.com/ToddyTheNoobDud"
|
|
50
50
|
}
|
|
51
|
-
]
|
|
52
|
-
"devDependencies": {
|
|
53
|
-
"discord.js": "^14.16.3"
|
|
54
|
-
}
|
|
51
|
+
]
|
|
55
52
|
}
|