aqualink 1.3.0 → 1.4.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 +13 -10
- package/build/handlers/fetchImage.js +30 -20
- package/build/structures/Aqua.js +107 -180
- package/build/structures/Node.js +48 -18
- package/build/structures/Player.js +162 -231
- package/build/structures/Rest.js +32 -33
- package/build/structures/Track.js +74 -64
- package/package.json +1 -2
package/README.md
CHANGED
|
@@ -25,16 +25,19 @@ 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
|
-
# Yay, Version 1.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
^^ Also
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
28
|
+
# Yay, Version 1.4.0 is released ! aqualink so cool
|
|
29
|
+
|
|
30
|
+
- Misc changes to TRACK handler (fixed some bugs, improved overall speed and caching)
|
|
31
|
+
- Reworked some stuff from FetchImage (improves speed, made the requests dynamic, remove useless caching)
|
|
32
|
+
- Rewrited Player.js (Detailed: Rewrite Events (use switch, new methods), Rewrite disconnect, stop and destroy, improve cleanUp system)
|
|
33
|
+
|
|
34
|
+
^^ Also fixes a lot of bugs (example: create a connection first, needs an connection, and others i forgot)
|
|
35
|
+
|
|
36
|
+
- Rewrite the REST system again (uses async, dynamic requests, and re-utilizes some stuff for better speed)
|
|
37
|
+
- Remade NODE manager (this brings better speed, dynamic requests, better caching system, and faster connecting speeds)
|
|
38
|
+
- Rewrited the AQUA manager (this makes the resolve 2x faster, uses better functions, auto clean up system, better overall manager and faster)
|
|
39
|
+
- Misc Fixes: fixed the queue looping system, improved the finished handler, better error handler, more automations
|
|
40
|
+
|
|
38
41
|
|
|
39
42
|
# How to install
|
|
40
43
|
|
|
@@ -1,39 +1,49 @@
|
|
|
1
|
-
const
|
|
1
|
+
const { request } = require("undici");
|
|
2
|
+
|
|
3
|
+
const YOUTUBE_URLS = [
|
|
4
|
+
(id) => `https://img.youtube.com/vi/${id}/maxresdefault.jpg`,
|
|
5
|
+
(id) => `https://img.youtube.com/vi/${id}/hqdefault.jpg`,
|
|
6
|
+
(id) => `https://img.youtube.com/vi/${id}/mqdefault.jpg`,
|
|
7
|
+
(id) => `https://img.youtube.com/vi/${id}/default.jpg`,
|
|
8
|
+
];
|
|
2
9
|
|
|
3
10
|
async function getImageUrl(info) {
|
|
4
11
|
if (!info || !info.sourceName || !info.uri) return null;
|
|
5
12
|
|
|
6
13
|
switch (info.sourceName.toLowerCase()) {
|
|
7
14
|
case "spotify":
|
|
8
|
-
return
|
|
9
|
-
|
|
10
|
-
case "soundcloud":
|
|
11
|
-
return (await fetchFromUrl(`https://soundcloud.com/oembed?format=json&url=${info.uri}`))?.json().then(json => json.thumbnail_url || null);
|
|
12
|
-
|
|
15
|
+
return await fetchThumbnail(`https://open.spotify.com/oembed?url=${info.uri}`);
|
|
13
16
|
case "youtube":
|
|
14
|
-
|
|
15
|
-
`https://img.youtube.com/vi/${info.identifier}/maxresdefault.jpg`,
|
|
16
|
-
`https://img.youtube.com/vi/${info.identifier}/hqdefault.jpg`,
|
|
17
|
-
`https://img.youtube.com/vi/${info.identifier}/mqdefault.jpg`,
|
|
18
|
-
`https://img.youtube.com/vi/${info.identifier}/default.jpg`,
|
|
19
|
-
];
|
|
20
|
-
|
|
21
|
-
const firstValidUrl = await Promise.any(urls.map(url => fetchFromUrl(url)));
|
|
22
|
-
return firstValidUrl ? firstValidUrl.url : null;
|
|
23
|
-
|
|
17
|
+
return await fetchYouTubeThumbnail(info.identifier);
|
|
24
18
|
default:
|
|
25
19
|
return null;
|
|
26
20
|
}
|
|
27
21
|
}
|
|
28
22
|
|
|
29
|
-
async function
|
|
23
|
+
async function fetchThumbnail(url) {
|
|
30
24
|
try {
|
|
31
|
-
const response = await
|
|
32
|
-
|
|
25
|
+
const response = await request(url, { method: "GET" });
|
|
26
|
+
if (response.ok) {
|
|
27
|
+
const json = await response.json();
|
|
28
|
+
return json.thumbnail_url || null;
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
33
31
|
} catch (error) {
|
|
34
32
|
console.error(`Error fetching ${url}:`, error);
|
|
35
33
|
return null;
|
|
36
34
|
}
|
|
37
35
|
}
|
|
38
36
|
|
|
39
|
-
|
|
37
|
+
async function fetchYouTubeThumbnail(identifier) {
|
|
38
|
+
const fetchPromises = YOUTUBE_URLS.map(urlFunc => fetchThumbnail(urlFunc(identifier)));
|
|
39
|
+
const results = await Promise.allSettled(fetchPromises);
|
|
40
|
+
|
|
41
|
+
for (const result of results) {
|
|
42
|
+
if (result.status === "fulfilled" && result.value) {
|
|
43
|
+
return result.value;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = { getImageUrl };
|
package/build/structures/Aqua.js
CHANGED
|
@@ -5,57 +5,36 @@ const { Track } = require("./Track");
|
|
|
5
5
|
const { version: pkgVersion } = require("../../package.json");
|
|
6
6
|
|
|
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.
|
|
14
|
-
* @param {string} [options.restVersion="v4"] - Version of the REST API.
|
|
15
|
-
* @param {Array<Object>} [options.plugins=[]] - Plugins to load.
|
|
16
|
-
* @param {string} [options.shouldDeleteMessage='none'] - Should delete your message? (true, false)
|
|
17
|
-
*/
|
|
18
8
|
constructor(client, nodes, options) {
|
|
19
9
|
super();
|
|
20
|
-
|
|
21
|
-
if (!Array.isArray(nodes) || nodes.length === 0) throw new Error(`Nodes must be a non-empty Array (Received ${typeof nodes})`);
|
|
22
|
-
if (typeof options.send !== "function") throw new Error("Send function is required to initialize Aqua");
|
|
23
|
-
|
|
10
|
+
this.validateInputs(client, nodes, options);
|
|
24
11
|
this.client = client;
|
|
25
12
|
this.nodes = nodes;
|
|
26
13
|
this.nodeMap = new Map();
|
|
27
14
|
this.players = new Map();
|
|
28
15
|
this.clientId = null;
|
|
29
16
|
this.initiated = false;
|
|
30
|
-
this.
|
|
31
|
-
this.
|
|
32
|
-
this.defaultSearchPlatform = options.defaultSearchPlatform || "ytmsearch";
|
|
17
|
+
this.shouldDeleteMessage = options.shouldDeleteMessage || false;
|
|
18
|
+
this.defaultSearchPlatform = options.defaultSearchPlatform || "ytsearch";
|
|
33
19
|
this.restVersion = options.restVersion || "v3";
|
|
34
20
|
this.plugins = options.plugins || [];
|
|
35
21
|
this.version = pkgVersion;
|
|
36
|
-
this.loadType = null;
|
|
37
|
-
this.tracks = [];
|
|
38
|
-
this.playlistInfo = null;
|
|
39
|
-
this.pluginInfo = null;
|
|
40
22
|
this.options = options;
|
|
41
|
-
this.send = options.send
|
|
23
|
+
this.send = options.send;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
validateInputs(client, nodes, options) {
|
|
27
|
+
if (!client) throw new Error("Client is required to initialize Aqua");
|
|
28
|
+
if (!Array.isArray(nodes) || nodes.length === 0) throw new Error(`Nodes must be a non-empty Array (Received ${typeof nodes})`);
|
|
29
|
+
if (typeof options.send !== "function") throw new Error("Send function is required to initialize Aqua");
|
|
42
30
|
}
|
|
43
31
|
|
|
44
|
-
/**
|
|
45
|
-
* Gets the least used nodes based on call count.
|
|
46
|
-
* @returns {Array<Node>} Array of least used nodes.
|
|
47
|
-
*/
|
|
48
32
|
get leastUsedNodes() {
|
|
49
33
|
return [...this.nodeMap.values()]
|
|
50
34
|
.filter(node => node.connected)
|
|
51
35
|
.sort((a, b) => a.rest.calls - b.rest.calls);
|
|
52
36
|
}
|
|
53
37
|
|
|
54
|
-
/**
|
|
55
|
-
* Initializes Aqua with the provided client ID.
|
|
56
|
-
* @param {string} clientId - The client ID
|
|
57
|
-
* @returns {Aqua} The Aqua instance.
|
|
58
|
-
*/
|
|
59
38
|
init(clientId) {
|
|
60
39
|
if (this.initiated) return this;
|
|
61
40
|
this.clientId = clientId;
|
|
@@ -65,11 +44,6 @@ class Aqua extends EventEmitter {
|
|
|
65
44
|
return this;
|
|
66
45
|
}
|
|
67
46
|
|
|
68
|
-
/**
|
|
69
|
-
* Creates a new node with the specified options.
|
|
70
|
-
* @param {Object} options - The configuration for the node.
|
|
71
|
-
* @returns {Node} The created node instance.
|
|
72
|
-
*/
|
|
73
47
|
createNode(options) {
|
|
74
48
|
const node = new Node(this, options, this.options);
|
|
75
49
|
this.nodeMap.set(options.name || options.host, node);
|
|
@@ -78,215 +52,159 @@ class Aqua extends EventEmitter {
|
|
|
78
52
|
return node;
|
|
79
53
|
}
|
|
80
54
|
|
|
81
|
-
/**
|
|
82
|
-
* Destroys a node identified by the given identifier.
|
|
83
|
-
* @param {string} identifier - The identifier of the node to destroy.
|
|
84
|
-
*/
|
|
85
55
|
destroyNode(identifier) {
|
|
86
56
|
const node = this.nodeMap.get(identifier);
|
|
87
|
-
if (
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
57
|
+
if (node) {
|
|
58
|
+
node.disconnect();
|
|
59
|
+
this.nodeMap.delete(identifier);
|
|
60
|
+
this.emit("nodeDestroy", node);
|
|
61
|
+
}
|
|
91
62
|
}
|
|
92
63
|
|
|
93
|
-
/**
|
|
94
|
-
* Updates the voice state based on the received packet.
|
|
95
|
-
* @param {Object} packet - The packet containing voice state information.
|
|
96
|
-
*/
|
|
97
64
|
updateVoiceState(packet) {
|
|
98
65
|
const player = this.players.get(packet.d.guild_id);
|
|
99
|
-
if (
|
|
100
|
-
|
|
101
|
-
|
|
66
|
+
if (player) {
|
|
67
|
+
if (packet.t === "VOICE_SERVER_UPDATE") {
|
|
68
|
+
player.connection.setServerUpdate(packet.d);
|
|
69
|
+
} else if (packet.t === "VOICE_STATE_UPDATE" && packet.d.user_id === this.clientId) {
|
|
70
|
+
player.connection.setStateUpdate(packet.d);
|
|
71
|
+
if (packet.d.status === "disconnected") {
|
|
72
|
+
this.cleanupPlayer(player); // Cleanup when disconnected
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
102
76
|
}
|
|
103
77
|
|
|
104
|
-
/**
|
|
105
|
-
* Fetches nodes by the specified region.
|
|
106
|
-
* @param {string} region - The region to filter nodes by.
|
|
107
|
-
* @returns {Array<Node>} Array of nodes in the specified region.
|
|
108
|
-
*/
|
|
109
78
|
fetchRegion(region) {
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
.
|
|
113
|
-
|
|
114
|
-
const bLoad = b.stats.cpu ? (b.stats.cpu.systemLoad / b.stats.cpu.cores) * 100 : 0;
|
|
115
|
-
return aLoad - bLoad;
|
|
116
|
-
});
|
|
117
|
-
return nodesByRegion;
|
|
79
|
+
const lowerRegion = region?.toLowerCase();
|
|
80
|
+
return [...this.nodeMap.values()]
|
|
81
|
+
.filter(node => node.connected && node.regions?.includes(lowerRegion))
|
|
82
|
+
.sort((a, b) => this.calculateLoad(a) - this.calculateLoad(b));
|
|
118
83
|
}
|
|
119
84
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
* @param {string} options.guildId - The ID of the guild.
|
|
124
|
-
* @param {string} [options.region] - The region to connect to.
|
|
125
|
-
* @returns {Player} The created player instance.
|
|
126
|
-
*/
|
|
127
|
-
createConnection(options) {
|
|
128
|
-
if (!this.initiated) throw new Error("BRO! Get aqua on before !!!");
|
|
129
|
-
if (!this.leastUsedNodes.length) throw new Error("No nodes are available");
|
|
85
|
+
calculateLoad(node) {
|
|
86
|
+
return node.stats.cpu ? (node.stats.cpu.systemLoad / node.stats.cpu.cores) * 100 : 0;
|
|
87
|
+
}
|
|
130
88
|
|
|
89
|
+
createConnection(options) {
|
|
90
|
+
this.ensureInitialized();
|
|
131
91
|
const player = this.players.get(options.guildId);
|
|
132
92
|
if (player && player.voiceChannel) return player;
|
|
133
|
-
|
|
134
|
-
const node = (options.region ? this.fetchRegion(options.region) : this.leastUsedNodes)[0];
|
|
93
|
+
const node = options.region ? this.fetchRegion(options.region)[0] : this.leastUsedNodes[0];
|
|
135
94
|
if (!node) throw new Error("No nodes are available");
|
|
136
|
-
|
|
137
95
|
return this.createPlayer(node, options);
|
|
138
96
|
}
|
|
139
97
|
|
|
140
|
-
/**
|
|
141
|
-
* Creates a player using the specified node.
|
|
142
|
-
* @param {Node} node - The node to create the player with.
|
|
143
|
-
* @param {Object} options - The player options.
|
|
144
|
-
* @returns {Player} The created player instance.
|
|
145
|
-
*/
|
|
146
98
|
createPlayer(node, options) {
|
|
147
99
|
const player = new Player(this, node, options);
|
|
148
100
|
this.players.set(options.guildId, player);
|
|
149
101
|
player.connect(options);
|
|
102
|
+
player.on("destroy", () => this.cleanupPlayer(player)); // Listen for player destruction
|
|
150
103
|
this.emit("playerCreate", player);
|
|
151
104
|
return player;
|
|
152
105
|
}
|
|
153
106
|
|
|
154
|
-
/**
|
|
155
|
-
* Destroys the player associated with the given guild ID.
|
|
156
|
-
* @param {string} guildId - The ID of the guild.
|
|
157
|
-
*/
|
|
158
107
|
destroyPlayer(guildId) {
|
|
159
108
|
const player = this.players.get(guildId);
|
|
160
|
-
player.clearData();
|
|
161
|
-
if (!player) return;
|
|
162
|
-
player.destroy();
|
|
163
|
-
this.players.delete(guildId);
|
|
164
|
-
this.emit("playerDestroy", player);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Removes the connection for the specified guild ID.
|
|
169
|
-
* @param {string} guildId - The ID of the guild.
|
|
170
|
-
*/
|
|
171
|
-
removeConnection(guildId) {
|
|
172
|
-
const player = this.players.get(guildId);
|
|
173
|
-
player.clearData();
|
|
174
109
|
if (player) {
|
|
110
|
+
player.clearData();
|
|
175
111
|
player.destroy();
|
|
176
112
|
this.players.delete(guildId);
|
|
113
|
+
this.emit("playerDestroy", player);
|
|
177
114
|
}
|
|
178
115
|
}
|
|
179
116
|
|
|
180
117
|
/**
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
* @param {
|
|
184
|
-
* @param {string}
|
|
185
|
-
* @param {
|
|
186
|
-
* @param {
|
|
187
|
-
* @
|
|
118
|
+
* Resolve a track into an array of {@link Track} objects.
|
|
119
|
+
*
|
|
120
|
+
* @param {Object} options - The options for the resolve operation.
|
|
121
|
+
* @param {string} options.query - The query to search for.
|
|
122
|
+
* @param {string} [options.source] - The source to use for the search. Defaults to the bot's default search platform.
|
|
123
|
+
* @param {GuildMember} options.requester - The member who requested the track.
|
|
124
|
+
* @param {Node[]} [options.nodes] - The nodes to prioritize for the search. Defaults to all nodes.
|
|
125
|
+
* @returns {Promise<Track[]>} The resolved tracks.
|
|
188
126
|
*/
|
|
189
127
|
async resolve({ query, source, requester, nodes }) {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
128
|
+
this.ensureInitialized();
|
|
129
|
+
const requestNode = this.getRequestNode(nodes);
|
|
130
|
+
const formattedQuery = this.formatQuery(query, source || this.defaultSearchPlatform);
|
|
131
|
+
try {
|
|
132
|
+
let response = await requestNode.rest.makeRequest("GET", `/v4/loadtracks?identifier=${encodeURIComponent(formattedQuery)}`);
|
|
133
|
+
if (["empty", "NO_MATCHES"].includes(response.loadType)) {
|
|
134
|
+
response = await this.handleNoMatches(requestNode.rest, query);
|
|
135
|
+
}
|
|
136
|
+
return this.constructorResponse(response, requester, requestNode);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
console.error("Error resolving track:", error);
|
|
139
|
+
throw new Error("Failed to resolve track");
|
|
193
140
|
}
|
|
141
|
+
}
|
|
194
142
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const formattedQuery = /^https?:\/\//.test(query) ? query : `${searchSources}:${query}`;
|
|
200
|
-
let response = await requestNode.rest.makeRequest("GET", `/v4/loadtracks?identifier=${encodeURIComponent(formattedQuery)}`);
|
|
143
|
+
ensureInitialized() {
|
|
144
|
+
if (!this.initiated) throw new Error("Aqua must be initialized before this operation");
|
|
145
|
+
}
|
|
201
146
|
|
|
202
|
-
|
|
203
|
-
if (
|
|
204
|
-
|
|
147
|
+
getRequestNode(nodes) {
|
|
148
|
+
if (nodes && (typeof nodes !== "string" && !(nodes instanceof Node))) {
|
|
149
|
+
throw new Error(`'nodes' must be a string or Node instance, but received: ${typeof nodes}`);
|
|
205
150
|
}
|
|
151
|
+
return (typeof nodes === 'string' ? this.nodeMap.get(nodes) : nodes) || this.leastUsedNodes[0];
|
|
152
|
+
}
|
|
206
153
|
|
|
207
|
-
|
|
154
|
+
formatQuery(query, source) {
|
|
155
|
+
return /^https?:\/\//.test(query) ? query : `${source}:${query}`;
|
|
208
156
|
}
|
|
209
157
|
|
|
210
|
-
/**
|
|
211
|
-
* Handles cases where no matches were found for a query.
|
|
212
|
-
* @param {Object} rest - The REST client for making requests.
|
|
213
|
-
* @param {string} query - The original query string.
|
|
214
|
-
* @returns {Promise<Object>} The response object from the request.
|
|
215
|
-
*/
|
|
216
158
|
async handleNoMatches(rest, query) {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
159
|
+
try {
|
|
160
|
+
const spotifyResponse = await rest.makeRequest("GET", `/v4/loadtracks?identifier=https://open.spotify.com/track/${query}`);
|
|
161
|
+
if (["empty", "NO_MATCHES"].includes(spotifyResponse.loadType)) {
|
|
162
|
+
return await rest.makeRequest("GET", `/v4/loadtracks?identifier=https://www.youtube.com/watch?v=${query}`);
|
|
163
|
+
}
|
|
164
|
+
return spotifyResponse;
|
|
165
|
+
} catch (error) {
|
|
166
|
+
console.error("Error handling no matches:", error);
|
|
167
|
+
throw new Error("Failed to handle no matches");
|
|
220
168
|
}
|
|
221
|
-
return response;
|
|
222
169
|
}
|
|
223
170
|
|
|
224
|
-
/**
|
|
225
|
-
* Loads tracks from the resolved response.
|
|
226
|
-
* @param {Object} response - The response from the track resolution.
|
|
227
|
-
* @param {Object} requester - The requester of the tracks.
|
|
228
|
-
* @param {Node} requestNode - The node that handled the request.
|
|
229
|
-
* @returns {Object} The constructed response.
|
|
230
|
-
*/
|
|
231
171
|
constructorResponse(response, requester, requestNode) {
|
|
172
|
+
const baseResponse = {
|
|
173
|
+
loadType: response.loadType,
|
|
174
|
+
exception: null,
|
|
175
|
+
playlistInfo: null,
|
|
176
|
+
pluginInfo: response.pluginInfo || {},
|
|
177
|
+
tracks: [],
|
|
178
|
+
};
|
|
232
179
|
switch (response.loadType) {
|
|
233
180
|
case "track":
|
|
234
181
|
if (response.data) {
|
|
235
|
-
|
|
236
|
-
loadType: response.loadType,
|
|
237
|
-
exception: null,
|
|
238
|
-
playlistInfo: null,
|
|
239
|
-
pluginInfo: response.pluginInfo || {},
|
|
240
|
-
tracks: [new Track(response.data, requester, requestNode)],
|
|
241
|
-
};
|
|
182
|
+
baseResponse.tracks.push(new Track(response.data, requester, requestNode));
|
|
242
183
|
}
|
|
243
184
|
break;
|
|
244
185
|
case "playlist":
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
playlistInfo: {
|
|
249
|
-
name: response.data?.info?.name || response.data?.info?.title,
|
|
250
|
-
...response.data?.info,
|
|
251
|
-
},
|
|
252
|
-
pluginInfo: response.pluginInfo || {},
|
|
253
|
-
tracks: response.data?.tracks?.map(track => new Track(track, requester, requestNode)) || [],
|
|
186
|
+
baseResponse.playlistInfo = {
|
|
187
|
+
name: response.data?.info?.name || response.data?.info?.title,
|
|
188
|
+
...response.data?.info,
|
|
254
189
|
};
|
|
190
|
+
baseResponse.tracks = response.data?.tracks?.map(track => new Track(track, requester, requestNode)) || [];
|
|
191
|
+
break;
|
|
255
192
|
case "search":
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
exception: null,
|
|
259
|
-
playlistInfo: null,
|
|
260
|
-
pluginInfo: response.pluginInfo || {},
|
|
261
|
-
tracks: response.data?.map(track => new Track(track, requester, requestNode)),
|
|
262
|
-
};
|
|
193
|
+
baseResponse.tracks = response.data?.map(track => new Track(track, requester, requestNode)) || [];
|
|
194
|
+
break;
|
|
263
195
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
playlistInfo: null,
|
|
269
|
-
pluginInfo: response.pluginInfo || {},
|
|
270
|
-
tracks: [],
|
|
271
|
-
};
|
|
196
|
+
if (response.loadType === "error" || response.loadType === "LOAD_FAILED") {
|
|
197
|
+
baseResponse.exception = response.loadType.data || response.loadType.exception;
|
|
198
|
+
}
|
|
199
|
+
return baseResponse;
|
|
272
200
|
}
|
|
273
201
|
|
|
274
|
-
|
|
275
|
-
/**
|
|
276
|
-
* Gets the player associated with the specified guild ID.
|
|
277
|
-
* @param {string} guildId - The ID of the guild.
|
|
278
|
-
* @returns {Player} The player instance.
|
|
279
|
-
* @throws {Error} If the player is not found.
|
|
280
|
-
*/
|
|
281
202
|
get(guildId) {
|
|
282
203
|
const player = this.players.get(guildId);
|
|
283
204
|
if (!player) throw new Error(`Player not found for guild ID: ${guildId}`);
|
|
284
205
|
return player;
|
|
285
206
|
}
|
|
286
207
|
|
|
287
|
-
/**
|
|
288
|
-
* Cleans up idle players and nodes to free up resources.
|
|
289
|
-
*/
|
|
290
208
|
cleanupIdle() {
|
|
291
209
|
for (const [guildId, player] of this.players) {
|
|
292
210
|
if (!player.playing && !player.paused && player.queue.isEmpty()) {
|
|
@@ -296,6 +214,15 @@ class Aqua extends EventEmitter {
|
|
|
296
214
|
}
|
|
297
215
|
}
|
|
298
216
|
}
|
|
217
|
+
|
|
218
|
+
cleanupPlayer(player) {
|
|
219
|
+
if (player) {
|
|
220
|
+
player.clearData();
|
|
221
|
+
player.destroy();
|
|
222
|
+
this.players.delete(player.guildId);
|
|
223
|
+
this.emit("playerDestroy", player);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
299
226
|
}
|
|
300
227
|
|
|
301
|
-
module.exports = { Aqua };
|
|
228
|
+
module.exports = { Aqua };
|
package/build/structures/Node.js
CHANGED
|
@@ -26,7 +26,7 @@ class Node {
|
|
|
26
26
|
this.resumeKey = options.resumeKey || null;
|
|
27
27
|
this.resumeTimeout = options.resumeTimeout || 60;
|
|
28
28
|
this.autoResume = options.autoResume || false;
|
|
29
|
-
this.reconnectTimeout = options.reconnectTimeout ||
|
|
29
|
+
this.reconnectTimeout = options.reconnectTimeout || 2000;
|
|
30
30
|
this.reconnectTries = options.reconnectTries || 3;
|
|
31
31
|
this.reconnectAttempted = 0;
|
|
32
32
|
this.lastStatsRequest = 0; // Track the last time stats were requested
|
|
@@ -42,17 +42,21 @@ class Node {
|
|
|
42
42
|
used: 0,
|
|
43
43
|
allocated: 0,
|
|
44
44
|
reservable: 0,
|
|
45
|
+
freePercentage: 0,
|
|
46
|
+
usedPercentage: 0,
|
|
45
47
|
},
|
|
46
48
|
cpu: {
|
|
47
49
|
cores: 0,
|
|
48
50
|
systemLoad: 0,
|
|
49
51
|
lavalinkLoad: 0,
|
|
52
|
+
lavalinkLoadPercentage: 0,
|
|
50
53
|
},
|
|
51
54
|
frameStats: {
|
|
52
55
|
sent: 0,
|
|
53
56
|
nulled: 0,
|
|
54
57
|
deficit: 0,
|
|
55
58
|
},
|
|
59
|
+
ping: 0,
|
|
56
60
|
};
|
|
57
61
|
}
|
|
58
62
|
|
|
@@ -61,7 +65,9 @@ class Node {
|
|
|
61
65
|
}
|
|
62
66
|
|
|
63
67
|
async connect() {
|
|
64
|
-
if (this.ws)
|
|
68
|
+
if (this.ws) {
|
|
69
|
+
this.ws.close();
|
|
70
|
+
}
|
|
65
71
|
this.aqua.emit('debug', this.name, `Attempting to connect...`);
|
|
66
72
|
this.ws = new WebSocket(this.wsUrl, { headers: this.constructHeaders() });
|
|
67
73
|
this.setupWebSocketListeners();
|
|
@@ -93,15 +99,13 @@ class Node {
|
|
|
93
99
|
this.aqua.emit('debug', `Failed to fetch info: ${err.message}`);
|
|
94
100
|
this.info = null;
|
|
95
101
|
}
|
|
96
|
-
|
|
97
102
|
if (!this.info && !this.aqua.bypassChecks.nodeFetchInfo) {
|
|
98
103
|
throw new Error(`Failed to fetch node info.`);
|
|
99
104
|
}
|
|
100
|
-
|
|
101
105
|
if (this.autoResume) {
|
|
102
106
|
this.resumePlayers();
|
|
103
107
|
}
|
|
104
|
-
this.
|
|
108
|
+
this.lastStatsRequest = Date.now();
|
|
105
109
|
}
|
|
106
110
|
|
|
107
111
|
async getStats() {
|
|
@@ -109,7 +113,6 @@ class Node {
|
|
|
109
113
|
if (now - this.lastStatsRequest < 5000) {
|
|
110
114
|
return this.stats; // Return cached stats if requested too soon
|
|
111
115
|
}
|
|
112
|
-
|
|
113
116
|
try {
|
|
114
117
|
const response = await this.rest.makeRequest("GET", `/v4/stats`);
|
|
115
118
|
const stats = await response.json();
|
|
@@ -137,7 +140,13 @@ class Node {
|
|
|
137
140
|
onMessage(msg) {
|
|
138
141
|
if (Array.isArray(msg)) msg = Buffer.concat(msg);
|
|
139
142
|
if (msg instanceof ArrayBuffer) msg = Buffer.from(msg);
|
|
140
|
-
|
|
143
|
+
let payload;
|
|
144
|
+
try {
|
|
145
|
+
payload = JSON.parse(msg.toString());
|
|
146
|
+
} catch (err) {
|
|
147
|
+
this.aqua.emit('debug', `Failed to parse message: ${err.message}`);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
141
150
|
if (!payload.op) return;
|
|
142
151
|
this.aqua.emit("raw", "Node", payload);
|
|
143
152
|
this.aqua.emit("debug", this.name, `Received update: ${JSON.stringify(payload)}`);
|
|
@@ -147,8 +156,7 @@ class Node {
|
|
|
147
156
|
handlePayload(payload) {
|
|
148
157
|
switch (payload.op) {
|
|
149
158
|
case "stats":
|
|
150
|
-
this.
|
|
151
|
-
this.lastStats = Date.now();
|
|
159
|
+
this.updateStats(payload);
|
|
152
160
|
break;
|
|
153
161
|
case "ready":
|
|
154
162
|
this.initializeSessionId(payload.sessionId);
|
|
@@ -161,6 +169,35 @@ class Node {
|
|
|
161
169
|
}
|
|
162
170
|
}
|
|
163
171
|
|
|
172
|
+
updateStats(payload) {
|
|
173
|
+
this.stats = {
|
|
174
|
+
...this.stats,
|
|
175
|
+
players: payload.players || 0,
|
|
176
|
+
playingPlayers: payload.playingPlayers || 0,
|
|
177
|
+
uptime: payload.uptime || 0,
|
|
178
|
+
memory: {
|
|
179
|
+
free: payload.memory?.free || 0,
|
|
180
|
+
used: payload.memory?.used || 0,
|
|
181
|
+
allocated: payload.memory?.allocated || 0,
|
|
182
|
+
reservable: payload.memory?.reservable || 0,
|
|
183
|
+
freePercentage: payload.memory ? (payload.memory.free / payload.memory.allocated) * 100 : 0,
|
|
184
|
+
usedPercentage: payload.memory ? (payload.memory.used / payload.memory.allocated) * 100 : 0,
|
|
185
|
+
},
|
|
186
|
+
cpu: {
|
|
187
|
+
cores: payload.cpu?.cores || 0,
|
|
188
|
+
systemLoad: payload.cpu?.systemLoad || 0,
|
|
189
|
+
lavalinkLoad: payload.cpu?.lavalinkLoad || 0,
|
|
190
|
+
lavalinkLoadPercentage: payload.cpu ? (payload.cpu.lavalinkLoad / payload.cpu.cores) * 100 : 0,
|
|
191
|
+
},
|
|
192
|
+
frameStats: {
|
|
193
|
+
sent: payload.frameStats?.sent || 0,
|
|
194
|
+
nulled: payload.frameStats?.nulled || 0,
|
|
195
|
+
deficit: payload.frameStats?.deficit || 0,
|
|
196
|
+
},
|
|
197
|
+
ping: payload.ping || 0,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
164
201
|
initializeSessionId(sessionId) {
|
|
165
202
|
if (this.sessionId !== sessionId) {
|
|
166
203
|
this.rest.setSessionId(sessionId);
|
|
@@ -179,7 +216,6 @@ class Node {
|
|
|
179
216
|
this.aqua.emit("nodeError", this, new Error(`Unable to connect after ${this.reconnectTries} attempts.`));
|
|
180
217
|
return this.destroy();
|
|
181
218
|
}
|
|
182
|
-
|
|
183
219
|
setTimeout(() => {
|
|
184
220
|
this.aqua.emit("nodeReconnect", this);
|
|
185
221
|
this.connect();
|
|
@@ -188,8 +224,6 @@ class Node {
|
|
|
188
224
|
|
|
189
225
|
destroy(clean = false) {
|
|
190
226
|
if (clean) {
|
|
191
|
-
this.ws?.removeAllListeners();
|
|
192
|
-
this.ws = null;
|
|
193
227
|
this.aqua.emit("nodeDestroy", this);
|
|
194
228
|
this.aqua.nodes.delete(this.name);
|
|
195
229
|
return;
|
|
@@ -198,9 +232,6 @@ class Node {
|
|
|
198
232
|
this.aqua.players.forEach((player) => {
|
|
199
233
|
if (player.node === this) player.destroy();
|
|
200
234
|
});
|
|
201
|
-
this.ws?.close(1000, "destroy");
|
|
202
|
-
this.ws?.removeAllListeners();
|
|
203
|
-
this.ws = null;
|
|
204
235
|
this.aqua.emit("nodeDestroy", this);
|
|
205
236
|
this.aqua.nodeMap.delete(this.name);
|
|
206
237
|
this.connected = false;
|
|
@@ -208,8 +239,7 @@ class Node {
|
|
|
208
239
|
|
|
209
240
|
disconnect() {
|
|
210
241
|
if (!this.connected) return;
|
|
211
|
-
this.aqua.players.forEach((player) => { if (player.node === this) { player.move() } });
|
|
212
|
-
this.ws.close(1000, "disconnect");
|
|
242
|
+
this.aqua.players.forEach((player) => { if (player.node === this) { player.move(); } });
|
|
213
243
|
this.aqua.emit("nodeDisconnect", this);
|
|
214
244
|
this.connected = false;
|
|
215
245
|
}
|
|
@@ -229,4 +259,4 @@ class Node {
|
|
|
229
259
|
}
|
|
230
260
|
}
|
|
231
261
|
|
|
232
|
-
module.exports = { Node };
|
|
262
|
+
module.exports = { Node };
|