aqualink 2.1.2 → 2.2.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 +36 -37
- package/build/handlers/autoplay.js +95 -0
- package/build/structures/Aqua.js +33 -21
- package/build/structures/Filters.js +127 -87
- package/build/structures/Player.js +147 -47
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,50 +22,47 @@ This code is based in riffy, but its an 100% Rewrite made from scratch...
|
|
|
22
22
|
- https://github.com/topi314/LavaLyrics (RECOMMENDED)
|
|
23
23
|
- https://github.com/DRSchlaubi/lyrics.kt (?)
|
|
24
24
|
- https://github.com/DuncteBot/java-timed-lyrics (RECOMMENDED)
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# Tralalero Tralala 2.2.0 Released
|
|
27
28
|
---
|
|
28
29
|
- Improved the `AQUA` module
|
|
29
|
-
-
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
|
|
34
|
-
- Remade the `Connection` system
|
|
35
|
-
- Less overheard now.
|
|
36
|
-
- Faster connections
|
|
37
|
-
- Improved the checkings
|
|
38
|
-
- Improved error handling
|
|
39
|
-
- Fixed creating useless Objects and arrays
|
|
40
|
-
|
|
41
|
-
- Fully rewrite the `Node` system
|
|
42
|
-
- Way faster connections
|
|
43
|
-
- More stable (i think so)
|
|
44
|
-
- Faster events / messages / payloads handling
|
|
45
|
-
- Better stats handling (reusing, creating, destroyin)
|
|
46
|
-
- Some more bug fixes and stuff i forgot.
|
|
47
|
-
|
|
48
|
-
- Remade the `Player` module
|
|
49
|
-
- Now support Lazy Loading by default
|
|
50
|
-
- Better State Updates
|
|
51
|
-
- Improved Garbage Collection
|
|
52
|
-
- Rewrite to use direct comparasions
|
|
30
|
+
- Added Fast path in getRequestNode ( Reduces unnecessary type checks )
|
|
31
|
+
- Early return in handleNoMatches ( Avoids unnecessary Spotify requests )
|
|
32
|
+
- Rewrite to use manual loops on constructResponse ( faster than Array.prototype.map, makes the playlists and tracks load way faster and less recourses )
|
|
33
|
+
- Pre-allocated arrays ( Avoids dynamic resizing )
|
|
34
|
+
- Also fixed it sending double requests to lavalink.
|
|
53
35
|
|
|
54
|
-
- Improved the `Rest` module
|
|
55
|
-
- Lazy loading of http2
|
|
56
|
-
- Faster request chunks
|
|
57
|
-
- Some overall upgrades
|
|
58
36
|
|
|
59
|
-
-
|
|
60
|
-
-
|
|
61
|
-
-
|
|
37
|
+
- Remade the `Player` module
|
|
38
|
+
- More efficient track addition, void Array re-call
|
|
39
|
+
- faster event handling with direct states
|
|
40
|
+
- Faster autoplay system and more efficient by map()
|
|
41
|
+
- Reduced Object Creation
|
|
42
|
+
- Rewrite destroy() method
|
|
43
|
+
- Also improved Resource Cleanup
|
|
44
|
+
- Now emit TrackEnd and queueEnd correctly
|
|
45
|
+
|
|
46
|
+
- Rewrite the `autoplay` system
|
|
47
|
+
- Added redirect handling
|
|
48
|
+
- More efficient regex processing
|
|
49
|
+
- Set for unique URLs to avoid duplicate
|
|
50
|
+
- Use array chunks for better performance
|
|
51
|
+
- About ~30%-40% faster for resolving now.
|
|
52
|
+
|
|
53
|
+
- Rewrite the Filter system
|
|
54
|
+
- Uses Direct Assigments
|
|
55
|
+
- Avoid recreating objects on each update
|
|
56
|
+
- Property reuse in updateFilters()
|
|
57
|
+
- Uses traditional for loop
|
|
62
58
|
|
|
63
|
-
- Remade the INDEX.D.TS File: Added more 1000 lines of code. Added autocomplete, options, and documented everything.
|
|
64
59
|
|
|
65
60
|
# Docs (Wiki)
|
|
66
|
-
- https://github.
|
|
61
|
+
- https://toddythenoobdud.github.io/aqualink.github.io/
|
|
62
|
+
|
|
63
|
+
- Example bot: https://github.com/ToddyTheNoobDud/Kenium-Music
|
|
67
64
|
|
|
68
|
-
-
|
|
65
|
+
- Discord support: https://discord.com/invite/K4CVv84VBC
|
|
69
66
|
|
|
70
67
|
|
|
71
68
|
# How to install
|
|
@@ -147,7 +144,9 @@ client.on("messageCreate", async (message) => {
|
|
|
147
144
|
|
|
148
145
|
if (resolve.loadType === 'playlist') {
|
|
149
146
|
await message.channel.send(`Added ${resolve.tracks.length} songs from ${resolve.playlistInfo.name} playlist.`);
|
|
150
|
-
|
|
147
|
+
for (const track of result.tracks) {
|
|
148
|
+
player.queue.add(track);
|
|
149
|
+
}
|
|
151
150
|
if (!player.playing && !player.paused) return player.play();
|
|
152
151
|
|
|
153
152
|
} else if (resolve.loadType === 'search' || resolve.loadType === 'track') {
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
const https = require('https');
|
|
2
|
+
|
|
3
|
+
function fetch(url, options = {}) {
|
|
4
|
+
return new Promise((resolve, reject) => {
|
|
5
|
+
const req = https.get(url, options, (res) => {
|
|
6
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
7
|
+
return fetch(res.headers.location, options).then(resolve).catch(reject);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (res.statusCode !== 200) {
|
|
11
|
+
res.resume();
|
|
12
|
+
reject(new Error(`Request failed. Status code: ${res.statusCode}`));
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const chunks = [];
|
|
17
|
+
res.on('data', chunk => chunks.push(chunk));
|
|
18
|
+
res.on('end', () => resolve(Buffer.concat(chunks).toString()));
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
req.setTimeout(10000, () => {
|
|
22
|
+
req.destroy();
|
|
23
|
+
reject(new Error('Request timeout'));
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
req.on('error', err => {
|
|
27
|
+
reject(err);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
req.end();
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function scAutoPlay(url) {
|
|
35
|
+
try {
|
|
36
|
+
const html = await fetch(`${url}/recommended`);
|
|
37
|
+
|
|
38
|
+
const regex = /<a itemprop="url" href="(\/.*?)"/g;
|
|
39
|
+
const hrefs = new Set();
|
|
40
|
+
let match;
|
|
41
|
+
|
|
42
|
+
while ((match = regex.exec(html)) !== null) {
|
|
43
|
+
hrefs.add(`https://soundcloud.com${match[1]}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (hrefs.size === 0) {
|
|
47
|
+
throw new Error("No recommended tracks found on SoundCloud.");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const shuffledHrefs = Array.from(hrefs);
|
|
51
|
+
for (let i = shuffledHrefs.length - 1; i > 0; i--) {
|
|
52
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
53
|
+
[shuffledHrefs[i], shuffledHrefs[j]] = [shuffledHrefs[j], shuffledHrefs[i]];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return shuffledHrefs;
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.error("Error fetching SoundCloud recommendations:", error);
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function spAutoPlay(track_id) {
|
|
64
|
+
try {
|
|
65
|
+
const tokenResponse = await fetch("https://open.spotify.com/get_access_token?reason=transport&productType=embed");
|
|
66
|
+
const tokenData = JSON.parse(tokenResponse);
|
|
67
|
+
const accessToken = tokenData?.accessToken;
|
|
68
|
+
|
|
69
|
+
if (!accessToken) throw new Error("Failed to retrieve Spotify access token");
|
|
70
|
+
|
|
71
|
+
const recommendationsResponse = await fetch(
|
|
72
|
+
`https://api.spotify.com/v1/recommendations?limit=5&seed_tracks=${track_id}&fields=tracks.id`,
|
|
73
|
+
{
|
|
74
|
+
headers: {
|
|
75
|
+
Authorization: `Bearer ${accessToken}`,
|
|
76
|
+
'Content-Type': 'application/json'
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const data = JSON.parse(recommendationsResponse);
|
|
82
|
+
const tracks = data?.tracks || [];
|
|
83
|
+
|
|
84
|
+
if (tracks.length === 0) {
|
|
85
|
+
throw new Error("No recommended tracks found on Spotify.");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return tracks[Math.floor(Math.random() * tracks.length)].id;
|
|
89
|
+
} catch (error) {
|
|
90
|
+
console.error("Error fetching Spotify recommendations:", error);
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
module.exports = { scAutoPlay, spAutoPlay };
|
package/build/structures/Aqua.js
CHANGED
|
@@ -21,7 +21,7 @@ class Aqua extends EventEmitter {
|
|
|
21
21
|
this.players = new Map();
|
|
22
22
|
this.clientId = null;
|
|
23
23
|
this.initiated = false;
|
|
24
|
-
|
|
24
|
+
|
|
25
25
|
this.shouldDeleteMessage = options.shouldDeleteMessage ?? false;
|
|
26
26
|
this.defaultSearchPlatform = options.defaultSearchPlatform ?? 'ytsearch';
|
|
27
27
|
this.leaveOnEnd = options.leaveOnEnd ?? true;
|
|
@@ -32,7 +32,7 @@ class Aqua extends EventEmitter {
|
|
|
32
32
|
this.autoResume = options.autoResume ?? false;
|
|
33
33
|
this.infiniteReconnects = options.infiniteReconnects ?? false;
|
|
34
34
|
this.options = options;
|
|
35
|
-
|
|
35
|
+
|
|
36
36
|
this.setMaxListeners(0);
|
|
37
37
|
this._leastUsedCache = { nodes: [], timestamp: 0 };
|
|
38
38
|
}
|
|
@@ -67,11 +67,11 @@ class Aqua extends EventEmitter {
|
|
|
67
67
|
if (this.initiated) return this;
|
|
68
68
|
|
|
69
69
|
this.clientId = clientId;
|
|
70
|
-
|
|
70
|
+
|
|
71
71
|
try {
|
|
72
72
|
for (let i = 0; i < this.nodes.length; i++) { this.createNode(this.nodes[i]); }
|
|
73
73
|
if (this.plugins.length > 0) { for (let i = 0; i < this.plugins.length; i++) { this.plugins[i].load(this); } }
|
|
74
|
-
|
|
74
|
+
|
|
75
75
|
this.initiated = true;
|
|
76
76
|
} catch (error) {
|
|
77
77
|
this.initiated = false;
|
|
@@ -129,7 +129,7 @@ class Aqua extends EventEmitter {
|
|
|
129
129
|
if (!region) return this.leastUsedNodes;
|
|
130
130
|
|
|
131
131
|
const lowerRegion = region.toLowerCase();
|
|
132
|
-
const regionNodes = Array.from(this.nodeMap.values()).filter(node =>
|
|
132
|
+
const regionNodes = Array.from(this.nodeMap.values()).filter(node =>
|
|
133
133
|
node.connected && node.regions?.includes(lowerRegion)
|
|
134
134
|
);
|
|
135
135
|
regionNodes.sort((a, b) => this.calculateLoad(a) - this.calculateLoad(b));
|
|
@@ -151,7 +151,7 @@ class Aqua extends EventEmitter {
|
|
|
151
151
|
const availableNodes = options.region ? this.fetchRegion(options.region) : this.leastUsedNodes;
|
|
152
152
|
const node = availableNodes[0];
|
|
153
153
|
if (!node) throw new Error("No nodes are available");
|
|
154
|
-
|
|
154
|
+
|
|
155
155
|
return this.createPlayer(node, options);
|
|
156
156
|
}
|
|
157
157
|
|
|
@@ -159,11 +159,11 @@ class Aqua extends EventEmitter {
|
|
|
159
159
|
this.destroyPlayer(options.guildId);
|
|
160
160
|
const player = new Player(this, node, options);
|
|
161
161
|
this.players.set(options.guildId, player);
|
|
162
|
-
|
|
162
|
+
|
|
163
163
|
player.once("destroy", () => {
|
|
164
164
|
this.players.delete(options.guildId);
|
|
165
165
|
});
|
|
166
|
-
|
|
166
|
+
|
|
167
167
|
player.connect(options);
|
|
168
168
|
this.emit("playerCreate", player);
|
|
169
169
|
return player;
|
|
@@ -193,7 +193,7 @@ class Aqua extends EventEmitter {
|
|
|
193
193
|
if (["empty", "NO_MATCHES"].includes(response.loadType)) {
|
|
194
194
|
return await this.handleNoMatches(requestNode.rest, query);
|
|
195
195
|
}
|
|
196
|
-
return this.
|
|
196
|
+
return this.constructResponse(response, requester, requestNode);
|
|
197
197
|
} catch (error) {
|
|
198
198
|
if (error.name === "AbortError") {
|
|
199
199
|
throw new Error("Request timed out");
|
|
@@ -203,9 +203,12 @@ class Aqua extends EventEmitter {
|
|
|
203
203
|
}
|
|
204
204
|
|
|
205
205
|
getRequestNode(nodes) {
|
|
206
|
-
if (
|
|
206
|
+
if (!nodes) return this.leastUsedNodes[0];
|
|
207
|
+
|
|
208
|
+
if (!(typeof nodes === "string" || nodes instanceof Node)) {
|
|
207
209
|
throw new TypeError(`'nodes' must be a string or Node instance, received: ${typeof nodes}`);
|
|
208
210
|
}
|
|
211
|
+
|
|
209
212
|
return (typeof nodes === "string" ? this.nodeMap.get(nodes) : nodes) ?? this.leastUsedNodes[0];
|
|
210
213
|
}
|
|
211
214
|
|
|
@@ -221,17 +224,20 @@ class Aqua extends EventEmitter {
|
|
|
221
224
|
try {
|
|
222
225
|
const ytIdentifier = `/v4/loadtracks?identifier=https://www.youtube.com/watch?v=${query}`;
|
|
223
226
|
const youtubeResponse = await rest.makeRequest("GET", ytIdentifier);
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
return
|
|
227
|
+
|
|
228
|
+
if (!["empty", "NO_MATCHES"].includes(youtubeResponse.loadType)) {
|
|
229
|
+
return youtubeResponse;
|
|
227
230
|
}
|
|
228
|
-
|
|
231
|
+
|
|
232
|
+
const spotifyIdentifier = `/v4/loadtracks?identifier=https://open.spotify.com/track/${query}`;
|
|
233
|
+
return await rest.makeRequest("GET", spotifyIdentifier);
|
|
229
234
|
} catch (error) {
|
|
230
235
|
console.error(`Failed to resolve track: ${error.message}`);
|
|
236
|
+
throw error;
|
|
231
237
|
}
|
|
232
238
|
}
|
|
233
239
|
|
|
234
|
-
|
|
240
|
+
constructResponse(response, requester, requestNode) {
|
|
235
241
|
const baseResponse = {
|
|
236
242
|
loadType: response.loadType,
|
|
237
243
|
exception: null,
|
|
@@ -245,8 +251,7 @@ class Aqua extends EventEmitter {
|
|
|
245
251
|
return baseResponse;
|
|
246
252
|
}
|
|
247
253
|
|
|
248
|
-
|
|
249
|
-
const trackFactory = trackData => new Track(trackData, requester, requestNode);
|
|
254
|
+
const trackFactory = (trackData) => new Track(trackData, requester, requestNode);
|
|
250
255
|
|
|
251
256
|
switch (response.loadType) {
|
|
252
257
|
case "track":
|
|
@@ -254,6 +259,7 @@ class Aqua extends EventEmitter {
|
|
|
254
259
|
baseResponse.tracks.push(trackFactory(response.data));
|
|
255
260
|
}
|
|
256
261
|
break;
|
|
262
|
+
|
|
257
263
|
case "playlist":
|
|
258
264
|
if (response.data?.info) {
|
|
259
265
|
baseResponse.playlistInfo = {
|
|
@@ -261,26 +267,32 @@ class Aqua extends EventEmitter {
|
|
|
261
267
|
...response.data.info
|
|
262
268
|
};
|
|
263
269
|
}
|
|
270
|
+
|
|
264
271
|
const tracks = response.data?.tracks;
|
|
265
272
|
if (tracks?.length) {
|
|
266
|
-
|
|
267
|
-
|
|
273
|
+
const len = tracks.length;
|
|
274
|
+
baseResponse.tracks = new Array(len);
|
|
275
|
+
for (let i = 0; i < len; i++) {
|
|
268
276
|
baseResponse.tracks[i] = trackFactory(tracks[i]);
|
|
269
277
|
}
|
|
270
278
|
}
|
|
271
279
|
break;
|
|
280
|
+
|
|
272
281
|
case "search":
|
|
273
282
|
const searchData = response.data ?? [];
|
|
274
283
|
if (searchData.length) {
|
|
275
|
-
|
|
276
|
-
|
|
284
|
+
const len = searchData.length;
|
|
285
|
+
baseResponse.tracks = new Array(len);
|
|
286
|
+
for (let i = 0; i < len; i++) {
|
|
277
287
|
baseResponse.tracks[i] = trackFactory(searchData[i]);
|
|
278
288
|
}
|
|
279
289
|
}
|
|
280
290
|
break;
|
|
281
291
|
}
|
|
292
|
+
|
|
282
293
|
return baseResponse;
|
|
283
294
|
}
|
|
295
|
+
|
|
284
296
|
get(guildId) {
|
|
285
297
|
const player = this.players.get(guildId);
|
|
286
298
|
if (!player) throw new Error(`Player not found for guild ID: ${guildId}`);
|
|
@@ -3,21 +3,49 @@
|
|
|
3
3
|
class Filters {
|
|
4
4
|
constructor(player, options = {}) {
|
|
5
5
|
this.player = player;
|
|
6
|
-
this.volume = options.volume
|
|
7
|
-
this.equalizer = options.equalizer
|
|
8
|
-
this.karaoke = options.karaoke
|
|
9
|
-
this.timescale = options.timescale
|
|
10
|
-
this.tremolo = options.tremolo
|
|
11
|
-
this.vibrato = options.vibrato
|
|
12
|
-
this.rotation = options.rotation
|
|
13
|
-
this.distortion = options.distortion
|
|
14
|
-
this.channelMix = options.channelMix
|
|
15
|
-
this.lowPass = options.lowPass
|
|
16
|
-
this.bassboost = options.bassboost
|
|
17
|
-
this.slowmode = options.slowmode
|
|
18
|
-
this.nightcore = options.nightcore
|
|
19
|
-
this.vaporwave = options.vaporwave
|
|
20
|
-
this._8d = options._8d
|
|
6
|
+
this.volume = options.volume || 1;
|
|
7
|
+
this.equalizer = options.equalizer || [];
|
|
8
|
+
this.karaoke = options.karaoke || null;
|
|
9
|
+
this.timescale = options.timescale || null;
|
|
10
|
+
this.tremolo = options.tremolo || null;
|
|
11
|
+
this.vibrato = options.vibrato || null;
|
|
12
|
+
this.rotation = options.rotation || null;
|
|
13
|
+
this.distortion = options.distortion || null;
|
|
14
|
+
this.channelMix = options.channelMix || null;
|
|
15
|
+
this.lowPass = options.lowPass || null;
|
|
16
|
+
this.bassboost = options.bassboost || null;
|
|
17
|
+
this.slowmode = options.slowmode || null;
|
|
18
|
+
this.nightcore = options.nightcore || null;
|
|
19
|
+
this.vaporwave = options.vaporwave || null;
|
|
20
|
+
this._8d = options._8d || null;
|
|
21
|
+
|
|
22
|
+
this._filterDataTemplate = {
|
|
23
|
+
volume: this.volume,
|
|
24
|
+
equalizer: this.equalizer,
|
|
25
|
+
karaoke: null,
|
|
26
|
+
timescale: null,
|
|
27
|
+
tremolo: null,
|
|
28
|
+
vibrato: null,
|
|
29
|
+
rotation: null,
|
|
30
|
+
distortion: null,
|
|
31
|
+
channelMix: null,
|
|
32
|
+
lowPass: null
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
_setFilter(filterName, enabled, options, defaults) {
|
|
37
|
+
if (!enabled) {
|
|
38
|
+
this[filterName] = null;
|
|
39
|
+
return this.updateFilters();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const filterObj = {};
|
|
43
|
+
for (const [key, defaultValue] of Object.entries(defaults)) {
|
|
44
|
+
filterObj[key] = options[key] !== undefined ? options[key] : defaultValue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
this[filterName] = filterObj;
|
|
48
|
+
return this.updateFilters();
|
|
21
49
|
}
|
|
22
50
|
|
|
23
51
|
setEqualizer(bands) {
|
|
@@ -26,127 +54,137 @@ class Filters {
|
|
|
26
54
|
}
|
|
27
55
|
|
|
28
56
|
setKaraoke(enabled, options = {}) {
|
|
29
|
-
this.karaoke
|
|
30
|
-
level:
|
|
31
|
-
monoLevel:
|
|
32
|
-
filterBand:
|
|
33
|
-
filterWidth:
|
|
34
|
-
}
|
|
35
|
-
return this.updateFilters();
|
|
57
|
+
return this._setFilter('karaoke', enabled, options, {
|
|
58
|
+
level: 1.0,
|
|
59
|
+
monoLevel: 1.0,
|
|
60
|
+
filterBand: 220.0,
|
|
61
|
+
filterWidth: 100.0
|
|
62
|
+
});
|
|
36
63
|
}
|
|
37
64
|
|
|
38
65
|
setTimescale(enabled, options = {}) {
|
|
39
|
-
this.timescale
|
|
40
|
-
speed:
|
|
41
|
-
pitch:
|
|
42
|
-
rate:
|
|
43
|
-
}
|
|
44
|
-
return this.updateFilters();
|
|
66
|
+
return this._setFilter('timescale', enabled, options, {
|
|
67
|
+
speed: 1.0,
|
|
68
|
+
pitch: 1.0,
|
|
69
|
+
rate: 1.0
|
|
70
|
+
});
|
|
45
71
|
}
|
|
46
72
|
|
|
47
73
|
setTremolo(enabled, options = {}) {
|
|
48
|
-
this.tremolo
|
|
49
|
-
frequency:
|
|
50
|
-
depth:
|
|
51
|
-
}
|
|
52
|
-
return this.updateFilters();
|
|
74
|
+
return this._setFilter('tremolo', enabled, options, {
|
|
75
|
+
frequency: 2.0,
|
|
76
|
+
depth: 0.5
|
|
77
|
+
});
|
|
53
78
|
}
|
|
54
79
|
|
|
55
80
|
setVibrato(enabled, options = {}) {
|
|
56
|
-
this.vibrato
|
|
57
|
-
frequency:
|
|
58
|
-
depth:
|
|
59
|
-
}
|
|
60
|
-
return this.updateFilters();
|
|
81
|
+
return this._setFilter('vibrato', enabled, options, {
|
|
82
|
+
frequency: 2.0,
|
|
83
|
+
depth: 0.5
|
|
84
|
+
});
|
|
61
85
|
}
|
|
62
86
|
|
|
63
87
|
setRotation(enabled, options = {}) {
|
|
64
|
-
this.rotation
|
|
65
|
-
rotationHz:
|
|
66
|
-
}
|
|
67
|
-
return this.updateFilters();
|
|
88
|
+
return this._setFilter('rotation', enabled, options, {
|
|
89
|
+
rotationHz: 0.0
|
|
90
|
+
});
|
|
68
91
|
}
|
|
69
92
|
|
|
70
93
|
setDistortion(enabled, options = {}) {
|
|
71
|
-
this.distortion
|
|
72
|
-
sinOffset:
|
|
73
|
-
sinScale:
|
|
74
|
-
cosOffset:
|
|
75
|
-
cosScale:
|
|
76
|
-
tanOffset:
|
|
77
|
-
tanScale:
|
|
78
|
-
offset:
|
|
79
|
-
scale:
|
|
80
|
-
}
|
|
81
|
-
return this.updateFilters();
|
|
94
|
+
return this._setFilter('distortion', enabled, options, {
|
|
95
|
+
sinOffset: 0.0,
|
|
96
|
+
sinScale: 1.0,
|
|
97
|
+
cosOffset: 0.0,
|
|
98
|
+
cosScale: 1.0,
|
|
99
|
+
tanOffset: 0.0,
|
|
100
|
+
tanScale: 1.0,
|
|
101
|
+
offset: 0.0,
|
|
102
|
+
scale: 1.0
|
|
103
|
+
});
|
|
82
104
|
}
|
|
83
105
|
|
|
84
106
|
setChannelMix(enabled, options = {}) {
|
|
85
|
-
this.channelMix
|
|
86
|
-
leftToLeft:
|
|
87
|
-
leftToRight:
|
|
88
|
-
rightToLeft:
|
|
89
|
-
rightToRight:
|
|
90
|
-
}
|
|
91
|
-
return this.updateFilters();
|
|
107
|
+
return this._setFilter('channelMix', enabled, options, {
|
|
108
|
+
leftToLeft: 1.0,
|
|
109
|
+
leftToRight: 0.0,
|
|
110
|
+
rightToLeft: 0.0,
|
|
111
|
+
rightToRight: 1.0
|
|
112
|
+
});
|
|
92
113
|
}
|
|
93
114
|
|
|
94
115
|
setLowPass(enabled, options = {}) {
|
|
95
|
-
this.lowPass
|
|
96
|
-
smoothing:
|
|
97
|
-
}
|
|
98
|
-
return this.updateFilters();
|
|
116
|
+
return this._setFilter('lowPass', enabled, options, {
|
|
117
|
+
smoothing: 20.0
|
|
118
|
+
});
|
|
99
119
|
}
|
|
100
120
|
|
|
101
121
|
setBassboost(enabled, options = {}) {
|
|
102
|
-
if (enabled) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
this.bassboost = value;
|
|
106
|
-
const num = (value - 1) * (1.25 / 9) - 0.25;
|
|
107
|
-
return this.setEqualizer(Array(13).fill(0).map((_, i) => ({
|
|
108
|
-
band: i,
|
|
109
|
-
gain: num
|
|
110
|
-
})));
|
|
122
|
+
if (!enabled) {
|
|
123
|
+
this.bassboost = null;
|
|
124
|
+
return this.setEqualizer([]);
|
|
111
125
|
}
|
|
112
|
-
|
|
113
|
-
|
|
126
|
+
|
|
127
|
+
const value = options.value || 5;
|
|
128
|
+
if (value < 0 || value > 5) throw new Error("Bassboost value must be between 0 and 5");
|
|
129
|
+
|
|
130
|
+
this.bassboost = value;
|
|
131
|
+
const num = (value - 1) * (1.25 / 9) - 0.25;
|
|
132
|
+
|
|
133
|
+
const eq = new Array(13);
|
|
134
|
+
for (let i = 0; i < 13; i++) {
|
|
135
|
+
eq[i] = { band: i, gain: num };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return this.setEqualizer(eq);
|
|
114
139
|
}
|
|
115
140
|
|
|
116
141
|
setSlowmode(enabled, options = {}) {
|
|
117
142
|
this.slowmode = enabled;
|
|
118
|
-
return this.setTimescale(enabled, { rate: enabled ? options.rate
|
|
143
|
+
return this.setTimescale(enabled, { rate: enabled ? (options.rate || 0.8) : 1.0 });
|
|
119
144
|
}
|
|
120
145
|
|
|
121
146
|
setNightcore(enabled, options = {}) {
|
|
122
147
|
this.nightcore = enabled;
|
|
123
|
-
|
|
124
|
-
return this.setTimescale(true, { rate: options.rate ?? 1.5 });
|
|
125
|
-
}
|
|
126
|
-
return this.setTimescale(false);
|
|
148
|
+
return this.setTimescale(enabled, { rate: enabled ? (options.rate || 1.5) : 1.0 });
|
|
127
149
|
}
|
|
128
150
|
|
|
129
151
|
setVaporwave(enabled, options = {}) {
|
|
130
152
|
this.vaporwave = enabled;
|
|
131
|
-
|
|
132
|
-
return this.setTimescale(true, { pitch: options.pitch ?? 0.5 });
|
|
133
|
-
}
|
|
134
|
-
return this.setTimescale(false);
|
|
153
|
+
return this.setTimescale(enabled, { pitch: enabled ? (options.pitch || 0.5) : 1.0 });
|
|
135
154
|
}
|
|
136
155
|
|
|
137
156
|
set8D(enabled, options = {}) {
|
|
138
157
|
this._8d = enabled;
|
|
139
|
-
return this.setRotation(enabled, { rotationHz: enabled ? options.rotationHz
|
|
158
|
+
return this.setRotation(enabled, { rotationHz: enabled ? (options.rotationHz || 0.2) : 0.0 });
|
|
140
159
|
}
|
|
141
160
|
|
|
142
161
|
async clearFilters() {
|
|
143
|
-
|
|
162
|
+
this.volume = 1;
|
|
163
|
+
this.equalizer = [];
|
|
164
|
+
this.karaoke = null;
|
|
165
|
+
this.timescale = null;
|
|
166
|
+
this.tremolo = null;
|
|
167
|
+
this.vibrato = null;
|
|
168
|
+
this.rotation = null;
|
|
169
|
+
this.distortion = null;
|
|
170
|
+
this.channelMix = null;
|
|
171
|
+
this.lowPass = null;
|
|
172
|
+
this.bassboost = null;
|
|
173
|
+
this.slowmode = null;
|
|
174
|
+
this.nightcore = null;
|
|
175
|
+
this.vaporwave = null;
|
|
176
|
+
this._8d = null;
|
|
177
|
+
|
|
178
|
+
this._filterDataTemplate.volume = 1;
|
|
179
|
+
this._filterDataTemplate.equalizer = [];
|
|
180
|
+
|
|
144
181
|
await this.updateFilters();
|
|
145
182
|
return this;
|
|
146
183
|
}
|
|
147
184
|
|
|
148
185
|
async updateFilters() {
|
|
149
186
|
const filterData = {
|
|
187
|
+
...this._filterDataTemplate,
|
|
150
188
|
volume: this.volume,
|
|
151
189
|
equalizer: this.equalizer,
|
|
152
190
|
karaoke: this.karaoke,
|
|
@@ -159,6 +197,8 @@ class Filters {
|
|
|
159
197
|
lowPass: this.lowPass
|
|
160
198
|
};
|
|
161
199
|
|
|
200
|
+
this._filterDataTemplate = { ...filterData };
|
|
201
|
+
|
|
162
202
|
await this.player.nodes.rest.updatePlayer({
|
|
163
203
|
guildId: this.player.guildId,
|
|
164
204
|
data: { filters: filterData }
|
|
@@ -168,4 +208,4 @@ class Filters {
|
|
|
168
208
|
}
|
|
169
209
|
}
|
|
170
210
|
|
|
171
|
-
module.exports = Filters
|
|
211
|
+
module.exports = Filters;
|
|
@@ -4,6 +4,7 @@ const { EventEmitter } = require("events");
|
|
|
4
4
|
const Connection = require("./Connection");
|
|
5
5
|
const Queue = require("./Queue");
|
|
6
6
|
const Filters = require("./Filters");
|
|
7
|
+
const { spAutoPlay, scAutoPlay } = require('../handlers/autoplay');
|
|
7
8
|
|
|
8
9
|
class Player extends EventEmitter {
|
|
9
10
|
static LOOP_MODES = Object.freeze({
|
|
@@ -25,6 +26,7 @@ class Player extends EventEmitter {
|
|
|
25
26
|
|
|
26
27
|
constructor(aqua, nodes, options = {}) {
|
|
27
28
|
super();
|
|
29
|
+
|
|
28
30
|
this.aqua = aqua;
|
|
29
31
|
this.nodes = nodes;
|
|
30
32
|
this.guildId = options.guildId;
|
|
@@ -34,55 +36,145 @@ class Player extends EventEmitter {
|
|
|
34
36
|
this.connection = new Connection(this);
|
|
35
37
|
this.filters = new Filters(this);
|
|
36
38
|
|
|
37
|
-
|
|
38
|
-
this.volume = defaultVolume > 200 ? 200 : (defaultVolume < 0 ? 0 : defaultVolume);
|
|
39
|
-
|
|
39
|
+
this.volume = Math.max(0, Math.min(options.defaultVolume ?? 100, 200));
|
|
40
40
|
this.loop = Player.validModes.has(options.loop) ? options.loop : Player.LOOP_MODES.NONE;
|
|
41
41
|
|
|
42
42
|
this.queue = new Queue();
|
|
43
|
-
this.previousTracks =
|
|
44
|
-
this.
|
|
45
|
-
this.
|
|
43
|
+
this.previousTracks = Array(50);
|
|
44
|
+
this.previousTracksCount = 0;
|
|
45
|
+
this.shouldDeleteMessage = Boolean(options.shouldDeleteMessage);
|
|
46
|
+
this.leaveOnEnd = Boolean(options.leaveOnEnd);
|
|
46
47
|
|
|
47
48
|
this.playing = false;
|
|
48
49
|
this.paused = false;
|
|
49
50
|
this.connected = false;
|
|
50
51
|
this.current = null;
|
|
52
|
+
this.position = 0;
|
|
51
53
|
this.timestamp = 0;
|
|
52
54
|
this.ping = 0;
|
|
53
55
|
this.nowPlayingMessage = null;
|
|
54
|
-
|
|
56
|
+
this.isAutoplayEnabled = false;
|
|
57
|
+
this.isAutoplay = false;
|
|
58
|
+
|
|
55
59
|
this._handlePlayerUpdate = this._handlePlayerUpdate.bind(this);
|
|
56
60
|
this._handleEvent = this._handleEvent.bind(this);
|
|
57
61
|
|
|
58
62
|
this.on("playerUpdate", this._handlePlayerUpdate);
|
|
59
63
|
this.on("event", this._handleEvent);
|
|
60
|
-
|
|
61
|
-
this._dataStore = null;
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
get previous() {
|
|
65
|
-
return this.previousTracks[0]
|
|
67
|
+
return this.previousTracksCount > 0 ? this.previousTracks[0] : null;
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
get currenttrack() {
|
|
69
71
|
return this.current;
|
|
70
72
|
}
|
|
71
73
|
|
|
74
|
+
async autoplay(player) {
|
|
75
|
+
if (!player) throw new Error("Quick Fix: player.autoplay(player)");
|
|
76
|
+
|
|
77
|
+
if (!this.isAutoplayEnabled) {
|
|
78
|
+
this.aqua.emit("debug", this.guildId, "Autoplay is disabled.");
|
|
79
|
+
return this;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
this.isAutoplay = true;
|
|
83
|
+
if (!this.previous) return this;
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const { sourceName, identifier, uri, requester } = this.previous.info;
|
|
87
|
+
this.aqua.emit("debug", this.guildId, `Attempting autoplay for ${sourceName}`);
|
|
88
|
+
|
|
89
|
+
let query, source, response;
|
|
90
|
+
|
|
91
|
+
const sourceHandlers = {
|
|
92
|
+
youtube: async () => {
|
|
93
|
+
return {
|
|
94
|
+
query: `https://www.youtube.com/watch?v=${identifier}&list=RD${identifier}`,
|
|
95
|
+
source: "ytmsearch"
|
|
96
|
+
};
|
|
97
|
+
},
|
|
98
|
+
soundcloud: async () => {
|
|
99
|
+
const scResults = await scAutoPlay(uri);
|
|
100
|
+
if (!scResults?.length) return null;
|
|
101
|
+
return {
|
|
102
|
+
query: scResults[0],
|
|
103
|
+
source: "scsearch"
|
|
104
|
+
};
|
|
105
|
+
},
|
|
106
|
+
spotify: async () => {
|
|
107
|
+
const spResult = await spAutoPlay(identifier);
|
|
108
|
+
if (!spResult) return null;
|
|
109
|
+
return {
|
|
110
|
+
query: `https://open.spotify.com/track/${spResult}`,
|
|
111
|
+
source: "spotify"
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const handler = sourceHandlers[sourceName];
|
|
117
|
+
if (!handler) return this;
|
|
118
|
+
|
|
119
|
+
const result = await handler();
|
|
120
|
+
if (!result) return this;
|
|
121
|
+
|
|
122
|
+
({ query, source } = result);
|
|
123
|
+
|
|
124
|
+
response = await this.aqua.resolve({ query, source, requester });
|
|
125
|
+
|
|
126
|
+
if (!response?.tracks?.length || ["error", "empty", "LOAD_FAILED", "NO_MATCHES"].includes(response.loadType)) {
|
|
127
|
+
return this.stop();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const track = response.tracks[Math.floor(Math.random() * response.tracks.length)];
|
|
131
|
+
|
|
132
|
+
if (!track?.info?.title) {
|
|
133
|
+
throw new Error("Invalid track object: missing title or info.");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
track.requester = this.previous.requester || { id: "Unknown" };
|
|
137
|
+
|
|
138
|
+
this.queue.push(track);
|
|
139
|
+
await this.play();
|
|
140
|
+
|
|
141
|
+
return this;
|
|
142
|
+
} catch (error) {
|
|
143
|
+
console.error("Autoplay error:", error);
|
|
144
|
+
return this.stop();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
setAutoplay(enabled) {
|
|
149
|
+
this.isAutoplayEnabled = Boolean(enabled);
|
|
150
|
+
this.aqua.emit("debug", this.guildId, `Autoplay has been ${enabled ? "enabled" : "disabled"}.`);
|
|
151
|
+
return this;
|
|
152
|
+
}
|
|
153
|
+
|
|
72
154
|
addToPreviousTrack(track) {
|
|
73
155
|
if (!track) return;
|
|
74
156
|
|
|
75
|
-
if (this.
|
|
76
|
-
this.
|
|
157
|
+
if (this.previousTracksCount < 50) {
|
|
158
|
+
for (let i = this.previousTracksCount; i > 0; i--) {
|
|
159
|
+
this.previousTracks[i] = this.previousTracks[i-1];
|
|
160
|
+
}
|
|
161
|
+
this.previousTracks[0] = track;
|
|
162
|
+
this.previousTracksCount++;
|
|
163
|
+
} else {
|
|
164
|
+
for (let i = 49; i > 0; i--) {
|
|
165
|
+
this.previousTracks[i] = this.previousTracks[i-1];
|
|
166
|
+
}
|
|
167
|
+
this.previousTracks[0] = track;
|
|
77
168
|
}
|
|
78
|
-
this.previousTracks.unshift(track);
|
|
79
169
|
}
|
|
80
170
|
|
|
81
171
|
_handlePlayerUpdate({ state }) {
|
|
82
172
|
if (state) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (
|
|
173
|
+
const { position, timestamp, ping } = state;
|
|
174
|
+
|
|
175
|
+
if (position !== undefined) this.position = position;
|
|
176
|
+
if (timestamp !== undefined) this.timestamp = timestamp;
|
|
177
|
+
if (ping !== undefined) this.ping = ping;
|
|
86
178
|
}
|
|
87
179
|
this.aqua.emit("playerUpdate", this, { state });
|
|
88
180
|
}
|
|
@@ -111,12 +203,14 @@ class Player extends EventEmitter {
|
|
|
111
203
|
connect({ guildId, voiceChannel, deaf = true, mute = false }) {
|
|
112
204
|
if (this.connected) throw new Error("Player is already connected.");
|
|
113
205
|
|
|
114
|
-
|
|
206
|
+
const payload = {
|
|
115
207
|
guild_id: guildId,
|
|
116
208
|
channel_id: voiceChannel,
|
|
117
209
|
self_deaf: deaf,
|
|
118
210
|
self_mute: mute
|
|
119
|
-
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
this.send(payload);
|
|
120
214
|
|
|
121
215
|
this.connected = true;
|
|
122
216
|
this.aqua.emit("debug", this.guildId, `Player connected to voice channel: ${voiceChannel}.`);
|
|
@@ -128,8 +222,13 @@ class Player extends EventEmitter {
|
|
|
128
222
|
|
|
129
223
|
this.disconnect();
|
|
130
224
|
|
|
131
|
-
this.nowPlayingMessage
|
|
225
|
+
if (this.nowPlayingMessage) {
|
|
226
|
+
this.nowPlayingMessage.delete().catch(() => {});
|
|
227
|
+
this.nowPlayingMessage = null;
|
|
228
|
+
}
|
|
132
229
|
|
|
230
|
+
this.isAutoplay = false;
|
|
231
|
+
|
|
133
232
|
this.aqua.destroyPlayer(this.guildId);
|
|
134
233
|
this.nodes.rest.destroyPlayer(this.guildId);
|
|
135
234
|
this.clearData();
|
|
@@ -165,7 +264,6 @@ class Player extends EventEmitter {
|
|
|
165
264
|
|
|
166
265
|
stop() {
|
|
167
266
|
if (!this.playing) return this;
|
|
168
|
-
|
|
169
267
|
this.playing = false;
|
|
170
268
|
this.position = 0;
|
|
171
269
|
this.updatePlayer({ track: { encoded: null } });
|
|
@@ -202,6 +300,7 @@ class Player extends EventEmitter {
|
|
|
202
300
|
}
|
|
203
301
|
|
|
204
302
|
this.voiceChannel = channel;
|
|
303
|
+
|
|
205
304
|
this.connect({
|
|
206
305
|
deaf: this.deaf,
|
|
207
306
|
guildId: this.guildId,
|
|
@@ -244,30 +343,30 @@ class Player extends EventEmitter {
|
|
|
244
343
|
}
|
|
245
344
|
|
|
246
345
|
async trackStart(player, track) {
|
|
247
|
-
this.
|
|
346
|
+
this.playing = true;
|
|
347
|
+
this.paused = false;
|
|
248
348
|
this.aqua.emit("trackStart", player, track);
|
|
249
349
|
}
|
|
250
350
|
|
|
251
|
-
async trackChange(player, track) {
|
|
252
|
-
this.updateTrackState(true, false);
|
|
253
|
-
this.aqua.emit("trackChange", player, track);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
351
|
async trackEnd(player, track, payload) {
|
|
352
|
+
this.addToPreviousTrack(track);
|
|
353
|
+
|
|
257
354
|
if (this.shouldDeleteMessage && this.nowPlayingMessage) {
|
|
258
355
|
try {
|
|
259
356
|
await this.nowPlayingMessage.delete();
|
|
260
357
|
this.nowPlayingMessage = null;
|
|
261
|
-
} catch {
|
|
358
|
+
} catch (error) {
|
|
359
|
+
console.error("Error deleting now playing message:", error);
|
|
360
|
+
}
|
|
262
361
|
}
|
|
263
|
-
|
|
264
|
-
const reason = payload.reason?.toLowerCase().replace("_", "");
|
|
265
362
|
|
|
266
|
-
|
|
363
|
+
const reason = payload.reason;
|
|
364
|
+
if (reason === "LOAD_FAILED" || reason === "CLEANUP") {
|
|
267
365
|
if (!player.queue.length) {
|
|
268
366
|
this.clearData();
|
|
269
367
|
this.aqua.emit("queueEnd", player);
|
|
270
368
|
} else {
|
|
369
|
+
this.aqua.emit("trackEnd", player, track, reason);
|
|
271
370
|
await player.play();
|
|
272
371
|
}
|
|
273
372
|
return;
|
|
@@ -280,16 +379,20 @@ class Player extends EventEmitter {
|
|
|
280
379
|
}
|
|
281
380
|
|
|
282
381
|
if (player.queue.isEmpty()) {
|
|
283
|
-
this.
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
this.
|
|
382
|
+
if (this.isAutoplayEnabled) {
|
|
383
|
+
await player.autoplay(player);
|
|
384
|
+
} else {
|
|
385
|
+
this.playing = false;
|
|
386
|
+
if (this.leaveOnEnd) {
|
|
387
|
+
this.clearData();
|
|
388
|
+
this.cleanup();
|
|
389
|
+
}
|
|
390
|
+
this.aqua.emit("queueEnd", player);
|
|
287
391
|
}
|
|
288
|
-
|
|
289
|
-
|
|
392
|
+
} else {
|
|
393
|
+
this.aqua.emit("trackEnd", player, track, reason);
|
|
394
|
+
await player.play();
|
|
290
395
|
}
|
|
291
|
-
|
|
292
|
-
await player.play();
|
|
293
396
|
}
|
|
294
397
|
|
|
295
398
|
async trackError(player, track, payload) {
|
|
@@ -303,11 +406,11 @@ class Player extends EventEmitter {
|
|
|
303
406
|
}
|
|
304
407
|
|
|
305
408
|
async socketClosed(player, payload) {
|
|
306
|
-
const code = payload
|
|
409
|
+
const { code, guildId } = payload || {};
|
|
307
410
|
|
|
308
411
|
if (code === 4015 || code === 4009) {
|
|
309
412
|
this.send({
|
|
310
|
-
guild_id:
|
|
413
|
+
guild_id: guildId,
|
|
311
414
|
channel_id: this.voiceChannel,
|
|
312
415
|
self_mute: this.mute,
|
|
313
416
|
self_deaf: this.deaf,
|
|
@@ -325,7 +428,7 @@ class Player extends EventEmitter {
|
|
|
325
428
|
|
|
326
429
|
set(key, value) {
|
|
327
430
|
if (!this._dataStore) {
|
|
328
|
-
this._dataStore = new
|
|
431
|
+
this._dataStore = new Map();
|
|
329
432
|
}
|
|
330
433
|
this._dataStore.set(key, value);
|
|
331
434
|
}
|
|
@@ -335,7 +438,9 @@ class Player extends EventEmitter {
|
|
|
335
438
|
}
|
|
336
439
|
|
|
337
440
|
clearData() {
|
|
338
|
-
if (this.previousTracks)
|
|
441
|
+
if (this.previousTracks) {
|
|
442
|
+
this.previousTracksCount = 0;
|
|
443
|
+
}
|
|
339
444
|
this._dataStore = null;
|
|
340
445
|
return this;
|
|
341
446
|
}
|
|
@@ -354,11 +459,6 @@ class Player extends EventEmitter {
|
|
|
354
459
|
this.destroy();
|
|
355
460
|
}
|
|
356
461
|
}
|
|
357
|
-
|
|
358
|
-
updateTrackState(playing, paused) {
|
|
359
|
-
this.playing = playing;
|
|
360
|
-
this.paused = paused;
|
|
361
|
-
}
|
|
362
462
|
}
|
|
363
463
|
|
|
364
464
|
module.exports = Player;
|