aqualink 2.6.1 → 2.6.2
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 +9 -9
- package/build/handlers/autoplay.js +77 -105
- package/build/structures/Aqua.js +140 -64
- package/build/structures/Node.js +41 -10
- package/build/structures/Player.js +22 -16
- package/build/structures/Rest.js +1 -1
- package/build/structures/Track.js +2 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -195,18 +195,18 @@ client.login("Yourtokenhere");
|
|
|
195
195
|
<div align="center">
|
|
196
196
|
<table>
|
|
197
197
|
<tr>
|
|
198
|
-
<td align="center" width="
|
|
199
|
-
<img width="120" height="120" src="https://img.icons8.com/fluent/240/000000/musical-notes.png"/>
|
|
200
|
-
<br/>
|
|
201
|
-
<img src="https://img.shields.io/badge/Rive-0061ff?style=for-the-badge&logo=discord&logoColor=white" /><br />
|
|
202
|
-
<a href="https://discord.com/oauth2/authorize?client_id=1350601402325405806">Add to Discord</a>
|
|
203
|
-
</td>
|
|
204
|
-
<td align="center" width="50%">
|
|
198
|
+
<td align="center" width="33%">
|
|
205
199
|
<img width="120" height="120" src="https://img.icons8.com/fluent/240/000000/water-element.png"/>
|
|
206
200
|
<br/>
|
|
207
201
|
<img src="https://img.shields.io/badge/Kenium-00bfff?style=for-the-badge&logo=discord&logoColor=white" /><br />
|
|
208
202
|
<a href="https://discord.com/oauth2/authorize?client_id=1202232935311495209">Add to Discord</a>
|
|
209
203
|
</td>
|
|
204
|
+
<td align="center" width="33%">
|
|
205
|
+
<img width="120" height="120" src="https://cdn.discordapp.com/attachments/1347414750463660032/1365654298989690930/soya1.jpg?ex=680e182d&is=680cc6ad&hm=3055de34e2af31a3a430f52b147a00215f8b88c8dcc9363cab5359c50ce8d75f&"/>
|
|
206
|
+
<br/>
|
|
207
|
+
<img src="https://img.shields.io/badge/SoyaMusic-22c55e?style=for-the-badge&logo=discord&logoColor=white" /><br />
|
|
208
|
+
<a href="https://discord.com/oauth2/authorize?client_id=997906613082013868&permissions=281357446481&integration_type=0&scope=bot+applications.commands">Add to Discord</a>
|
|
209
|
+
</td>
|
|
210
210
|
</tr>
|
|
211
211
|
</table>
|
|
212
212
|
</div>
|
|
@@ -237,8 +237,8 @@ For detailed usage, API references, and examples, check out our official documen
|
|
|
237
237
|
|
|
238
238
|
| Bot | Invite Link | Features |
|
|
239
239
|
|-----|-------------|----------|
|
|
240
|
-
| Rive | [Add to Discord](https://discord.com/oauth2/authorize?client_id=1350601402325405806) | Music playback, Queue management |
|
|
241
240
|
| Kenium | [Add to Discord](https://discord.com/oauth2/authorize?client_id=1202232935311495209) | Audio streaming, Discord integration |
|
|
241
|
+
| Soya Music | [Add to Discord](https://discord.com/oauth2/authorize?client_id=997906613082013868&permissions=281357446481&integration_type=0&scope=bot+applications.commands) | Audio streaming, Discord integration |
|
|
242
242
|
|
|
243
243
|
## 🛠️ Advanced Features
|
|
244
244
|
|
|
@@ -347,4 +347,4 @@ Join our thriving community of developers and bot creators!
|
|
|
347
347
|
|
|
348
348
|
<sub>Built with 💙 by the Aqualink Team</sub>
|
|
349
349
|
|
|
350
|
-
</div>
|
|
350
|
+
</div>
|
|
@@ -1,142 +1,114 @@
|
|
|
1
1
|
const https = require('https');
|
|
2
2
|
const crypto = require('crypto');
|
|
3
3
|
|
|
4
|
-
const agent = new https.Agent({
|
|
4
|
+
const agent = new https.Agent({
|
|
5
|
+
keepAlive: true,
|
|
6
|
+
maxSockets: 5,
|
|
7
|
+
maxFreeSockets: 2,
|
|
8
|
+
timeout: 8000,
|
|
9
|
+
freeSocketTimeout: 4000
|
|
10
|
+
});
|
|
5
11
|
|
|
6
12
|
const TOTP_SECRET = Buffer.from("5507145853487499592248630329347", 'utf8');
|
|
7
13
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
15
|
-
if (redirectCount >= maxRedirects) {
|
|
16
|
-
return reject(new Error('Too many redirects'));
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
res.resume();
|
|
20
|
-
try {
|
|
21
|
-
const resolved = await quickFetch(
|
|
22
|
-
new URL(res.headers.location, url).toString(),
|
|
23
|
-
options,
|
|
24
|
-
redirectCount + 1
|
|
25
|
-
);
|
|
26
|
-
resolve(resolved);
|
|
27
|
-
} catch (err) {
|
|
28
|
-
reject(err);
|
|
29
|
-
}
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if (res.statusCode !== 200) {
|
|
34
|
-
res.resume();
|
|
35
|
-
return reject(new Error(`Request failed. Status code: ${res.statusCode}`));
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const chunks = [];
|
|
39
|
-
let length = 0;
|
|
40
|
-
|
|
41
|
-
res.on('data', (chunk) => {
|
|
42
|
-
chunks.push(chunk);
|
|
43
|
-
length += chunk.length;
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
res.on('end', () => {
|
|
47
|
-
resolve(Buffer.concat(chunks, length).toString());
|
|
48
|
-
});
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
req.on('error', reject);
|
|
52
|
-
req.setTimeout(10000, () => {
|
|
53
|
-
req.destroy(new Error('Request timeout'));
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
} catch (err) {
|
|
57
|
-
throw err;
|
|
14
|
+
const SOUNDCLOUD_REGEX = /<a\s+itemprop="url"\s+href="(\/[^"]+)"/g;
|
|
15
|
+
|
|
16
|
+
const shuffleArray = (arr) => {
|
|
17
|
+
for (let i = arr.length - 1; i > 0; i--) {
|
|
18
|
+
const j = Math.random() * (i + 1) | 0;
|
|
19
|
+
[arr[i], arr[j]] = [arr[j], arr[i]];
|
|
58
20
|
}
|
|
59
|
-
|
|
21
|
+
return arr;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const fastFetch = (url, options = {}) => {
|
|
25
|
+
return new Promise((resolve, reject) => {
|
|
26
|
+
const req = https.get(url, { ...options, agent }, (res) => {
|
|
27
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
28
|
+
res.resume();
|
|
29
|
+
return fastFetch(new URL(res.headers.location, url).href, options)
|
|
30
|
+
.then(resolve, reject);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (res.statusCode !== 200) {
|
|
34
|
+
res.resume();
|
|
35
|
+
return reject(new Error(`HTTP ${res.statusCode}`));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const chunks = [];
|
|
39
|
+
res.on('data', chunk => chunks.push(chunk));
|
|
40
|
+
res.on('end', () => resolve(Buffer.concat(chunks).toString()));
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
req.on('error', reject);
|
|
44
|
+
req.setTimeout(8000, () => req.destroy(new Error('Timeout')));
|
|
45
|
+
});
|
|
46
|
+
};
|
|
60
47
|
|
|
61
|
-
async
|
|
48
|
+
const soundAutoPlay = async (baseUrl) => {
|
|
62
49
|
try {
|
|
63
|
-
const html = await
|
|
64
|
-
const links = new Set();
|
|
65
|
-
const regex = /<a\s+itemprop="url"\s+href="(\/[^"]+)"/g;
|
|
50
|
+
const html = await fastFetch(`${baseUrl}/recommended`);
|
|
66
51
|
|
|
52
|
+
const links = [];
|
|
67
53
|
let match;
|
|
68
|
-
while ((match =
|
|
69
|
-
links.
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (!links.size) {
|
|
73
|
-
throw new Error("No recommended tracks found on SoundCloud.");
|
|
54
|
+
while ((match = SOUNDCLOUD_REGEX.exec(html)) && links.length < 50) {
|
|
55
|
+
links.push(`https://soundcloud.com${match[1]}`);
|
|
74
56
|
}
|
|
75
57
|
|
|
76
|
-
|
|
77
|
-
for (let i = urls.length - 1; i > 0; i--) {
|
|
78
|
-
const j = Math.random() * (i + 1) | 0;
|
|
79
|
-
[urls[i], urls[j]] = [urls[j], urls[i]];
|
|
80
|
-
}
|
|
58
|
+
if (!links.length) throw new Error("No tracks found");
|
|
81
59
|
|
|
82
|
-
return
|
|
60
|
+
return shuffleArray(links);
|
|
83
61
|
} catch (err) {
|
|
84
|
-
console.error("
|
|
62
|
+
console.error("SoundCloud error:", err.message);
|
|
85
63
|
return [];
|
|
86
64
|
}
|
|
87
|
-
}
|
|
65
|
+
};
|
|
88
66
|
|
|
89
|
-
|
|
90
|
-
const timeStep =
|
|
91
|
-
const counter = Buffer.
|
|
92
|
-
counter.
|
|
67
|
+
const generateToken = () => {
|
|
68
|
+
const timeStep = (Date.now() / 30000) | 0;
|
|
69
|
+
const counter = Buffer.allocUnsafe(8);
|
|
70
|
+
counter.writeBigUInt64BE(BigInt(timeStep), 0);
|
|
93
71
|
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
const hash = hmac.digest();
|
|
97
|
-
const offset = hash[hash.length - 1] & 0x0f;
|
|
72
|
+
const hash = crypto.createHmac('sha1', TOTP_SECRET).update(counter).digest();
|
|
73
|
+
const offset = hash[19] & 0x0f;
|
|
98
74
|
|
|
99
75
|
const binCode = (
|
|
100
|
-
(hash[offset] << 24
|
|
101
|
-
|
|
102
|
-
|
|
76
|
+
(hash[offset] & 0x7f) << 24 |
|
|
77
|
+
hash[offset + 1] << 16 |
|
|
78
|
+
hash[offset + 2] << 8 |
|
|
103
79
|
hash[offset + 3]
|
|
104
|
-
)
|
|
80
|
+
);
|
|
105
81
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
82
|
+
return [
|
|
83
|
+
(binCode % 1000000).toString().padStart(6, '0'),
|
|
84
|
+
timeStep * 30000
|
|
85
|
+
];
|
|
86
|
+
};
|
|
109
87
|
|
|
110
|
-
async
|
|
88
|
+
const spotifyAutoPlay = async (seedTrackId) => {
|
|
111
89
|
const [totp, ts] = generateToken();
|
|
112
|
-
|
|
113
|
-
reason: "transport",
|
|
114
|
-
productType: "embed",
|
|
115
|
-
totp,
|
|
116
|
-
totpVer: "5",
|
|
117
|
-
ts: ts.toString()
|
|
118
|
-
});
|
|
119
|
-
|
|
90
|
+
|
|
120
91
|
try {
|
|
121
|
-
const
|
|
122
|
-
const
|
|
92
|
+
const tokenUrl = `https://open.spotify.com/api/token?reason=init&productType=embed&totp=${totp}&totpVer=5&ts=${ts}`;
|
|
93
|
+
const tokenResponse = await fastFetch(tokenUrl);
|
|
94
|
+
const { accessToken } = JSON.parse(tokenResponse);
|
|
123
95
|
|
|
124
|
-
if (!accessToken) throw new Error("
|
|
96
|
+
if (!accessToken) throw new Error("No access token");
|
|
125
97
|
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
);
|
|
98
|
+
const recUrl = `https://api.spotify.com/v1/recommendations?limit=10&seed_tracks=${seedTrackId}`;
|
|
99
|
+
const recResponse = await fastFetch(recUrl, {
|
|
100
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
101
|
+
});
|
|
130
102
|
|
|
131
|
-
const { tracks } = JSON.parse(
|
|
132
|
-
if (!tracks?.length) throw new Error("No tracks
|
|
103
|
+
const { tracks } = JSON.parse(recResponse);
|
|
104
|
+
if (!tracks?.length) throw new Error("No tracks");
|
|
133
105
|
|
|
134
106
|
return tracks[Math.random() * tracks.length | 0].id;
|
|
135
107
|
} catch (err) {
|
|
136
|
-
console.error("Spotify
|
|
108
|
+
console.error("Spotify error:", err.message);
|
|
137
109
|
throw err;
|
|
138
110
|
}
|
|
139
|
-
}
|
|
111
|
+
};
|
|
140
112
|
|
|
141
113
|
module.exports = {
|
|
142
114
|
scAutoPlay: soundAutoPlay,
|
package/build/structures/Aqua.js
CHANGED
|
@@ -4,16 +4,17 @@ const Player = require("./Player");
|
|
|
4
4
|
const Track = require("./Track");
|
|
5
5
|
const { version: pkgVersion } = require("../../package.json");
|
|
6
6
|
const { EventEmitter } = require('tseep');
|
|
7
|
+
const fs = require('fs-extra');
|
|
7
8
|
|
|
8
9
|
const URL_REGEX = /^https?:\/\//;
|
|
9
10
|
const DEFAULT_OPTIONS = Object.freeze({
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
shouldDeleteMessage: false,
|
|
12
|
+
defaultSearchPlatform: 'ytsearch',
|
|
13
|
+
leaveOnEnd: true,
|
|
14
|
+
restVersion: 'v4',
|
|
15
|
+
plugins: [],
|
|
16
|
+
autoResume: false,
|
|
17
|
+
infiniteReconnects: false
|
|
17
18
|
});
|
|
18
19
|
const LEAST_USED_CACHE_TTL = 50;
|
|
19
20
|
|
|
@@ -32,9 +33,9 @@ class Aqua extends EventEmitter {
|
|
|
32
33
|
this.clientId = null;
|
|
33
34
|
this.initiated = false;
|
|
34
35
|
this.version = pkgVersion;
|
|
35
|
-
|
|
36
|
+
|
|
36
37
|
this.options = Object.assign({}, DEFAULT_OPTIONS, options);
|
|
37
|
-
|
|
38
|
+
|
|
38
39
|
const {
|
|
39
40
|
shouldDeleteMessage,
|
|
40
41
|
defaultSearchPlatform,
|
|
@@ -45,7 +46,7 @@ class Aqua extends EventEmitter {
|
|
|
45
46
|
infiniteReconnects,
|
|
46
47
|
send
|
|
47
48
|
} = this.options;
|
|
48
|
-
|
|
49
|
+
|
|
49
50
|
this.shouldDeleteMessage = shouldDeleteMessage;
|
|
50
51
|
this.defaultSearchPlatform = defaultSearchPlatform;
|
|
51
52
|
this.leaveOnEnd = leaveOnEnd;
|
|
@@ -53,9 +54,9 @@ class Aqua extends EventEmitter {
|
|
|
53
54
|
this.plugins = plugins;
|
|
54
55
|
this.autoResume = autoResume;
|
|
55
56
|
this.infiniteReconnects = infiniteReconnects;
|
|
56
|
-
|
|
57
|
+
|
|
57
58
|
this.send = send || this.defaultSendFunction.bind(this);
|
|
58
|
-
|
|
59
|
+
|
|
59
60
|
this._leastUsedCache = { nodes: [], timestamp: 0 };
|
|
60
61
|
}
|
|
61
62
|
|
|
@@ -69,14 +70,14 @@ class Aqua extends EventEmitter {
|
|
|
69
70
|
if (now - this._leastUsedCache.timestamp < LEAST_USED_CACHE_TTL) {
|
|
70
71
|
return this._leastUsedCache.nodes;
|
|
71
72
|
}
|
|
72
|
-
|
|
73
|
+
|
|
73
74
|
const connectedNodes = [];
|
|
74
75
|
for (const node of this.nodeMap.values()) {
|
|
75
76
|
if (node.connected) connectedNodes.push(node);
|
|
76
77
|
}
|
|
77
|
-
|
|
78
|
+
|
|
78
79
|
connectedNodes.sort((a, b) => a.rest.calls - b.rest.calls);
|
|
79
|
-
|
|
80
|
+
|
|
80
81
|
this._leastUsedCache = { nodes: connectedNodes, timestamp: now };
|
|
81
82
|
return connectedNodes;
|
|
82
83
|
}
|
|
@@ -84,35 +85,35 @@ class Aqua extends EventEmitter {
|
|
|
84
85
|
async init(clientId) {
|
|
85
86
|
if (this.initiated) return this;
|
|
86
87
|
this.clientId = clientId;
|
|
87
|
-
|
|
88
|
+
|
|
88
89
|
try {
|
|
89
90
|
const nodePromises = [];
|
|
90
91
|
for (const node of this.nodes) {
|
|
91
92
|
nodePromises.push(this.createNode(node));
|
|
92
93
|
}
|
|
93
94
|
await Promise.all(nodePromises);
|
|
94
|
-
|
|
95
|
+
|
|
95
96
|
for (const plugin of this.plugins) {
|
|
96
97
|
plugin.load(this);
|
|
97
98
|
}
|
|
98
|
-
|
|
99
|
+
|
|
99
100
|
this.initiated = true;
|
|
100
101
|
} catch (error) {
|
|
101
102
|
this.initiated = false;
|
|
102
103
|
throw error;
|
|
103
104
|
}
|
|
104
|
-
|
|
105
|
+
|
|
105
106
|
return this;
|
|
106
107
|
}
|
|
107
108
|
|
|
108
109
|
async createNode(options) {
|
|
109
110
|
const nodeId = options.name || options.host;
|
|
110
111
|
this.destroyNode(nodeId);
|
|
111
|
-
|
|
112
|
+
|
|
112
113
|
const node = new Node(this, options, this.options);
|
|
113
114
|
this.nodeMap.set(nodeId, node);
|
|
114
115
|
this._leastUsedCache.timestamp = 0;
|
|
115
|
-
|
|
116
|
+
|
|
116
117
|
try {
|
|
117
118
|
await node.connect();
|
|
118
119
|
this.emit("nodeCreate", node);
|
|
@@ -127,7 +128,7 @@ class Aqua extends EventEmitter {
|
|
|
127
128
|
destroyNode(identifier) {
|
|
128
129
|
const node = this.nodeMap.get(identifier);
|
|
129
130
|
if (!node) return;
|
|
130
|
-
|
|
131
|
+
|
|
131
132
|
node.destroy();
|
|
132
133
|
this.nodeMap.delete(identifier);
|
|
133
134
|
this._leastUsedCache.timestamp = 0;
|
|
@@ -137,14 +138,14 @@ class Aqua extends EventEmitter {
|
|
|
137
138
|
updateVoiceState({ d, t }) {
|
|
138
139
|
const player = this.players.get(d.guild_id);
|
|
139
140
|
if (!player) return;
|
|
140
|
-
|
|
141
|
+
|
|
141
142
|
if (t === "VOICE_SERVER_UPDATE" || (t === "VOICE_STATE_UPDATE" && d.user_id === this.clientId)) {
|
|
142
143
|
if (t === "VOICE_SERVER_UPDATE") {
|
|
143
144
|
player.connection?.setServerUpdate?.(d);
|
|
144
145
|
} else {
|
|
145
146
|
player.connection?.setStateUpdate?.(d);
|
|
146
147
|
}
|
|
147
|
-
|
|
148
|
+
|
|
148
149
|
if (d.channel_id === null) {
|
|
149
150
|
this.cleanupPlayer(player);
|
|
150
151
|
}
|
|
@@ -153,23 +154,23 @@ class Aqua extends EventEmitter {
|
|
|
153
154
|
|
|
154
155
|
fetchRegion(region) {
|
|
155
156
|
if (!region) return this.leastUsedNodes;
|
|
156
|
-
|
|
157
|
+
|
|
157
158
|
const lowerRegion = region.toLowerCase();
|
|
158
159
|
const regionNodes = [];
|
|
159
|
-
|
|
160
|
+
|
|
160
161
|
for (const node of this.nodeMap.values()) {
|
|
161
162
|
if (node.connected && node.regions?.includes(lowerRegion)) {
|
|
162
163
|
regionNodes.push(node);
|
|
163
164
|
}
|
|
164
165
|
}
|
|
165
|
-
|
|
166
|
+
|
|
166
167
|
const loadCache = new Map();
|
|
167
168
|
regionNodes.sort((a, b) => {
|
|
168
169
|
if (!loadCache.has(a)) loadCache.set(a, this.calculateLoad(a));
|
|
169
170
|
if (!loadCache.has(b)) loadCache.set(b, this.calculateLoad(b));
|
|
170
171
|
return loadCache.get(a) - loadCache.get(b);
|
|
171
172
|
});
|
|
172
|
-
|
|
173
|
+
|
|
173
174
|
return regionNodes;
|
|
174
175
|
}
|
|
175
176
|
|
|
@@ -182,27 +183,28 @@ class Aqua extends EventEmitter {
|
|
|
182
183
|
|
|
183
184
|
createConnection(options) {
|
|
184
185
|
if (!this.initiated) throw new Error("Aqua must be initialized before this operation");
|
|
185
|
-
|
|
186
|
+
|
|
186
187
|
const existingPlayer = this.players.get(options.guildId);
|
|
187
188
|
if (existingPlayer && existingPlayer.voiceChannel) return existingPlayer;
|
|
188
|
-
|
|
189
|
+
|
|
189
190
|
const availableNodes = options.region ? this.fetchRegion(options.region) : this.leastUsedNodes;
|
|
190
191
|
const node = availableNodes[0];
|
|
191
192
|
if (!node) throw new Error("No nodes are available");
|
|
192
|
-
|
|
193
|
+
|
|
193
194
|
return this.createPlayer(node, options);
|
|
194
195
|
}
|
|
195
196
|
|
|
196
197
|
createPlayer(node, options) {
|
|
197
198
|
this.destroyPlayer(options.guildId);
|
|
198
|
-
|
|
199
|
+
|
|
199
200
|
const player = new Player(this, node, options);
|
|
200
201
|
this.players.set(options.guildId, player);
|
|
201
|
-
|
|
202
|
+
|
|
202
203
|
player.on("destroy", () => {
|
|
203
204
|
this.players.delete(options.guildId);
|
|
205
|
+
this.emit("playerDestroy", player);
|
|
204
206
|
});
|
|
205
|
-
|
|
207
|
+
|
|
206
208
|
player.connect(options);
|
|
207
209
|
this.emit("playerCreate", player);
|
|
208
210
|
return player;
|
|
@@ -211,7 +213,7 @@ class Aqua extends EventEmitter {
|
|
|
211
213
|
async destroyPlayer(guildId) {
|
|
212
214
|
const player = this.players.get(guildId);
|
|
213
215
|
if (!player) return;
|
|
214
|
-
|
|
216
|
+
|
|
215
217
|
try {
|
|
216
218
|
await player.clearData();
|
|
217
219
|
player.removeAllListeners();
|
|
@@ -224,18 +226,18 @@ class Aqua extends EventEmitter {
|
|
|
224
226
|
|
|
225
227
|
async resolve({ query, source = this.defaultSearchPlatform, requester, nodes }) {
|
|
226
228
|
if (!this.initiated) throw new Error("Aqua must be initialized before this operation");
|
|
227
|
-
|
|
229
|
+
|
|
228
230
|
const requestNode = this.getRequestNode(nodes);
|
|
229
231
|
const formattedQuery = URL_REGEX.test(query) ? query : `${source}:${query}`;
|
|
230
|
-
|
|
232
|
+
|
|
231
233
|
try {
|
|
232
234
|
const endpoint = `/v4/loadtracks?identifier=${encodeURIComponent(formattedQuery)}`;
|
|
233
235
|
const response = await requestNode.rest.makeRequest("GET", endpoint);
|
|
234
|
-
|
|
236
|
+
|
|
235
237
|
if (["empty", "NO_MATCHES"].includes(response.loadType)) {
|
|
236
238
|
return await this.handleNoMatches(query);
|
|
237
239
|
}
|
|
238
|
-
|
|
240
|
+
|
|
239
241
|
return this.constructResponse(response, requester, requestNode);
|
|
240
242
|
} catch (error) {
|
|
241
243
|
if (error.name === "AbortError") {
|
|
@@ -244,19 +246,19 @@ class Aqua extends EventEmitter {
|
|
|
244
246
|
throw new Error(`Failed to resolve track: ${error.message}`);
|
|
245
247
|
}
|
|
246
248
|
}
|
|
247
|
-
|
|
249
|
+
|
|
248
250
|
getRequestNode(nodes) {
|
|
249
251
|
if (!nodes) return this.leastUsedNodes[0];
|
|
250
|
-
|
|
252
|
+
|
|
251
253
|
if (nodes instanceof Node) return nodes;
|
|
252
254
|
if (typeof nodes === "string") {
|
|
253
255
|
const mappedNode = this.nodeMap.get(nodes);
|
|
254
256
|
return mappedNode || this.leastUsedNodes[0];
|
|
255
257
|
}
|
|
256
|
-
|
|
258
|
+
|
|
257
259
|
throw new TypeError(`'nodes' must be a string or Node instance, received: ${typeof nodes}`);
|
|
258
260
|
}
|
|
259
|
-
|
|
261
|
+
|
|
260
262
|
async handleNoMatches(query) {
|
|
261
263
|
return {
|
|
262
264
|
loadType: "empty",
|
|
@@ -266,8 +268,8 @@ class Aqua extends EventEmitter {
|
|
|
266
268
|
tracks: []
|
|
267
269
|
};
|
|
268
270
|
}
|
|
269
|
-
|
|
270
|
-
constructResponse(response, requester, requestNode) {
|
|
271
|
+
|
|
272
|
+
async constructResponse(response, requester, requestNode) {
|
|
271
273
|
const baseResponse = {
|
|
272
274
|
loadType: response.loadType,
|
|
273
275
|
exception: null,
|
|
@@ -275,28 +277,29 @@ class Aqua extends EventEmitter {
|
|
|
275
277
|
pluginInfo: response.pluginInfo ?? {},
|
|
276
278
|
tracks: []
|
|
277
279
|
};
|
|
278
|
-
|
|
280
|
+
|
|
279
281
|
if (response.loadType === "error" || response.loadType === "LOAD_FAILED") {
|
|
280
282
|
baseResponse.exception = response.data ?? response.exception;
|
|
281
283
|
return baseResponse;
|
|
282
284
|
}
|
|
283
|
-
|
|
285
|
+
|
|
284
286
|
const trackFactory = (trackData) => new Track(trackData, requester, requestNode);
|
|
285
|
-
|
|
287
|
+
|
|
286
288
|
switch (response.loadType) {
|
|
287
289
|
case "track":
|
|
288
290
|
if (response.data) {
|
|
289
291
|
baseResponse.tracks.push(trackFactory(response.data));
|
|
290
292
|
}
|
|
291
293
|
break;
|
|
292
|
-
|
|
293
294
|
case "playlist": {
|
|
294
295
|
const info = response.data?.info;
|
|
295
296
|
if (info) {
|
|
296
|
-
|
|
297
|
+
const playlistInfo = {
|
|
297
298
|
name: info.name ?? info.title,
|
|
299
|
+
thumbnail: response.data.pluginInfo?.artworkUrl ?? (response.data.tracks?.[0]?.info?.artworkUrl || null),
|
|
298
300
|
...info
|
|
299
301
|
};
|
|
302
|
+
baseResponse.playlistInfo = playlistInfo;
|
|
300
303
|
}
|
|
301
304
|
|
|
302
305
|
const tracks = response.data?.tracks;
|
|
@@ -322,7 +325,7 @@ class Aqua extends EventEmitter {
|
|
|
322
325
|
break;
|
|
323
326
|
}
|
|
324
327
|
}
|
|
325
|
-
|
|
328
|
+
|
|
326
329
|
return baseResponse;
|
|
327
330
|
}
|
|
328
331
|
|
|
@@ -334,7 +337,7 @@ class Aqua extends EventEmitter {
|
|
|
334
337
|
|
|
335
338
|
async search(query, requester, source = this.defaultSearchPlatform) {
|
|
336
339
|
if (!query || !requester) return null;
|
|
337
|
-
|
|
340
|
+
|
|
338
341
|
try {
|
|
339
342
|
const { tracks } = await this.resolve({ query, source, requester });
|
|
340
343
|
return tracks || null;
|
|
@@ -344,21 +347,94 @@ class Aqua extends EventEmitter {
|
|
|
344
347
|
}
|
|
345
348
|
}
|
|
346
349
|
|
|
347
|
-
async
|
|
348
|
-
|
|
349
|
-
|
|
350
|
+
async savePlayer(filePath = "./AquaPlayers.json") {
|
|
351
|
+
const data = Array.from(this.players.values()).map(player => ({
|
|
352
|
+
guildId: player.guildId,
|
|
353
|
+
textChannel: player.textChannel,
|
|
354
|
+
voiceChannel: player.voiceChannel,
|
|
355
|
+
track: player.current ? {
|
|
356
|
+
identifier: player.current.identifier,
|
|
357
|
+
author: player.current.author,
|
|
358
|
+
title: player.current.title,
|
|
359
|
+
uri: player.current.uri,
|
|
360
|
+
sourceName: player.current.sourceName,
|
|
361
|
+
artworkUrl: player.current.artworkUrl,
|
|
362
|
+
duration: player.current.duration,
|
|
363
|
+
position: player.position,
|
|
364
|
+
} : null,
|
|
365
|
+
requester: player.requester || player.current?.requester,
|
|
366
|
+
volume: player.volume,
|
|
367
|
+
paused: player.paused
|
|
368
|
+
}));
|
|
369
|
+
console.log(`Saving ${data.length} players to ${filePath}`);
|
|
370
|
+
await fs.writeJSON(filePath, data, { spaces: 2 });
|
|
371
|
+
this.emit("debug", "Aqua", `Saved players to ${filePath}`);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async waitForFirstNode() {
|
|
375
|
+
if (this.leastUsedNodes.length > 0) return;
|
|
376
|
+
return new Promise(resolve => {
|
|
377
|
+
const check = () => {
|
|
378
|
+
if (this.leastUsedNodes.length > 0) {
|
|
379
|
+
resolve();
|
|
380
|
+
} else {
|
|
381
|
+
setTimeout(check, 100);
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
check();
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async loadPlayers(filePath = "./AquaPlayers.json") {
|
|
389
|
+
if (!fs.existsSync(filePath)) {
|
|
390
|
+
this.emit("debug", "Aqua", `No player data found at ${filePath}`);
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
350
393
|
try {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
394
|
+
await this.waitForFirstNode();
|
|
395
|
+
const data = await fs.readJSON(filePath);
|
|
396
|
+
for (const playerData of data) {
|
|
397
|
+
const { guildId, textChannel, voiceChannel, track, volume, paused, requester } = playerData;
|
|
398
|
+
let player = this.players.get(guildId);
|
|
399
|
+
|
|
400
|
+
if (!player) {
|
|
401
|
+
player = await this.createConnection({
|
|
402
|
+
guildId: guildId,
|
|
403
|
+
textChannel: textChannel,
|
|
404
|
+
voiceChannel: voiceChannel,
|
|
405
|
+
defaultVolume: volume || 65,
|
|
406
|
+
deaf: true
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (track && player) {
|
|
411
|
+
const resolved = await this.resolve({ query: track.uri, requester });
|
|
412
|
+
if (resolved.tracks && resolved.tracks.length > 0) {
|
|
413
|
+
player.queue.add(resolved.tracks[0]);
|
|
414
|
+
player.position = track.position || 0;
|
|
415
|
+
} else {
|
|
416
|
+
this.emit("debug", "Aqua", `Could not resolve track for guild ${guildId}: ${track.uri}`);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (player) {
|
|
421
|
+
player.paused = paused || false;
|
|
422
|
+
if (!player.playing && !player.paused && player.queue.size > 0) {
|
|
423
|
+
player.play();
|
|
424
|
+
}
|
|
357
425
|
}
|
|
358
426
|
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
427
|
+
this.emit("debug", "Aqua", `Loaded players from ${filePath}`);
|
|
428
|
+
} catch (error) {
|
|
429
|
+
console.error(`Failed to load players from ${filePath}:`, error);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
async cleanupPlayer(player) {
|
|
434
|
+
if (!player) return;
|
|
435
|
+
|
|
436
|
+
try {
|
|
437
|
+
await player.destroy();
|
|
362
438
|
} catch (error) {
|
|
363
439
|
console.error(`Error during player cleanup: ${error.message}`);
|
|
364
440
|
}
|
package/build/structures/Node.js
CHANGED
|
@@ -96,7 +96,7 @@ class Node {
|
|
|
96
96
|
this.aqua.emit("nodeConnected", this);
|
|
97
97
|
|
|
98
98
|
if (this.autoResume && this.sessionId) {
|
|
99
|
-
await this.
|
|
99
|
+
await this.aqua.loadPlayers();
|
|
100
100
|
}
|
|
101
101
|
} catch (err) {
|
|
102
102
|
this.info = null;
|
|
@@ -245,13 +245,33 @@ class Node {
|
|
|
245
245
|
}
|
|
246
246
|
|
|
247
247
|
async getStats() {
|
|
248
|
-
|
|
248
|
+
|
|
249
|
+
if (this.connected && this.stats) {
|
|
249
250
|
return this.stats;
|
|
250
251
|
}
|
|
251
252
|
|
|
252
253
|
try {
|
|
253
254
|
const newStats = await this.rest.getStats();
|
|
254
|
-
|
|
255
|
+
if (newStats && this.stats) {
|
|
256
|
+
this.stats.players = newStats.players ?? this.stats.players;
|
|
257
|
+
this.stats.playingPlayers = newStats.playingPlayers ?? this.stats.playingPlayers;
|
|
258
|
+
this.stats.uptime = newStats.uptime ?? this.stats.uptime;
|
|
259
|
+
this.stats.ping = newStats.ping ?? this.stats.ping;
|
|
260
|
+
|
|
261
|
+
if (newStats.memory) {
|
|
262
|
+
Object.assign(this.stats.memory, newStats.memory);
|
|
263
|
+
this._calculateMemoryPercentages();
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (newStats.cpu) {
|
|
267
|
+
Object.assign(this.stats.cpu, newStats.cpu);
|
|
268
|
+
this._calculateCpuPercentages();
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (newStats.frameStats) {
|
|
272
|
+
Object.assign(this.stats.frameStats, newStats.frameStats);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
255
275
|
return this.stats;
|
|
256
276
|
} catch (err) {
|
|
257
277
|
this.emitError(`Failed to fetch node stats: ${err.message}`);
|
|
@@ -269,13 +289,12 @@ class Node {
|
|
|
269
289
|
|
|
270
290
|
if (payload.memory) {
|
|
271
291
|
Object.assign(this.stats.memory, payload.memory);
|
|
272
|
-
this.
|
|
273
|
-
this.stats.memory.usedPercentage = (this.stats.memory.used / this.stats.memory.allocated) * 100;
|
|
292
|
+
this._calculateMemoryPercentages();
|
|
274
293
|
}
|
|
275
294
|
|
|
276
295
|
if (payload.cpu) {
|
|
277
296
|
Object.assign(this.stats.cpu, payload.cpu);
|
|
278
|
-
this.
|
|
297
|
+
this._calculateCpuPercentages();
|
|
279
298
|
}
|
|
280
299
|
|
|
281
300
|
if (payload.frameStats) {
|
|
@@ -283,6 +302,21 @@ class Node {
|
|
|
283
302
|
}
|
|
284
303
|
}
|
|
285
304
|
|
|
305
|
+
_calculateMemoryPercentages() {
|
|
306
|
+
const { memory } = this.stats;
|
|
307
|
+
if (memory.allocated > 0) {
|
|
308
|
+
memory.freePercentage = (memory.free / memory.allocated) * 100;
|
|
309
|
+
memory.usedPercentage = (memory.used / memory.allocated) * 100;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
_calculateCpuPercentages() {
|
|
314
|
+
const { cpu } = this.stats;
|
|
315
|
+
if (cpu.cores > 0) {
|
|
316
|
+
cpu.lavalinkLoadPercentage = (cpu.lavalinkLoad / cpu.cores) * 100;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
286
320
|
_handleReadyOp(payload) {
|
|
287
321
|
if (!payload.sessionId) {
|
|
288
322
|
this.emitError("Ready payload missing sessionId");
|
|
@@ -297,10 +331,7 @@ class Node {
|
|
|
297
331
|
|
|
298
332
|
async resumePlayers() {
|
|
299
333
|
try {
|
|
300
|
-
await this.
|
|
301
|
-
resuming: true,
|
|
302
|
-
timeout: this.resumeTimeout
|
|
303
|
-
});
|
|
334
|
+
await this.aqua.loadPlayers();
|
|
304
335
|
|
|
305
336
|
this.aqua.emit("debug", this.name, "Session resumed successfully");
|
|
306
337
|
} catch (err) {
|
|
@@ -58,13 +58,13 @@ class Player extends EventEmitter {
|
|
|
58
58
|
this._updateTimeout = null;
|
|
59
59
|
this._dataStore = new Map();
|
|
60
60
|
|
|
61
|
-
this.on("playerUpdate", (
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
61
|
+
this.on("playerUpdate", (packet) => {
|
|
62
|
+
this.position = packet.state.position;
|
|
63
|
+
this.connected = packet.state.connected;
|
|
64
|
+
this.ping = packet.state.ping;
|
|
65
|
+
this.timestamp = packet.state.timestamp;
|
|
66
|
+
|
|
67
|
+
this.aqua.emit("playerUpdate", this, packet);
|
|
68
68
|
});
|
|
69
69
|
|
|
70
70
|
this.on("event", async (payload) => {
|
|
@@ -185,18 +185,18 @@ class Player extends EventEmitter {
|
|
|
185
185
|
return this.batchUpdatePlayer({ track: { encoded: this.current.track } }, true);
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
-
connect(
|
|
188
|
+
connect(options = this) {
|
|
189
|
+
const { guildId, voiceChannel, deaf = true, mute = false } = options;
|
|
189
190
|
this.deaf = deaf;
|
|
190
191
|
this.mute = mute;
|
|
191
|
-
|
|
192
|
-
guild_id:
|
|
193
|
-
channel_id:
|
|
192
|
+
this.send({
|
|
193
|
+
guild_id: guildId,
|
|
194
|
+
channel_id: voiceChannel,
|
|
194
195
|
self_deaf: deaf,
|
|
195
|
-
self_mute: mute
|
|
196
|
-
};
|
|
197
|
-
this.send(payload);
|
|
196
|
+
self_mute: mute,
|
|
197
|
+
});
|
|
198
198
|
this.connected = true;
|
|
199
|
-
this.aqua.emit("debug",
|
|
199
|
+
this.aqua.emit("debug", guildId, `Player connected to voice channel: ${voiceChannel}.`);
|
|
200
200
|
return this;
|
|
201
201
|
}
|
|
202
202
|
|
|
@@ -289,7 +289,13 @@ class Player extends EventEmitter {
|
|
|
289
289
|
if (!channel?.length) throw new TypeError("Channel must be a non-empty string.");
|
|
290
290
|
if (this.connected && channel === this.voiceChannel) throw new ReferenceError(`Player already connected to ${channel}.`);
|
|
291
291
|
this.voiceChannel = channel;
|
|
292
|
-
this.connect({
|
|
292
|
+
this.connect({
|
|
293
|
+
deaf: this.deaf,
|
|
294
|
+
guildId: this.guildId,
|
|
295
|
+
voiceChannel: this.voiceChannel,
|
|
296
|
+
textChannel: this.textChannel,
|
|
297
|
+
mute: this.mute,
|
|
298
|
+
});
|
|
293
299
|
return this;
|
|
294
300
|
}
|
|
295
301
|
|
package/build/structures/Rest.js
CHANGED
|
@@ -10,6 +10,7 @@ class Track {
|
|
|
10
10
|
this.identifier = info.identifier || '';
|
|
11
11
|
this.isSeekable = Boolean(info.isSeekable);
|
|
12
12
|
this.author = info.author || '';
|
|
13
|
+
this.position = info.position || 0;
|
|
13
14
|
this.length = info.length || 0;
|
|
14
15
|
this.duration = info.length || 0;
|
|
15
16
|
this.isStream = Boolean(info.isStream);
|
|
@@ -29,6 +30,7 @@ class Track {
|
|
|
29
30
|
return {
|
|
30
31
|
identifier: this.identifier,
|
|
31
32
|
isSeekable: this.isSeekable,
|
|
33
|
+
position: this.position,
|
|
32
34
|
author: this.author,
|
|
33
35
|
length: this.length,
|
|
34
36
|
isStream: this.isStream,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aqualink",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.2",
|
|
4
4
|
"description": "An Lavalink client, focused in pure performance and features",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"ws": "^8.18.2",
|
|
45
|
+
"fs-extra": "^11.3.0",
|
|
45
46
|
"tseep": "^1.3.1"
|
|
46
47
|
},
|
|
47
48
|
"maintainers": [
|