ryanlink 1.0.1
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/LICENSE +37 -0
- package/README.md +455 -0
- package/dist/index.d.mts +1335 -0
- package/dist/index.d.ts +1335 -0
- package/dist/index.js +4694 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +4604 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +82 -0
- package/src/audio/AudioFilters.ts +316 -0
- package/src/audio/AudioQueue.ts +782 -0
- package/src/audio/AudioTrack.ts +242 -0
- package/src/audio/QueueController.ts +252 -0
- package/src/audio/TrackCollection.ts +138 -0
- package/src/audio/index.ts +9 -0
- package/src/config/defaults.ts +223 -0
- package/src/config/endpoints.ts +99 -0
- package/src/config/index.ts +9 -0
- package/src/config/patterns.ts +55 -0
- package/src/config/presets.ts +400 -0
- package/src/config/symbols.ts +31 -0
- package/src/core/PluginSystem.ts +50 -0
- package/src/core/RyanlinkPlayer.ts +403 -0
- package/src/core/index.ts +6 -0
- package/src/extensions/AutoplayExtension.ts +283 -0
- package/src/extensions/FairPlayExtension.ts +154 -0
- package/src/extensions/LyricsExtension.ts +187 -0
- package/src/extensions/PersistenceExtension.ts +182 -0
- package/src/extensions/SponsorBlockExtension.ts +81 -0
- package/src/extensions/index.ts +9 -0
- package/src/index.ts +19 -0
- package/src/lavalink/ConnectionPool.ts +326 -0
- package/src/lavalink/HttpClient.ts +316 -0
- package/src/lavalink/LavalinkConnection.ts +409 -0
- package/src/lavalink/index.ts +7 -0
- package/src/metadata.ts +88 -0
- package/src/types/api/Rest.ts +949 -0
- package/src/types/api/Websocket.ts +463 -0
- package/src/types/api/index.ts +6 -0
- package/src/types/audio/FilterManager.ts +29 -0
- package/src/types/audio/Queue.ts +4 -0
- package/src/types/audio/QueueManager.ts +30 -0
- package/src/types/audio/index.ts +7 -0
- package/src/types/common.ts +63 -0
- package/src/types/core/Player.ts +322 -0
- package/src/types/core/index.ts +5 -0
- package/src/types/index.ts +6 -0
- package/src/types/lavalink/Node.ts +173 -0
- package/src/types/lavalink/NodeManager.ts +34 -0
- package/src/types/lavalink/REST.ts +144 -0
- package/src/types/lavalink/index.ts +32 -0
- package/src/types/voice/VoiceManager.ts +176 -0
- package/src/types/voice/index.ts +5 -0
- package/src/utils/helpers.ts +169 -0
- package/src/utils/index.ts +6 -0
- package/src/utils/validators.ts +184 -0
- package/src/voice/RegionSelector.ts +184 -0
- package/src/voice/VoiceConnection.ts +451 -0
- package/src/voice/VoiceSession.ts +297 -0
- package/src/voice/index.ts +7 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,4604 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { URL } from 'url';
|
|
4
|
+
import { EventEmitter, once } from 'events';
|
|
5
|
+
import { clearTimeout as clearTimeout$1, setTimeout as setTimeout$1 } from 'timers';
|
|
6
|
+
import { WebSocket } from 'ws';
|
|
7
|
+
import { setTimeout as setTimeout$2 } from 'timers/promises';
|
|
8
|
+
|
|
9
|
+
/* Ryanlink v1.0.0 - Modern Lavalink Client | Apache-2.0 License | https://github.com/ryanwtf7/ryanlink */
|
|
10
|
+
|
|
11
|
+
var packageInfo = null;
|
|
12
|
+
function loadPackageInfo() {
|
|
13
|
+
if (packageInfo) {
|
|
14
|
+
return packageInfo;
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
const possiblePaths = [
|
|
18
|
+
join(__dirname, "..", "..", "package.json"),
|
|
19
|
+
join(__dirname, "..", "package.json"),
|
|
20
|
+
join(process.cwd(), "package.json")
|
|
21
|
+
];
|
|
22
|
+
for (const path of possiblePaths) {
|
|
23
|
+
try {
|
|
24
|
+
const content = readFileSync(path, "utf-8");
|
|
25
|
+
packageInfo = JSON.parse(content);
|
|
26
|
+
return packageInfo;
|
|
27
|
+
} catch {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
console.warn("Could not load package.json, using fallback values");
|
|
32
|
+
return {
|
|
33
|
+
name: "ryanlink",
|
|
34
|
+
version: "1.0.0",
|
|
35
|
+
repository: {
|
|
36
|
+
type: "git",
|
|
37
|
+
url: "https://github.com/ryanwtf7/ryanlink.git"
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
} catch (error) {
|
|
41
|
+
return {
|
|
42
|
+
name: "ryanlink",
|
|
43
|
+
version: "1.0.0",
|
|
44
|
+
repository: {
|
|
45
|
+
type: "git",
|
|
46
|
+
url: "https://github.com/ryanwtf7/ryanlink.git"
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
var pkg = loadPackageInfo();
|
|
52
|
+
var CLIENT_NAME = pkg.name;
|
|
53
|
+
var CLIENT_VERSION = pkg.version;
|
|
54
|
+
var CLIENT_REPOSITORY = pkg.repository?.url.replace(/\.git$/, "") ?? "https://github.com/ryanwtf7/ryanlink";
|
|
55
|
+
var PACKAGE_INFO = Object.freeze({
|
|
56
|
+
name: CLIENT_NAME,
|
|
57
|
+
version: CLIENT_VERSION,
|
|
58
|
+
repository: CLIENT_REPOSITORY,
|
|
59
|
+
description: pkg.description
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// src/config/defaults.ts
|
|
63
|
+
var DefaultRestOptions = Object.seal({
|
|
64
|
+
/**
|
|
65
|
+
* Lavalink API version
|
|
66
|
+
*/
|
|
67
|
+
version: 4,
|
|
68
|
+
/**
|
|
69
|
+
* User agent string sent with requests
|
|
70
|
+
*/
|
|
71
|
+
userAgent: `${CLIENT_NAME}/${CLIENT_VERSION} (${CLIENT_REPOSITORY})`,
|
|
72
|
+
/**
|
|
73
|
+
* Whether to include stack traces in error responses
|
|
74
|
+
*/
|
|
75
|
+
stackTrace: false,
|
|
76
|
+
/**
|
|
77
|
+
* Request timeout in milliseconds
|
|
78
|
+
*/
|
|
79
|
+
requestTimeout: 1e4
|
|
80
|
+
});
|
|
81
|
+
var DefaultNodeOptions = Object.seal({
|
|
82
|
+
/**
|
|
83
|
+
* Interval for stats updates in milliseconds
|
|
84
|
+
*/
|
|
85
|
+
statsInterval: 6e4,
|
|
86
|
+
/**
|
|
87
|
+
* Highest acceptable latency in milliseconds
|
|
88
|
+
*/
|
|
89
|
+
highestLatency: 2e3,
|
|
90
|
+
/**
|
|
91
|
+
* Delay before reconnection attempts in milliseconds
|
|
92
|
+
*/
|
|
93
|
+
reconnectDelay: 1e4,
|
|
94
|
+
/**
|
|
95
|
+
* Maximum number of reconnection attempts (-1 = infinite)
|
|
96
|
+
*/
|
|
97
|
+
reconnectLimit: 3,
|
|
98
|
+
/**
|
|
99
|
+
* WebSocket handshake timeout in milliseconds
|
|
100
|
+
*/
|
|
101
|
+
handshakeTimeout: 5e3
|
|
102
|
+
});
|
|
103
|
+
var DefaultPlayerOptions = Object.seal({
|
|
104
|
+
/**
|
|
105
|
+
* Automatically initialize player on construction
|
|
106
|
+
*/
|
|
107
|
+
autoInit: true,
|
|
108
|
+
/**
|
|
109
|
+
* Automatically sync queue state with Lavalink
|
|
110
|
+
*/
|
|
111
|
+
autoSync: true,
|
|
112
|
+
/**
|
|
113
|
+
* Default search prefix (ytsearch, ytmsearch, scsearch, etc.)
|
|
114
|
+
*/
|
|
115
|
+
queryPrefix: "ytsearch",
|
|
116
|
+
/**
|
|
117
|
+
* Automatically relocate queues when nodes disconnect
|
|
118
|
+
*/
|
|
119
|
+
relocateQueues: true,
|
|
120
|
+
/**
|
|
121
|
+
* Default function to fetch related tracks for autoplay
|
|
122
|
+
* Returns empty array by default (no autoplay)
|
|
123
|
+
*/
|
|
124
|
+
async fetchRelatedTracks() {
|
|
125
|
+
return await Promise.resolve([]);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
var DefaultQueueOptions = Object.seal({
|
|
129
|
+
/**
|
|
130
|
+
* Default volume (0-1000)
|
|
131
|
+
*/
|
|
132
|
+
volume: 100,
|
|
133
|
+
/**
|
|
134
|
+
* Default repeat mode
|
|
135
|
+
*/
|
|
136
|
+
repeatMode: "none",
|
|
137
|
+
/**
|
|
138
|
+
* Default autoplay state
|
|
139
|
+
*/
|
|
140
|
+
autoplay: false,
|
|
141
|
+
/**
|
|
142
|
+
* Default paused state
|
|
143
|
+
*/
|
|
144
|
+
paused: false
|
|
145
|
+
});
|
|
146
|
+
var DefaultFilterOptions = Object.seal({
|
|
147
|
+
/**
|
|
148
|
+
* Volume multiplier (0.0 - 5.0)
|
|
149
|
+
*/
|
|
150
|
+
volume: 1,
|
|
151
|
+
/**
|
|
152
|
+
* Equalizer bands (15 bands, -0.25 to 1.0 each)
|
|
153
|
+
*/
|
|
154
|
+
equalizer: [],
|
|
155
|
+
/**
|
|
156
|
+
* Karaoke filter
|
|
157
|
+
*/
|
|
158
|
+
karaoke: null,
|
|
159
|
+
/**
|
|
160
|
+
* Timescale filter (speed, pitch, rate)
|
|
161
|
+
*/
|
|
162
|
+
timescale: null,
|
|
163
|
+
/**
|
|
164
|
+
* Tremolo filter (frequency, depth)
|
|
165
|
+
*/
|
|
166
|
+
tremolo: null,
|
|
167
|
+
/**
|
|
168
|
+
* Vibrato filter (frequency, depth)
|
|
169
|
+
*/
|
|
170
|
+
vibrato: null,
|
|
171
|
+
/**
|
|
172
|
+
* Rotation filter (rotationHz)
|
|
173
|
+
*/
|
|
174
|
+
rotation: null,
|
|
175
|
+
/**
|
|
176
|
+
* Distortion filter
|
|
177
|
+
*/
|
|
178
|
+
distortion: null,
|
|
179
|
+
/**
|
|
180
|
+
* Channel mix filter
|
|
181
|
+
*/
|
|
182
|
+
channelMix: null,
|
|
183
|
+
/**
|
|
184
|
+
* Low pass filter
|
|
185
|
+
*/
|
|
186
|
+
lowPass: null
|
|
187
|
+
});
|
|
188
|
+
var HttpStatusCodes = Object.freeze({
|
|
189
|
+
OK: 200,
|
|
190
|
+
CREATED: 201,
|
|
191
|
+
NO_CONTENT: 204,
|
|
192
|
+
BAD_REQUEST: 400,
|
|
193
|
+
UNAUTHORIZED: 401,
|
|
194
|
+
FORBIDDEN: 403,
|
|
195
|
+
NOT_FOUND: 404,
|
|
196
|
+
METHOD_NOT_ALLOWED: 405,
|
|
197
|
+
TOO_MANY_REQUESTS: 429,
|
|
198
|
+
INTERNAL_SERVER_ERROR: 500,
|
|
199
|
+
BAD_GATEWAY: 502,
|
|
200
|
+
SERVICE_UNAVAILABLE: 503,
|
|
201
|
+
GATEWAY_TIMEOUT: 504
|
|
202
|
+
});
|
|
203
|
+
var WebSocketCloseCodes = Object.freeze({
|
|
204
|
+
NORMAL: 1e3,
|
|
205
|
+
GOING_AWAY: 1001,
|
|
206
|
+
PROTOCOL_ERROR: 1002,
|
|
207
|
+
UNSUPPORTED_DATA: 1003,
|
|
208
|
+
NO_STATUS_RECEIVED: 1005,
|
|
209
|
+
ABNORMAL_CLOSURE: 1006,
|
|
210
|
+
INVALID_FRAME_PAYLOAD_DATA: 1007,
|
|
211
|
+
POLICY_VIOLATION: 1008,
|
|
212
|
+
MESSAGE_TOO_BIG: 1009,
|
|
213
|
+
INTERNAL_ERROR: 1011
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// src/config/presets.ts
|
|
217
|
+
var EQPresets = {
|
|
218
|
+
/** A Bassboost Equalizer, so high it distorts the audio */
|
|
219
|
+
BassboostEarrape: [
|
|
220
|
+
{ band: 0, gain: 0.6 * 0.375 },
|
|
221
|
+
{ band: 1, gain: 0.67 * 0.375 },
|
|
222
|
+
{ band: 2, gain: 0.67 * 0.375 },
|
|
223
|
+
{ band: 3, gain: 0.4 * 0.375 },
|
|
224
|
+
{ band: 4, gain: -0.5 * 0.375 },
|
|
225
|
+
{ band: 5, gain: 0.15 * 0.375 },
|
|
226
|
+
{ band: 6, gain: -0.45 * 0.375 },
|
|
227
|
+
{ band: 7, gain: 0.23 * 0.375 },
|
|
228
|
+
{ band: 8, gain: 0.35 * 0.375 },
|
|
229
|
+
{ band: 9, gain: 0.45 * 0.375 },
|
|
230
|
+
{ band: 10, gain: 0.55 * 0.375 },
|
|
231
|
+
{ band: 11, gain: -0.6 * 0.375 },
|
|
232
|
+
{ band: 12, gain: 0.55 * 0.375 },
|
|
233
|
+
{ band: 13, gain: -0.5 * 0.375 },
|
|
234
|
+
{ band: 14, gain: -0.75 * 0.375 }
|
|
235
|
+
],
|
|
236
|
+
/** A High and decent Bassboost Equalizer */
|
|
237
|
+
BassboostHigh: [
|
|
238
|
+
{ band: 0, gain: 0.6 * 0.25 },
|
|
239
|
+
{ band: 1, gain: 0.67 * 0.25 },
|
|
240
|
+
{ band: 2, gain: 0.67 * 0.25 },
|
|
241
|
+
{ band: 3, gain: 0.4 * 0.25 },
|
|
242
|
+
{ band: 4, gain: -0.5 * 0.25 },
|
|
243
|
+
{ band: 5, gain: 0.15 * 0.25 },
|
|
244
|
+
{ band: 6, gain: -0.45 * 0.25 },
|
|
245
|
+
{ band: 7, gain: 0.23 * 0.25 },
|
|
246
|
+
{ band: 8, gain: 0.35 * 0.25 },
|
|
247
|
+
{ band: 9, gain: 0.45 * 0.25 },
|
|
248
|
+
{ band: 10, gain: 0.55 * 0.25 },
|
|
249
|
+
{ band: 11, gain: -0.6 * 0.25 },
|
|
250
|
+
{ band: 12, gain: 0.55 * 0.25 },
|
|
251
|
+
{ band: 13, gain: -0.5 * 0.25 },
|
|
252
|
+
{ band: 14, gain: -0.75 * 0.25 }
|
|
253
|
+
],
|
|
254
|
+
/** A decent Bassboost Equalizer */
|
|
255
|
+
BassboostMedium: [
|
|
256
|
+
{ band: 0, gain: 0.6 * 0.1875 },
|
|
257
|
+
{ band: 1, gain: 0.67 * 0.1875 },
|
|
258
|
+
{ band: 2, gain: 0.67 * 0.1875 },
|
|
259
|
+
{ band: 3, gain: 0.4 * 0.1875 },
|
|
260
|
+
{ band: 4, gain: -0.5 * 0.1875 },
|
|
261
|
+
{ band: 5, gain: 0.15 * 0.1875 },
|
|
262
|
+
{ band: 6, gain: -0.45 * 0.1875 },
|
|
263
|
+
{ band: 7, gain: 0.23 * 0.1875 },
|
|
264
|
+
{ band: 8, gain: 0.35 * 0.1875 },
|
|
265
|
+
{ band: 9, gain: 0.45 * 0.1875 },
|
|
266
|
+
{ band: 10, gain: 0.55 * 0.1875 },
|
|
267
|
+
{ band: 11, gain: -0.6 * 0.1875 },
|
|
268
|
+
{ band: 12, gain: 0.55 * 0.1875 },
|
|
269
|
+
{ band: 13, gain: -0.5 * 0.1875 },
|
|
270
|
+
{ band: 14, gain: -0.75 * 0.1875 }
|
|
271
|
+
],
|
|
272
|
+
/** A slight Bassboost Equalizer */
|
|
273
|
+
BassboostLow: [
|
|
274
|
+
{ band: 0, gain: 0.6 * 0.125 },
|
|
275
|
+
{ band: 1, gain: 0.67 * 0.125 },
|
|
276
|
+
{ band: 2, gain: 0.67 * 0.125 },
|
|
277
|
+
{ band: 3, gain: 0.4 * 0.125 },
|
|
278
|
+
{ band: 4, gain: -0.5 * 0.125 },
|
|
279
|
+
{ band: 5, gain: 0.15 * 0.125 },
|
|
280
|
+
{ band: 6, gain: -0.45 * 0.125 },
|
|
281
|
+
{ band: 7, gain: 0.23 * 0.125 },
|
|
282
|
+
{ band: 8, gain: 0.35 * 0.125 },
|
|
283
|
+
{ band: 9, gain: 0.45 * 0.125 },
|
|
284
|
+
{ band: 10, gain: 0.55 * 0.125 },
|
|
285
|
+
{ band: 11, gain: -0.6 * 0.125 },
|
|
286
|
+
{ band: 12, gain: 0.55 * 0.125 },
|
|
287
|
+
{ band: 13, gain: -0.5 * 0.125 },
|
|
288
|
+
{ band: 14, gain: -0.75 * 0.125 }
|
|
289
|
+
],
|
|
290
|
+
/** Professional high-fidelity audio preset for clear vocals and deep bass */
|
|
291
|
+
HighQuality: [
|
|
292
|
+
{ band: 0, gain: 0.15 },
|
|
293
|
+
{ band: 1, gain: 0.1 },
|
|
294
|
+
{ band: 2, gain: 0.05 },
|
|
295
|
+
{ band: 3, gain: 0 },
|
|
296
|
+
{ band: 4, gain: 0 },
|
|
297
|
+
{ band: 5, gain: 0 },
|
|
298
|
+
{ band: 6, gain: 0 },
|
|
299
|
+
{ band: 7, gain: 0 },
|
|
300
|
+
{ band: 8, gain: 0 },
|
|
301
|
+
{ band: 9, gain: 0 },
|
|
302
|
+
{ band: 10, gain: 0.05 },
|
|
303
|
+
{ band: 11, gain: 0.1 },
|
|
304
|
+
{ band: 12, gain: 0.15 },
|
|
305
|
+
{ band: 13, gain: 0.15 },
|
|
306
|
+
{ band: 14, gain: 0.15 }
|
|
307
|
+
],
|
|
308
|
+
/** Makes the Music slightly "better" */
|
|
309
|
+
BetterMusic: [
|
|
310
|
+
{ band: 0, gain: 0.25 },
|
|
311
|
+
{ band: 1, gain: 0.025 },
|
|
312
|
+
{ band: 2, gain: 0.0125 },
|
|
313
|
+
{ band: 3, gain: 0 },
|
|
314
|
+
{ band: 4, gain: 0 },
|
|
315
|
+
{ band: 5, gain: -0.0125 },
|
|
316
|
+
{ band: 6, gain: -0.025 },
|
|
317
|
+
{ band: 7, gain: -0.0175 },
|
|
318
|
+
{ band: 8, gain: 0 },
|
|
319
|
+
{ band: 9, gain: 0 },
|
|
320
|
+
{ band: 10, gain: 0.0125 },
|
|
321
|
+
{ band: 11, gain: 0.025 },
|
|
322
|
+
{ band: 12, gain: 0.25 },
|
|
323
|
+
{ band: 13, gain: 0.125 },
|
|
324
|
+
{ band: 14, gain: 0.125 }
|
|
325
|
+
],
|
|
326
|
+
/** Makes the Music sound like rock music / sound rock music better */
|
|
327
|
+
Rock: [
|
|
328
|
+
{ band: 0, gain: 0.3 },
|
|
329
|
+
{ band: 1, gain: 0.25 },
|
|
330
|
+
{ band: 2, gain: 0.2 },
|
|
331
|
+
{ band: 3, gain: 0.1 },
|
|
332
|
+
{ band: 4, gain: 0.05 },
|
|
333
|
+
{ band: 5, gain: -0.05 },
|
|
334
|
+
{ band: 6, gain: -0.15 },
|
|
335
|
+
{ band: 7, gain: -0.2 },
|
|
336
|
+
{ band: 8, gain: -0.1 },
|
|
337
|
+
{ band: 9, gain: -0.05 },
|
|
338
|
+
{ band: 10, gain: 0.05 },
|
|
339
|
+
{ band: 11, gain: 0.1 },
|
|
340
|
+
{ band: 12, gain: 0.2 },
|
|
341
|
+
{ band: 13, gain: 0.25 },
|
|
342
|
+
{ band: 14, gain: 0.3 }
|
|
343
|
+
],
|
|
344
|
+
/** Makes the Music sound like Classic music / sound Classic music better */
|
|
345
|
+
Classic: [
|
|
346
|
+
{ band: 0, gain: 0.375 },
|
|
347
|
+
{ band: 1, gain: 0.35 },
|
|
348
|
+
{ band: 2, gain: 0.125 },
|
|
349
|
+
{ band: 3, gain: 0 },
|
|
350
|
+
{ band: 4, gain: 0 },
|
|
351
|
+
{ band: 5, gain: 0.125 },
|
|
352
|
+
{ band: 6, gain: 0.55 },
|
|
353
|
+
{ band: 7, gain: 0.05 },
|
|
354
|
+
{ band: 8, gain: 0.125 },
|
|
355
|
+
{ band: 9, gain: 0.25 },
|
|
356
|
+
{ band: 10, gain: 0.2 },
|
|
357
|
+
{ band: 11, gain: 0.25 },
|
|
358
|
+
{ band: 12, gain: 0.3 },
|
|
359
|
+
{ band: 13, gain: 0.25 },
|
|
360
|
+
{ band: 14, gain: 0.3 }
|
|
361
|
+
],
|
|
362
|
+
/** Makes the Music sound like Pop music / sound Pop music better */
|
|
363
|
+
Pop: [
|
|
364
|
+
{ band: 0, gain: 0.26 },
|
|
365
|
+
{ band: 1, gain: 0.22 },
|
|
366
|
+
{ band: 2, gain: 0.18 },
|
|
367
|
+
{ band: 3, gain: 0.12 },
|
|
368
|
+
{ band: 4, gain: 0.1 },
|
|
369
|
+
{ band: 5, gain: 0.03 },
|
|
370
|
+
{ band: 6, gain: -5e-3 },
|
|
371
|
+
{ band: 7, gain: -0.01 },
|
|
372
|
+
{ band: 8, gain: -0.015 },
|
|
373
|
+
{ band: 9, gain: -0.015 },
|
|
374
|
+
{ band: 10, gain: -0.01 },
|
|
375
|
+
{ band: 11, gain: -5e-3 },
|
|
376
|
+
{ band: 12, gain: 0.08 },
|
|
377
|
+
{ band: 13, gain: 0.15 },
|
|
378
|
+
{ band: 14, gain: 0.2 }
|
|
379
|
+
],
|
|
380
|
+
/** Makes the Music sound like Electronic music / sound Electronic music better */
|
|
381
|
+
Electronic: [
|
|
382
|
+
{ band: 0, gain: 0.375 },
|
|
383
|
+
{ band: 1, gain: 0.35 },
|
|
384
|
+
{ band: 2, gain: 0.125 },
|
|
385
|
+
{ band: 3, gain: 0 },
|
|
386
|
+
{ band: 4, gain: 0 },
|
|
387
|
+
{ band: 5, gain: -0.125 },
|
|
388
|
+
{ band: 6, gain: -0.125 },
|
|
389
|
+
{ band: 7, gain: 0 },
|
|
390
|
+
{ band: 8, gain: 0.25 },
|
|
391
|
+
{ band: 9, gain: 0.125 },
|
|
392
|
+
{ band: 10, gain: 0.15 },
|
|
393
|
+
{ band: 11, gain: 0.2 },
|
|
394
|
+
{ band: 12, gain: 0.25 },
|
|
395
|
+
{ band: 13, gain: 0.35 },
|
|
396
|
+
{ band: 14, gain: 0.4 }
|
|
397
|
+
],
|
|
398
|
+
/** Boosts all Bands slightly for louder and fuller sound */
|
|
399
|
+
FullSound: [
|
|
400
|
+
{ band: 0, gain: 0.25 },
|
|
401
|
+
{ band: 1, gain: 0.25 },
|
|
402
|
+
{ band: 2, gain: 0.25 },
|
|
403
|
+
{ band: 3, gain: 0.25 },
|
|
404
|
+
{ band: 4, gain: 0.25 },
|
|
405
|
+
{ band: 5, gain: 0.25 },
|
|
406
|
+
{ band: 6, gain: 0.25 },
|
|
407
|
+
{ band: 7, gain: 0.25 },
|
|
408
|
+
{ band: 8, gain: 0.25 },
|
|
409
|
+
{ band: 9, gain: 0.25 },
|
|
410
|
+
{ band: 10, gain: 0.25 },
|
|
411
|
+
{ band: 11, gain: 0.25 },
|
|
412
|
+
{ band: 12, gain: 0.25 },
|
|
413
|
+
{ band: 13, gain: 0.25 },
|
|
414
|
+
{ band: 14, gain: 0.25 }
|
|
415
|
+
],
|
|
416
|
+
/** Makes the Music sound like being in a Gaming environment / sound Gaming music better */
|
|
417
|
+
Gaming: [
|
|
418
|
+
{ band: 0, gain: 0.35 },
|
|
419
|
+
{ band: 1, gain: 0.3 },
|
|
420
|
+
{ band: 2, gain: 0.25 },
|
|
421
|
+
{ band: 3, gain: 0.2 },
|
|
422
|
+
{ band: 4, gain: 0.15 },
|
|
423
|
+
{ band: 5, gain: 0.1 },
|
|
424
|
+
{ band: 6, gain: 0.075 },
|
|
425
|
+
{ band: 7, gain: 0 },
|
|
426
|
+
{ band: 8, gain: -0.05 },
|
|
427
|
+
{ band: 9, gain: 0.05 },
|
|
428
|
+
{ band: 10, gain: 0.1 },
|
|
429
|
+
{ band: 11, gain: 0.15 },
|
|
430
|
+
{ band: 12, gain: 0.25 },
|
|
431
|
+
{ band: 13, gain: 0.3 },
|
|
432
|
+
{ band: 14, gain: 0.35 }
|
|
433
|
+
],
|
|
434
|
+
/** Nightcore preset with speed and pitch adjustments */
|
|
435
|
+
Nightcore: [
|
|
436
|
+
{ band: 0, gain: 0.2 },
|
|
437
|
+
{ band: 1, gain: 0.15 },
|
|
438
|
+
{ band: 2, gain: 0.1 },
|
|
439
|
+
{ band: 3, gain: 0.05 },
|
|
440
|
+
{ band: 4, gain: 0 },
|
|
441
|
+
{ band: 5, gain: -0.05 },
|
|
442
|
+
{ band: 6, gain: -0.1 },
|
|
443
|
+
{ band: 7, gain: -0.05 },
|
|
444
|
+
{ band: 8, gain: 0 },
|
|
445
|
+
{ band: 9, gain: 0.05 },
|
|
446
|
+
{ band: 10, gain: 0.1 },
|
|
447
|
+
{ band: 11, gain: 0.15 },
|
|
448
|
+
{ band: 12, gain: 0.2 },
|
|
449
|
+
{ band: 13, gain: 0.25 },
|
|
450
|
+
{ band: 14, gain: 0.3 }
|
|
451
|
+
],
|
|
452
|
+
/** Vaporwave preset */
|
|
453
|
+
Vaporwave: [
|
|
454
|
+
{ band: 0, gain: 0.35 },
|
|
455
|
+
{ band: 1, gain: 0.3 },
|
|
456
|
+
{ band: 2, gain: 0.2 },
|
|
457
|
+
{ band: 3, gain: 0.1 },
|
|
458
|
+
{ band: 4, gain: 0 },
|
|
459
|
+
{ band: 5, gain: -0.05 },
|
|
460
|
+
{ band: 6, gain: -0.1 },
|
|
461
|
+
{ band: 7, gain: -0.15 },
|
|
462
|
+
{ band: 8, gain: -0.1 },
|
|
463
|
+
{ band: 9, gain: 0 },
|
|
464
|
+
{ band: 10, gain: 0.1 },
|
|
465
|
+
{ band: 11, gain: 0.15 },
|
|
466
|
+
{ band: 12, gain: 0.2 },
|
|
467
|
+
{ band: 13, gain: 0.25 },
|
|
468
|
+
{ band: 14, gain: 0.3 }
|
|
469
|
+
],
|
|
470
|
+
/** Treble and Bass boost */
|
|
471
|
+
TrebleBass: [
|
|
472
|
+
{ band: 0, gain: 0.3 },
|
|
473
|
+
{ band: 1, gain: 0.25 },
|
|
474
|
+
{ band: 2, gain: 0.2 },
|
|
475
|
+
{ band: 3, gain: 0.1 },
|
|
476
|
+
{ band: 4, gain: 0 },
|
|
477
|
+
{ band: 5, gain: -0.05 },
|
|
478
|
+
{ band: 6, gain: -0.1 },
|
|
479
|
+
{ band: 7, gain: -0.05 },
|
|
480
|
+
{ band: 8, gain: 0 },
|
|
481
|
+
{ band: 9, gain: 0.05 },
|
|
482
|
+
{ band: 10, gain: 0.1 },
|
|
483
|
+
{ band: 11, gain: 0.15 },
|
|
484
|
+
{ band: 12, gain: 0.2 },
|
|
485
|
+
{ band: 13, gain: 0.25 },
|
|
486
|
+
{ band: 14, gain: 0.3 }
|
|
487
|
+
],
|
|
488
|
+
/** Soft preset */
|
|
489
|
+
Soft: [
|
|
490
|
+
{ band: 0, gain: 0.1 },
|
|
491
|
+
{ band: 1, gain: 0.08 },
|
|
492
|
+
{ band: 2, gain: 0.06 },
|
|
493
|
+
{ band: 3, gain: 0.04 },
|
|
494
|
+
{ band: 4, gain: 0.02 },
|
|
495
|
+
{ band: 5, gain: 0 },
|
|
496
|
+
{ band: 6, gain: 0 },
|
|
497
|
+
{ band: 7, gain: 0 },
|
|
498
|
+
{ band: 8, gain: 0 },
|
|
499
|
+
{ band: 9, gain: 0 },
|
|
500
|
+
{ band: 10, gain: 0.02 },
|
|
501
|
+
{ band: 11, gain: 0.04 },
|
|
502
|
+
{ band: 12, gain: 0.06 },
|
|
503
|
+
{ band: 13, gain: 0.08 },
|
|
504
|
+
{ band: 14, gain: 0.1 }
|
|
505
|
+
],
|
|
506
|
+
/** TV preset */
|
|
507
|
+
TV: [
|
|
508
|
+
{ band: 0, gain: 0.2 },
|
|
509
|
+
{ band: 1, gain: 0.25 },
|
|
510
|
+
{ band: 2, gain: 0.3 },
|
|
511
|
+
{ band: 3, gain: 0.25 },
|
|
512
|
+
{ band: 4, gain: 0.2 },
|
|
513
|
+
{ band: 5, gain: 0.1 },
|
|
514
|
+
{ band: 6, gain: 0 },
|
|
515
|
+
{ band: 7, gain: 0 },
|
|
516
|
+
{ band: 8, gain: 0.1 },
|
|
517
|
+
{ band: 9, gain: 0.15 },
|
|
518
|
+
{ band: 10, gain: 0.2 },
|
|
519
|
+
{ band: 11, gain: 0.15 },
|
|
520
|
+
{ band: 12, gain: 0.1 },
|
|
521
|
+
{ band: 13, gain: 0.05 },
|
|
522
|
+
{ band: 14, gain: 0 }
|
|
523
|
+
],
|
|
524
|
+
/** Radio preset */
|
|
525
|
+
Radio: [
|
|
526
|
+
{ band: 0, gain: 0.15 },
|
|
527
|
+
{ band: 1, gain: 0.2 },
|
|
528
|
+
{ band: 2, gain: 0.25 },
|
|
529
|
+
{ band: 3, gain: 0.2 },
|
|
530
|
+
{ band: 4, gain: 0.15 },
|
|
531
|
+
{ band: 5, gain: 0.1 },
|
|
532
|
+
{ band: 6, gain: 0 },
|
|
533
|
+
{ band: 7, gain: 0 },
|
|
534
|
+
{ band: 8, gain: 0 },
|
|
535
|
+
{ band: 9, gain: 0.1 },
|
|
536
|
+
{ band: 10, gain: 0.15 },
|
|
537
|
+
{ band: 11, gain: 0.2 },
|
|
538
|
+
{ band: 12, gain: 0.15 },
|
|
539
|
+
{ band: 13, gain: 0.1 },
|
|
540
|
+
{ band: 14, gain: 0.05 }
|
|
541
|
+
],
|
|
542
|
+
/** Normalization preset (all bands at 0) */
|
|
543
|
+
Normalization: [
|
|
544
|
+
{ band: 0, gain: 0 },
|
|
545
|
+
{ band: 1, gain: 0 },
|
|
546
|
+
{ band: 2, gain: 0 },
|
|
547
|
+
{ band: 3, gain: 0 },
|
|
548
|
+
{ band: 4, gain: 0 },
|
|
549
|
+
{ band: 5, gain: 0 },
|
|
550
|
+
{ band: 6, gain: 0 },
|
|
551
|
+
{ band: 7, gain: 0 },
|
|
552
|
+
{ band: 8, gain: 0 },
|
|
553
|
+
{ band: 9, gain: 0 },
|
|
554
|
+
{ band: 10, gain: 0 },
|
|
555
|
+
{ band: 11, gain: 0 },
|
|
556
|
+
{ band: 12, gain: 0 },
|
|
557
|
+
{ band: 13, gain: 0 },
|
|
558
|
+
{ band: 14, gain: 0 }
|
|
559
|
+
]
|
|
560
|
+
};
|
|
561
|
+
function getEQPreset(name) {
|
|
562
|
+
return EQPresets[name];
|
|
563
|
+
}
|
|
564
|
+
function getEQPresetNames() {
|
|
565
|
+
return Object.keys(EQPresets);
|
|
566
|
+
}
|
|
567
|
+
function isValidEQPreset(name) {
|
|
568
|
+
return name in EQPresets;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// src/config/patterns.ts
|
|
572
|
+
var SnowflakeRegex = /^\d{17,20}$/;
|
|
573
|
+
var VoiceRegionIdRegex = /^([-a-z]{2,20})(?=[-a-z\d]*\.discord\.media:\d+$)/;
|
|
574
|
+
var UrlRegex = /^https?:\/\/.+/i;
|
|
575
|
+
var YoutubeRegex = /^(https?:\/\/)?(www\.)?(youtube\.com|youtu\.be)\/.+/i;
|
|
576
|
+
var SpotifyRegex = /^https?:\/\/open\.spotify\.com\/(track|album|playlist|artist)\/.+/i;
|
|
577
|
+
var SoundCloudRegex = /^https?:\/\/(www\.)?soundcloud\.com\/.+/i;
|
|
578
|
+
var BandcampRegex = /^https?:\/\/.+\.bandcamp\.com\/(track|album)\/.+/i;
|
|
579
|
+
var TwitchRegex = /^https?:\/\/(www\.)?twitch\.tv\/.+/i;
|
|
580
|
+
var AppleMusicRegex = /^https?:\/\/music\.apple\.com\/.+/i;
|
|
581
|
+
var DeezerRegex = /^https?:\/\/(www\.)?deezer\.com\/(track|album|playlist)\/.+/i;
|
|
582
|
+
var AudioFileRegex = /^https?:\/\/.+\.(mp3|wav|ogg|flac|m4a|aac|opus|webm)(\?.*)?$/i;
|
|
583
|
+
var TidalRegex = /^https?:\/\/(?:(?:listen|www)\.)?tidal\.com\/(?:browse\/)?(?:album|track|playlist|mix)\/[a-zA-Z0-9-]+(?:\/.*)?(?:\?.*)?$/i;
|
|
584
|
+
var YandexMusicRegex = /^(?:https?:\/\/)?music\.yandex\.(?:ru|com|kz|by)\/(?:artist|album|track|users\/[0-9A-Za-z@.-]+\/playlists|playlists)\/[0-9A-Za-z-.]+(?:\/track\/[0-9]+)?(?:\/)?$/i;
|
|
585
|
+
var AmazonMusicRegex = /^https?:\/\/music\.amazon\.[^/]+\/(?:albums|tracks|artists|playlists|user-playlists|community-playlists)\/[A-Za-z0-9]+(?:\/[^/?#]+)?(?:[/?].*)?$/i;
|
|
586
|
+
var JioSaavnRegex = /^(?:https?:\/\/)?(?:www\.)?jiosaavn\.com\/(?:song|album|featured|artist|s\/playlist)\/[a-zA-Z0-9_-]+(?:\/[a-zA-Z0-9_-]+)?$/i;
|
|
587
|
+
var PandoraRegex = /^@?(?:https?:\/\/)?(?:www\.)?pandora\.com\/(?:playlist\/PL:[\d:]+|artist\/[\w-]+(?:\/[\w-]+)*\/(?:TR|AL|AR)[A-Za-z0-9]+)(?:[?#].*)?$/i;
|
|
588
|
+
var QobuzRegex = /^https?:\/\/(?:www\.|play\.|open\.)?qobuz\.com\/(?:(?:[a-z]{2}-[a-z]{2}\/)?(?:album|playlist|track|artist)\/(?:.+?\/)?[a-zA-Z0-9]+|playlist\/\d+)$/i;
|
|
589
|
+
var AudiomackRegex = /^https?:\/\/audiomack\.com\/(?:(?:[^/]+\/(?:song|album|playlist)|(?:song|album|playlist))\/[^/?#]+)$/i;
|
|
590
|
+
var MixcloudRegex = /^https?:\/\/(?:(?:www|beta|m)\.)?mixcloud\.com\/[^/]+\/(?:playlists\/[^/]+|(?!stream|uploads|favorites|listens|playlists)[^/]+|(?:uploads|favorites|listens|stream))(?:\/)?$/i;
|
|
591
|
+
var AnghamiRegex = /^https?:\/\/(?:play\.)?anghami\.com\/(?:song|album|playlist|artist)\/[0-9]+$/i;
|
|
592
|
+
var AudiusRegex = /^https?:\/\/(?:www\.)?audius\.co\/[^/]+\/[^/]+$/i;
|
|
593
|
+
var GaanaRegex = /^https?:\/\/(?:www\.)?gaana\.com\/(?:song|album|playlist)\/[^/]+$/i;
|
|
594
|
+
var InstagramRegex = /^https?:\/\/(?:www\.)?instagram\.com\/(?:p|reel|tv)\/[A-Za-z0-9_-]+(?:\/)?$/i;
|
|
595
|
+
var ShazamRegex = /^https?:\/\/(?:www\.)?shazam\.com\/track\/[0-9]+$/i;
|
|
596
|
+
|
|
597
|
+
// src/config/endpoints.ts
|
|
598
|
+
var Routes = {
|
|
599
|
+
/**
|
|
600
|
+
* WebSocket endpoint
|
|
601
|
+
* @returns `/websocket`
|
|
602
|
+
*/
|
|
603
|
+
websocket() {
|
|
604
|
+
return "/websocket";
|
|
605
|
+
},
|
|
606
|
+
/**
|
|
607
|
+
* Track loading endpoint
|
|
608
|
+
* @returns `/loadtracks`
|
|
609
|
+
*/
|
|
610
|
+
trackLoading() {
|
|
611
|
+
return "/loadtracks";
|
|
612
|
+
},
|
|
613
|
+
/**
|
|
614
|
+
* Track decoding endpoint
|
|
615
|
+
* @param multiple Whether to decode multiple tracks
|
|
616
|
+
* @returns `/decodetrack` or `/decodetracks`
|
|
617
|
+
*/
|
|
618
|
+
trackDecoding(multiple) {
|
|
619
|
+
if (multiple) {
|
|
620
|
+
return "/decodetracks";
|
|
621
|
+
}
|
|
622
|
+
return "/decodetrack";
|
|
623
|
+
},
|
|
624
|
+
/**
|
|
625
|
+
* Player endpoint
|
|
626
|
+
* @param sessionId Lavalink session ID
|
|
627
|
+
* @param guildId Optional guild ID for specific player
|
|
628
|
+
* @returns `/sessions/{sessionId}/players` or `/sessions/{sessionId}/players/{guildId}`
|
|
629
|
+
*/
|
|
630
|
+
player(sessionId, guildId) {
|
|
631
|
+
if (guildId) {
|
|
632
|
+
return `/sessions/${sessionId}/players/${guildId}`;
|
|
633
|
+
}
|
|
634
|
+
return `/sessions/${sessionId}/players`;
|
|
635
|
+
},
|
|
636
|
+
/**
|
|
637
|
+
* Session endpoint
|
|
638
|
+
* @param sessionId Lavalink session ID
|
|
639
|
+
* @returns `/sessions/{sessionId}`
|
|
640
|
+
*/
|
|
641
|
+
session(sessionId) {
|
|
642
|
+
return `/sessions/${sessionId}`;
|
|
643
|
+
},
|
|
644
|
+
/**
|
|
645
|
+
* Info endpoint
|
|
646
|
+
* @returns `/info`
|
|
647
|
+
*/
|
|
648
|
+
info() {
|
|
649
|
+
return "/info";
|
|
650
|
+
},
|
|
651
|
+
/**
|
|
652
|
+
* Stats endpoint
|
|
653
|
+
* @returns `/stats`
|
|
654
|
+
*/
|
|
655
|
+
stats() {
|
|
656
|
+
return "/stats";
|
|
657
|
+
},
|
|
658
|
+
/**
|
|
659
|
+
* Route planner endpoint
|
|
660
|
+
* @param free Optional action: `address` or `all`
|
|
661
|
+
* @returns `/routeplanner/status` or `/routeplanner/free/{action}`
|
|
662
|
+
*/
|
|
663
|
+
routePlanner(free) {
|
|
664
|
+
if (free) {
|
|
665
|
+
return `/routeplanner/free/${free}`;
|
|
666
|
+
}
|
|
667
|
+
return "/routeplanner/status";
|
|
668
|
+
},
|
|
669
|
+
/**
|
|
670
|
+
* Version endpoint
|
|
671
|
+
* @returns `/version`
|
|
672
|
+
*/
|
|
673
|
+
version() {
|
|
674
|
+
return "/version";
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
|
|
678
|
+
// src/config/symbols.ts
|
|
679
|
+
var LookupSymbol = /* @__PURE__ */ Symbol.for("lookup");
|
|
680
|
+
var UpdateSymbol = /* @__PURE__ */ Symbol.for("update");
|
|
681
|
+
var OnPingUpdateSymbol = /* @__PURE__ */ Symbol("onPingUpdate");
|
|
682
|
+
var OnVoiceCloseSymbol = /* @__PURE__ */ Symbol("onVoiceClose");
|
|
683
|
+
var OnStateUpdateSymbol = /* @__PURE__ */ Symbol("onStateUpdate");
|
|
684
|
+
var OnEventUpdateSymbol = /* @__PURE__ */ Symbol("onEventUpdate");
|
|
685
|
+
|
|
686
|
+
// src/utils/helpers.ts
|
|
687
|
+
var noop = () => {
|
|
688
|
+
};
|
|
689
|
+
var formatDuration = (ms) => {
|
|
690
|
+
if (!Number.isSafeInteger(ms) || ms <= 0) {
|
|
691
|
+
return "00:00";
|
|
692
|
+
}
|
|
693
|
+
const s = Math.floor(ms / 1e3);
|
|
694
|
+
const ss = `${s % 60}`.padStart(2, "0");
|
|
695
|
+
const mm = `${Math.floor(s / 60) % 60}`.padStart(2, "0");
|
|
696
|
+
if (s < 3600) {
|
|
697
|
+
return `${(s === 3600 ? "01:" : "") + mm}:${ss}`;
|
|
698
|
+
}
|
|
699
|
+
return `${`${Math.floor(s / 3600)}`.padStart(2, "0")}:${mm}:${ss}`;
|
|
700
|
+
};
|
|
701
|
+
var parseDuration = (duration) => {
|
|
702
|
+
const parts = duration.split(":").map(Number);
|
|
703
|
+
if (parts.length === 1) {
|
|
704
|
+
return (parts[0] ?? 0) * 1e3;
|
|
705
|
+
}
|
|
706
|
+
if (parts.length === 2) {
|
|
707
|
+
return ((parts[0] ?? 0) * 60 + (parts[1] ?? 0)) * 1e3;
|
|
708
|
+
}
|
|
709
|
+
if (parts.length === 3) {
|
|
710
|
+
return ((parts[0] ?? 0) * 3600 + (parts[1] ?? 0) * 60 + (parts[2] ?? 0)) * 1e3;
|
|
711
|
+
}
|
|
712
|
+
return 0;
|
|
713
|
+
};
|
|
714
|
+
var clamp = (value, min, max) => {
|
|
715
|
+
return Math.min(Math.max(value, min), max);
|
|
716
|
+
};
|
|
717
|
+
var sleep = (ms) => {
|
|
718
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
719
|
+
};
|
|
720
|
+
var retry = async (fn, maxAttempts = 3, baseDelay = 1e3) => {
|
|
721
|
+
let lastError;
|
|
722
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
723
|
+
try {
|
|
724
|
+
return await fn();
|
|
725
|
+
} catch (error) {
|
|
726
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
727
|
+
if (attempt < maxAttempts - 1) {
|
|
728
|
+
const delay = baseDelay * Math.pow(2, attempt);
|
|
729
|
+
await sleep(delay);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
throw lastError ?? new Error("Retry failed");
|
|
734
|
+
};
|
|
735
|
+
var shuffle = (array) => {
|
|
736
|
+
for (let i = array.length - 1; i > 0; i--) {
|
|
737
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
738
|
+
[array[i], array[j]] = [array[j], array[i]];
|
|
739
|
+
}
|
|
740
|
+
return array;
|
|
741
|
+
};
|
|
742
|
+
var randomElement = (array) => {
|
|
743
|
+
if (array.length === 0) {
|
|
744
|
+
return void 0;
|
|
745
|
+
}
|
|
746
|
+
return array[Math.floor(Math.random() * array.length)];
|
|
747
|
+
};
|
|
748
|
+
var chunk = (array, size) => {
|
|
749
|
+
const chunks = [];
|
|
750
|
+
for (let i = 0; i < array.length; i += size) {
|
|
751
|
+
chunks.push(array.slice(i, i + size));
|
|
752
|
+
}
|
|
753
|
+
return chunks;
|
|
754
|
+
};
|
|
755
|
+
var unique = (array, key) => {
|
|
756
|
+
if (!key) {
|
|
757
|
+
return [...new Set(array)];
|
|
758
|
+
}
|
|
759
|
+
const seen = /* @__PURE__ */ new Set();
|
|
760
|
+
return array.filter((item) => {
|
|
761
|
+
const k = key(item);
|
|
762
|
+
if (seen.has(k)) {
|
|
763
|
+
return false;
|
|
764
|
+
}
|
|
765
|
+
seen.add(k);
|
|
766
|
+
return true;
|
|
767
|
+
});
|
|
768
|
+
};
|
|
769
|
+
var isNumber = (input, check) => {
|
|
770
|
+
if (check === void 0) {
|
|
771
|
+
return Number.isFinite(input);
|
|
772
|
+
}
|
|
773
|
+
if (check === "integer" || check === "safe-int") {
|
|
774
|
+
return Number.isSafeInteger(input);
|
|
775
|
+
}
|
|
776
|
+
if (check === "natural" || check === "positive") {
|
|
777
|
+
return Number.isSafeInteger(input) && input > 0;
|
|
778
|
+
}
|
|
779
|
+
if (check === "whole") {
|
|
780
|
+
return Number.isSafeInteger(input) && input >= 0;
|
|
781
|
+
}
|
|
782
|
+
return false;
|
|
783
|
+
};
|
|
784
|
+
var isString = (input, check) => {
|
|
785
|
+
if (typeof input !== "string") {
|
|
786
|
+
return false;
|
|
787
|
+
}
|
|
788
|
+
if (check === void 0) {
|
|
789
|
+
return true;
|
|
790
|
+
}
|
|
791
|
+
if (check === "url") {
|
|
792
|
+
return URL.canParse(input);
|
|
793
|
+
}
|
|
794
|
+
if (check === "non-empty") {
|
|
795
|
+
return input.trim().length > 0;
|
|
796
|
+
}
|
|
797
|
+
if (check instanceof RegExp) {
|
|
798
|
+
return check.test(input);
|
|
799
|
+
}
|
|
800
|
+
return false;
|
|
801
|
+
};
|
|
802
|
+
var isRecord = (input, check) => {
|
|
803
|
+
if (!input || input.constructor !== Object) {
|
|
804
|
+
return false;
|
|
805
|
+
}
|
|
806
|
+
if (check === void 0) {
|
|
807
|
+
return true;
|
|
808
|
+
}
|
|
809
|
+
if (check === "non-empty") {
|
|
810
|
+
return Object.keys(input).length > 0;
|
|
811
|
+
}
|
|
812
|
+
return false;
|
|
813
|
+
};
|
|
814
|
+
var isArray = (input, check) => {
|
|
815
|
+
if (!Array.isArray(input)) {
|
|
816
|
+
return false;
|
|
817
|
+
}
|
|
818
|
+
if (check === void 0) {
|
|
819
|
+
return true;
|
|
820
|
+
}
|
|
821
|
+
if (check === "non-empty") {
|
|
822
|
+
return input.length !== 0;
|
|
823
|
+
}
|
|
824
|
+
if (typeof check === "function") {
|
|
825
|
+
return input.every(check);
|
|
826
|
+
}
|
|
827
|
+
return false;
|
|
828
|
+
};
|
|
829
|
+
var isBoolean = (input) => {
|
|
830
|
+
return typeof input === "boolean";
|
|
831
|
+
};
|
|
832
|
+
var isFunction = (input) => {
|
|
833
|
+
return typeof input === "function";
|
|
834
|
+
};
|
|
835
|
+
var isNullish = (input) => {
|
|
836
|
+
return input === null || input === void 0;
|
|
837
|
+
};
|
|
838
|
+
var isSnowflake = (input) => {
|
|
839
|
+
return isString(input) && /^\d{17,20}$/.test(input);
|
|
840
|
+
};
|
|
841
|
+
var isUrl = (input) => {
|
|
842
|
+
return isString(input, "url");
|
|
843
|
+
};
|
|
844
|
+
var isError = (error) => {
|
|
845
|
+
return error instanceof Error;
|
|
846
|
+
};
|
|
847
|
+
function assert(condition, message) {
|
|
848
|
+
if (!condition) {
|
|
849
|
+
throw new Error(message);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
function validateNodeOptions(options) {
|
|
853
|
+
assert(isString(options.name, "non-empty"), "Node name must be a non-empty string");
|
|
854
|
+
assert(isString(options.host, "non-empty"), "Node host must be a non-empty string");
|
|
855
|
+
assert(isNumber(options.port, "natural"), "Node port must be a positive number");
|
|
856
|
+
assert(isString(options.password, "non-empty"), "Node password must be a non-empty string");
|
|
857
|
+
}
|
|
858
|
+
function validatePlayerOptions(options) {
|
|
859
|
+
assert(isArray(options.nodes, "non-empty"), "At least one node is required");
|
|
860
|
+
assert(isFunction(options.forwardVoiceUpdate), "forwardVoiceUpdate must be a function");
|
|
861
|
+
if (options.queryPrefix !== void 0) {
|
|
862
|
+
assert(isString(options.queryPrefix, "non-empty"), "queryPrefix must be a non-empty string");
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// src/lavalink/HttpClient.ts
|
|
867
|
+
var REST = class {
|
|
868
|
+
#origin;
|
|
869
|
+
#headers;
|
|
870
|
+
#sessionId = null;
|
|
871
|
+
#requestTimeout;
|
|
872
|
+
userAgent;
|
|
873
|
+
constructor(options) {
|
|
874
|
+
if (options.origin) {
|
|
875
|
+
if (!options.origin.startsWith("http://") && !options.origin.startsWith("https://")) {
|
|
876
|
+
throw new Error("Origin must start with http:// or https://");
|
|
877
|
+
}
|
|
878
|
+
this.#origin = options.origin;
|
|
879
|
+
} else {
|
|
880
|
+
const protocol = options.secure ? "https" : "http";
|
|
881
|
+
const port = options.port ?? 2333;
|
|
882
|
+
const host = options.host ?? "localhost";
|
|
883
|
+
this.#origin = `${protocol}://${host}:${port}`;
|
|
884
|
+
}
|
|
885
|
+
if (options.version !== void 0 && options.version <= 0) {
|
|
886
|
+
throw new Error("Version must be a positive number");
|
|
887
|
+
}
|
|
888
|
+
if (options.password.includes("\n") || options.password.includes("\r")) {
|
|
889
|
+
throw new Error("Password cannot contain newline characters");
|
|
890
|
+
}
|
|
891
|
+
const userAgent = options.userAgent ?? `${CLIENT_NAME}/${CLIENT_VERSION} (${CLIENT_REPOSITORY})`;
|
|
892
|
+
if (userAgent.includes("\n") || userAgent.includes("\r")) {
|
|
893
|
+
throw new Error("User agent cannot contain newline characters");
|
|
894
|
+
}
|
|
895
|
+
this.userAgent = userAgent;
|
|
896
|
+
const requestTimeout = options.requestTimeout ?? 1e4;
|
|
897
|
+
if (requestTimeout <= 0) {
|
|
898
|
+
throw new Error("Request timeout must be a positive number");
|
|
899
|
+
}
|
|
900
|
+
this.#requestTimeout = requestTimeout;
|
|
901
|
+
this.#headers = {
|
|
902
|
+
Authorization: options.password,
|
|
903
|
+
"User-Agent": this.userAgent,
|
|
904
|
+
"Content-Type": "application/json"
|
|
905
|
+
};
|
|
906
|
+
if (options.sessionId) {
|
|
907
|
+
this.#sessionId = options.sessionId;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
get origin() {
|
|
911
|
+
return this.#origin;
|
|
912
|
+
}
|
|
913
|
+
get sessionId() {
|
|
914
|
+
return this.#sessionId;
|
|
915
|
+
}
|
|
916
|
+
set sessionId(value) {
|
|
917
|
+
if (value !== null && (typeof value !== "string" || value.trim() === "")) {
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
this.#sessionId = value;
|
|
921
|
+
}
|
|
922
|
+
/**
|
|
923
|
+
* Make a request to the Lavalink REST API
|
|
924
|
+
*/
|
|
925
|
+
async #request(path, options = {}, timeout) {
|
|
926
|
+
const url = `${this.#origin}${path}`;
|
|
927
|
+
const controller = new AbortController();
|
|
928
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout ?? this.#requestTimeout);
|
|
929
|
+
try {
|
|
930
|
+
const response = await fetch(url, {
|
|
931
|
+
...options,
|
|
932
|
+
headers: {
|
|
933
|
+
...this.#headers,
|
|
934
|
+
...options.headers
|
|
935
|
+
},
|
|
936
|
+
signal: controller.signal
|
|
937
|
+
});
|
|
938
|
+
clearTimeout(timeoutId);
|
|
939
|
+
if (!response.ok) {
|
|
940
|
+
const error = await this.#parseError(response);
|
|
941
|
+
throw error;
|
|
942
|
+
}
|
|
943
|
+
if (response.status === 204) {
|
|
944
|
+
return void 0;
|
|
945
|
+
}
|
|
946
|
+
return await response.json();
|
|
947
|
+
} catch (error) {
|
|
948
|
+
clearTimeout(timeoutId);
|
|
949
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
950
|
+
throw new Error(`Request timeout after ${timeout ?? this.#requestTimeout}ms`);
|
|
951
|
+
}
|
|
952
|
+
throw error;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
async #parseError(response) {
|
|
956
|
+
let message = `HTTP ${response.status}: ${response.statusText}`;
|
|
957
|
+
try {
|
|
958
|
+
const data = await response.json();
|
|
959
|
+
if (data.error) {
|
|
960
|
+
message = data.error;
|
|
961
|
+
} else if (data.message) {
|
|
962
|
+
message = data.message;
|
|
963
|
+
}
|
|
964
|
+
} catch {
|
|
965
|
+
}
|
|
966
|
+
const error = new Error(message);
|
|
967
|
+
error.name = "LavalinkRestError";
|
|
968
|
+
return error;
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* Load tracks from a query or URL
|
|
972
|
+
*/
|
|
973
|
+
async loadTracks(identifier) {
|
|
974
|
+
const path = `${Routes.trackLoading()}?identifier=${encodeURIComponent(identifier)}`;
|
|
975
|
+
return this.#request(path, { method: "GET" });
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* Decode a single track
|
|
979
|
+
*/
|
|
980
|
+
async decodeTrack(encoded) {
|
|
981
|
+
const path = `${Routes.trackDecoding()}?encodedTrack=${encodeURIComponent(encoded)}`;
|
|
982
|
+
return this.#request(path, { method: "GET" });
|
|
983
|
+
}
|
|
984
|
+
/**
|
|
985
|
+
* Decode multiple tracks
|
|
986
|
+
*/
|
|
987
|
+
async decodeTracks(encoded) {
|
|
988
|
+
return this.#request(Routes.trackDecoding(true), {
|
|
989
|
+
method: "POST",
|
|
990
|
+
body: JSON.stringify(encoded)
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
/**
|
|
994
|
+
* Fetch all players for a session
|
|
995
|
+
*/
|
|
996
|
+
async fetchPlayers() {
|
|
997
|
+
if (!this.#sessionId) {
|
|
998
|
+
throw new Error("No session ID available");
|
|
999
|
+
}
|
|
1000
|
+
return this.#request(Routes.player(this.#sessionId), { method: "GET" });
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* Fetch a specific player
|
|
1004
|
+
*/
|
|
1005
|
+
async fetchPlayer(guildId) {
|
|
1006
|
+
if (!this.#sessionId) {
|
|
1007
|
+
throw new Error("No session ID available");
|
|
1008
|
+
}
|
|
1009
|
+
return this.#request(Routes.player(this.#sessionId, guildId), { method: "GET" });
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* Update a player
|
|
1013
|
+
*/
|
|
1014
|
+
async updatePlayer(guildId, data, params) {
|
|
1015
|
+
if (!this.#sessionId) {
|
|
1016
|
+
throw new Error("No session ID available");
|
|
1017
|
+
}
|
|
1018
|
+
let path = Routes.player(this.#sessionId, guildId);
|
|
1019
|
+
if (params?.noReplace) {
|
|
1020
|
+
path += "?noReplace=true";
|
|
1021
|
+
}
|
|
1022
|
+
return this.#request(path, {
|
|
1023
|
+
method: "PATCH",
|
|
1024
|
+
body: JSON.stringify(data)
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
/**
|
|
1028
|
+
* Destroy a player
|
|
1029
|
+
*/
|
|
1030
|
+
async destroyPlayer(guildId) {
|
|
1031
|
+
if (!this.#sessionId) {
|
|
1032
|
+
throw new Error("No session ID available");
|
|
1033
|
+
}
|
|
1034
|
+
return this.#request(Routes.player(this.#sessionId, guildId), { method: "DELETE" });
|
|
1035
|
+
}
|
|
1036
|
+
/**
|
|
1037
|
+
* Update session configuration
|
|
1038
|
+
*/
|
|
1039
|
+
async updateSession(data) {
|
|
1040
|
+
if (!this.#sessionId) {
|
|
1041
|
+
throw new Error("No session ID available");
|
|
1042
|
+
}
|
|
1043
|
+
return this.#request(Routes.session(this.#sessionId), {
|
|
1044
|
+
method: "PATCH",
|
|
1045
|
+
body: JSON.stringify(data)
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
/**
|
|
1049
|
+
* Fetch Lavalink server info
|
|
1050
|
+
*/
|
|
1051
|
+
async fetchInfo() {
|
|
1052
|
+
return this.#request(Routes.info(), { method: "GET" });
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* Fetch node statistics
|
|
1056
|
+
*/
|
|
1057
|
+
async fetchStats() {
|
|
1058
|
+
return this.#request(Routes.stats(), { method: "GET" });
|
|
1059
|
+
}
|
|
1060
|
+
/**
|
|
1061
|
+
* Fetch Lavalink version
|
|
1062
|
+
*/
|
|
1063
|
+
async fetchVersion() {
|
|
1064
|
+
return this.#request("/version", { method: "GET" });
|
|
1065
|
+
}
|
|
1066
|
+
/**
|
|
1067
|
+
* Fetch route planner status
|
|
1068
|
+
*/
|
|
1069
|
+
async fetchRoutePlannerStatus() {
|
|
1070
|
+
return this.#request(Routes.routePlanner(), { method: "GET" });
|
|
1071
|
+
}
|
|
1072
|
+
/**
|
|
1073
|
+
* Free a specific address from the route planner
|
|
1074
|
+
*/
|
|
1075
|
+
async freeRoutePlannerAddress(address) {
|
|
1076
|
+
return this.#request(Routes.routePlanner("address"), {
|
|
1077
|
+
method: "POST",
|
|
1078
|
+
body: JSON.stringify({ address })
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
/**
|
|
1082
|
+
* Free all addresses from the route planner
|
|
1083
|
+
*/
|
|
1084
|
+
async freeAllRoutePlannerAddresses() {
|
|
1085
|
+
return this.#request(Routes.routePlanner("all"), { method: "POST" });
|
|
1086
|
+
}
|
|
1087
|
+
/**
|
|
1088
|
+
* Set SponsorBlock segments for a player
|
|
1089
|
+
*/
|
|
1090
|
+
async setSponsorBlock(guildId, segments) {
|
|
1091
|
+
if (!this.#sessionId) {
|
|
1092
|
+
throw new Error("No session ID available");
|
|
1093
|
+
}
|
|
1094
|
+
return this.#request(`${Routes.player(this.#sessionId, guildId)}/sponsorblock`, {
|
|
1095
|
+
method: "PATCH",
|
|
1096
|
+
body: JSON.stringify(segments)
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
/**
|
|
1100
|
+
* Get current SponsorBlock segments for a player
|
|
1101
|
+
*/
|
|
1102
|
+
async getSponsorBlock(guildId) {
|
|
1103
|
+
if (!this.#sessionId) {
|
|
1104
|
+
throw new Error("No session ID available");
|
|
1105
|
+
}
|
|
1106
|
+
return this.#request(`${Routes.player(this.#sessionId, guildId)}/sponsorblock`, { method: "GET" });
|
|
1107
|
+
}
|
|
1108
|
+
/**
|
|
1109
|
+
* Delete SponsorBlock configuration for a player
|
|
1110
|
+
*/
|
|
1111
|
+
async deleteSponsorBlock(guildId) {
|
|
1112
|
+
if (!this.#sessionId) {
|
|
1113
|
+
throw new Error("No session ID available");
|
|
1114
|
+
}
|
|
1115
|
+
return this.#request(`${Routes.player(this.#sessionId, guildId)}/sponsorblock`, { method: "DELETE" });
|
|
1116
|
+
}
|
|
1117
|
+
};
|
|
1118
|
+
var CloseCodes = /* @__PURE__ */ ((CloseCodes2) => {
|
|
1119
|
+
CloseCodes2[CloseCodes2["Normal"] = 1e3] = "Normal";
|
|
1120
|
+
CloseCodes2[CloseCodes2["GoingAway"] = 1001] = "GoingAway";
|
|
1121
|
+
CloseCodes2[CloseCodes2["ProtocolError"] = 1002] = "ProtocolError";
|
|
1122
|
+
CloseCodes2[CloseCodes2["UnsupportedData"] = 1003] = "UnsupportedData";
|
|
1123
|
+
CloseCodes2[CloseCodes2["NoStatusReceived"] = 1005] = "NoStatusReceived";
|
|
1124
|
+
CloseCodes2[CloseCodes2["AbnormalClosure"] = 1006] = "AbnormalClosure";
|
|
1125
|
+
CloseCodes2[CloseCodes2["InvalidFramePayloadData"] = 1007] = "InvalidFramePayloadData";
|
|
1126
|
+
CloseCodes2[CloseCodes2["PolicyViolation"] = 1008] = "PolicyViolation";
|
|
1127
|
+
CloseCodes2[CloseCodes2["MessageTooBig"] = 1009] = "MessageTooBig";
|
|
1128
|
+
CloseCodes2[CloseCodes2["InternalError"] = 1011] = "InternalError";
|
|
1129
|
+
return CloseCodes2;
|
|
1130
|
+
})(CloseCodes || {});
|
|
1131
|
+
var LavalinkNode = class extends EventEmitter {
|
|
1132
|
+
#socketConfig;
|
|
1133
|
+
#connectPromise = null;
|
|
1134
|
+
#disconnectPromise = null;
|
|
1135
|
+
#pingTimer = null;
|
|
1136
|
+
#reconnectTimer = null;
|
|
1137
|
+
#ping = null;
|
|
1138
|
+
#lastPingTime = null;
|
|
1139
|
+
#reconnectCycle = true;
|
|
1140
|
+
#reconnectAttempts = 0;
|
|
1141
|
+
#manualDisconnect = false;
|
|
1142
|
+
#socket = null;
|
|
1143
|
+
#stats = null;
|
|
1144
|
+
#socketUrl;
|
|
1145
|
+
#pingTimeout;
|
|
1146
|
+
#reconnectDelay;
|
|
1147
|
+
#reconnectLimit;
|
|
1148
|
+
name;
|
|
1149
|
+
rest;
|
|
1150
|
+
constructor(options) {
|
|
1151
|
+
super({ captureRejections: false });
|
|
1152
|
+
if (!options.name || typeof options.name !== "string") {
|
|
1153
|
+
throw new Error("Node name must be a non-empty string");
|
|
1154
|
+
}
|
|
1155
|
+
if (!options.clientId || typeof options.clientId !== "string") {
|
|
1156
|
+
throw new Error("Client ID must be a non-empty string");
|
|
1157
|
+
}
|
|
1158
|
+
this.rest = new REST(options);
|
|
1159
|
+
this.#socketConfig = {
|
|
1160
|
+
headers: {
|
|
1161
|
+
"Client-Name": `${CLIENT_NAME}/${CLIENT_VERSION}`,
|
|
1162
|
+
"User-Id": options.clientId,
|
|
1163
|
+
"User-Agent": this.rest.userAgent,
|
|
1164
|
+
Authorization: options.password
|
|
1165
|
+
},
|
|
1166
|
+
perMessageDeflate: false,
|
|
1167
|
+
handshakeTimeout: options.handshakeTimeout ?? 3e4
|
|
1168
|
+
};
|
|
1169
|
+
if (this.rest.sessionId) {
|
|
1170
|
+
this.#socketConfig.headers["Session-Id"] = this.rest.sessionId;
|
|
1171
|
+
}
|
|
1172
|
+
const protocol = options.secure ? "wss" : "ws";
|
|
1173
|
+
this.#socketUrl = `${protocol}://${options.host}:${options.port}/v4/websocket`;
|
|
1174
|
+
this.#pingTimeout = (options.statsInterval ?? 6e4) + (options.highestLatency ?? 5e3);
|
|
1175
|
+
this.#reconnectDelay = options.reconnectDelay ?? 5e3;
|
|
1176
|
+
this.#reconnectLimit = options.reconnectLimit ?? -1;
|
|
1177
|
+
this.name = options.name;
|
|
1178
|
+
Object.defineProperty(this.rest, "sessionId", {
|
|
1179
|
+
configurable: false,
|
|
1180
|
+
get: () => this.sessionId,
|
|
1181
|
+
set: () => {
|
|
1182
|
+
}
|
|
1183
|
+
});
|
|
1184
|
+
const immutable = {
|
|
1185
|
+
writable: false,
|
|
1186
|
+
configurable: false
|
|
1187
|
+
};
|
|
1188
|
+
Object.defineProperties(this, {
|
|
1189
|
+
name: immutable,
|
|
1190
|
+
rest: immutable
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
get clientId() {
|
|
1194
|
+
return this.#socketConfig.headers["User-Id"];
|
|
1195
|
+
}
|
|
1196
|
+
get sessionId() {
|
|
1197
|
+
return this.#socketConfig.headers["Session-Id"] ?? null;
|
|
1198
|
+
}
|
|
1199
|
+
get ping() {
|
|
1200
|
+
return this.#ping;
|
|
1201
|
+
}
|
|
1202
|
+
get stats() {
|
|
1203
|
+
return this.#stats;
|
|
1204
|
+
}
|
|
1205
|
+
get state() {
|
|
1206
|
+
if (this.connecting) {
|
|
1207
|
+
return "connecting";
|
|
1208
|
+
}
|
|
1209
|
+
if (this.connected) {
|
|
1210
|
+
return this.ready ? "ready" : "connected";
|
|
1211
|
+
}
|
|
1212
|
+
return this.reconnecting ? "reconnecting" : "disconnected";
|
|
1213
|
+
}
|
|
1214
|
+
get connecting() {
|
|
1215
|
+
return this.#socket?.readyState === WebSocket.CONNECTING;
|
|
1216
|
+
}
|
|
1217
|
+
get connected() {
|
|
1218
|
+
return this.#socket?.readyState === WebSocket.OPEN;
|
|
1219
|
+
}
|
|
1220
|
+
get ready() {
|
|
1221
|
+
return this.connected && this.sessionId !== null;
|
|
1222
|
+
}
|
|
1223
|
+
get reconnecting() {
|
|
1224
|
+
return this.#socket === null && this.#reconnectTimer !== null;
|
|
1225
|
+
}
|
|
1226
|
+
get disconnected() {
|
|
1227
|
+
return this.#socket === null && !this.reconnecting;
|
|
1228
|
+
}
|
|
1229
|
+
get reconnectLimit() {
|
|
1230
|
+
return this.#reconnectLimit;
|
|
1231
|
+
}
|
|
1232
|
+
get reconnectAttempts() {
|
|
1233
|
+
return this.#reconnectAttempts;
|
|
1234
|
+
}
|
|
1235
|
+
#error(err) {
|
|
1236
|
+
const data = "errors" in err && Array.isArray(err.errors) ? err.errors[err.errors.length - 1] : err;
|
|
1237
|
+
const error = data instanceof Error ? data : new Error(String(data));
|
|
1238
|
+
error.name = `Error [${this.constructor.name}]`;
|
|
1239
|
+
return error;
|
|
1240
|
+
}
|
|
1241
|
+
#cleanup() {
|
|
1242
|
+
this.#socket?.removeAllListeners();
|
|
1243
|
+
if (this.#pingTimer !== null) {
|
|
1244
|
+
clearTimeout$1(this.#pingTimer);
|
|
1245
|
+
}
|
|
1246
|
+
this.#socket = this.#pingTimer = this.#stats = null;
|
|
1247
|
+
this.#lastPingTime = this.#ping = null;
|
|
1248
|
+
}
|
|
1249
|
+
#reconnect() {
|
|
1250
|
+
this.#reconnectCycle = false;
|
|
1251
|
+
this.#reconnectTimer?.refresh();
|
|
1252
|
+
this.#reconnectTimer ??= setTimeout$1(() => {
|
|
1253
|
+
this.#reconnectCycle = true;
|
|
1254
|
+
void this.connect();
|
|
1255
|
+
}, this.#reconnectDelay).unref();
|
|
1256
|
+
}
|
|
1257
|
+
#stopReconnecting(resetCount = true, reconnectCycle = false) {
|
|
1258
|
+
this.#reconnectCycle = reconnectCycle;
|
|
1259
|
+
if (resetCount) {
|
|
1260
|
+
this.#reconnectAttempts = 0;
|
|
1261
|
+
}
|
|
1262
|
+
if (this.#reconnectTimer !== null) {
|
|
1263
|
+
clearTimeout$1(this.#reconnectTimer);
|
|
1264
|
+
}
|
|
1265
|
+
this.#reconnectTimer = null;
|
|
1266
|
+
}
|
|
1267
|
+
#keepAliveAndPing() {
|
|
1268
|
+
this.#pingTimer?.refresh();
|
|
1269
|
+
this.#pingTimer ??= setTimeout$1(() => {
|
|
1270
|
+
this.#socket?.terminate();
|
|
1271
|
+
this.#cleanup();
|
|
1272
|
+
this.#reconnect();
|
|
1273
|
+
}, this.#pingTimeout).unref();
|
|
1274
|
+
this.#lastPingTime = Date.now();
|
|
1275
|
+
this.#socket?.ping();
|
|
1276
|
+
}
|
|
1277
|
+
#parseMessageData(data) {
|
|
1278
|
+
try {
|
|
1279
|
+
return JSON.parse(data);
|
|
1280
|
+
} catch {
|
|
1281
|
+
return null;
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
/**
|
|
1285
|
+
* Connect to the Lavalink node
|
|
1286
|
+
* Handles reconnection attempts and session resumption
|
|
1287
|
+
*/
|
|
1288
|
+
async connect() {
|
|
1289
|
+
if (this.#socket !== null) {
|
|
1290
|
+
return this.#connectPromise ?? this.connected;
|
|
1291
|
+
}
|
|
1292
|
+
if (this.reconnecting) {
|
|
1293
|
+
this.#reconnectAttempts++;
|
|
1294
|
+
if (!this.#reconnectCycle) {
|
|
1295
|
+
this.#stopReconnecting(false, true);
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
this.#socket = new WebSocket(this.#socketUrl, this.#socketConfig);
|
|
1299
|
+
this.#socket.once("open", () => {
|
|
1300
|
+
this.emit("connect", this.#reconnectAttempts, this.name);
|
|
1301
|
+
});
|
|
1302
|
+
this.#socket.on("message", (data) => {
|
|
1303
|
+
void this.#onMessage(data.toString("utf8"));
|
|
1304
|
+
});
|
|
1305
|
+
this.#socket.on("error", (err) => {
|
|
1306
|
+
this.emit("error", this.#error(err), this.name);
|
|
1307
|
+
});
|
|
1308
|
+
this.#socket.on("close", (code, reason) => {
|
|
1309
|
+
this.#onClose(code, reason.toString("utf8"));
|
|
1310
|
+
});
|
|
1311
|
+
this.#socket.on("pong", () => {
|
|
1312
|
+
if (this.#lastPingTime === null) {
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
this.#ping = Math.max(0, Date.now() - this.#lastPingTime);
|
|
1316
|
+
});
|
|
1317
|
+
let resolve;
|
|
1318
|
+
let reject;
|
|
1319
|
+
const promise = new Promise((res, rej) => {
|
|
1320
|
+
resolve = res;
|
|
1321
|
+
reject = rej;
|
|
1322
|
+
});
|
|
1323
|
+
const resolver = { promise, resolve, reject };
|
|
1324
|
+
this.#connectPromise = resolver.promise;
|
|
1325
|
+
const controller = new AbortController();
|
|
1326
|
+
try {
|
|
1327
|
+
await Promise.race([
|
|
1328
|
+
once(this.#socket, "open", { signal: controller.signal }),
|
|
1329
|
+
once(this.#socket, "close", { signal: controller.signal })
|
|
1330
|
+
]);
|
|
1331
|
+
} catch {
|
|
1332
|
+
this.#cleanup();
|
|
1333
|
+
} finally {
|
|
1334
|
+
controller.abort();
|
|
1335
|
+
const connected = this.connected;
|
|
1336
|
+
resolver.resolve(connected);
|
|
1337
|
+
this.#connectPromise = null;
|
|
1338
|
+
}
|
|
1339
|
+
return this.connected;
|
|
1340
|
+
}
|
|
1341
|
+
/**
|
|
1342
|
+
* Disconnect from the Lavalink node
|
|
1343
|
+
* @param code - WebSocket close code
|
|
1344
|
+
* @param reason - Disconnect reason
|
|
1345
|
+
*/
|
|
1346
|
+
async disconnect(code = 1e3 /* Normal */, reason = "disconnected") {
|
|
1347
|
+
if (this.#disconnectPromise !== null) {
|
|
1348
|
+
return this.#disconnectPromise;
|
|
1349
|
+
}
|
|
1350
|
+
this.#stopReconnecting();
|
|
1351
|
+
if (this.#socket === null) {
|
|
1352
|
+
return;
|
|
1353
|
+
}
|
|
1354
|
+
if (this.connecting) {
|
|
1355
|
+
this.#manualDisconnect = true;
|
|
1356
|
+
this.#socket.terminate();
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
if (!this.connected) {
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1362
|
+
this.#manualDisconnect = true;
|
|
1363
|
+
this.#disconnectPromise = once(this.#socket, "close").then(
|
|
1364
|
+
() => {
|
|
1365
|
+
},
|
|
1366
|
+
() => {
|
|
1367
|
+
}
|
|
1368
|
+
);
|
|
1369
|
+
this.#socket.close(code, reason);
|
|
1370
|
+
await this.#disconnectPromise;
|
|
1371
|
+
this.#disconnectPromise = null;
|
|
1372
|
+
}
|
|
1373
|
+
async #onMessage(data) {
|
|
1374
|
+
const payload = this.#parseMessageData(data);
|
|
1375
|
+
if (payload === null) {
|
|
1376
|
+
return this.disconnect(1003 /* UnsupportedData */, "expected json payload");
|
|
1377
|
+
}
|
|
1378
|
+
if (payload.op === "stats" /* Stats */) {
|
|
1379
|
+
this.#stats = payload;
|
|
1380
|
+
this.#keepAliveAndPing();
|
|
1381
|
+
} else if (payload.op === "ready" /* Ready */) {
|
|
1382
|
+
this.#stopReconnecting();
|
|
1383
|
+
this.#socketConfig.headers["Session-Id"] = payload.sessionId;
|
|
1384
|
+
this.emit("ready", payload.resumed, payload.sessionId, this.name);
|
|
1385
|
+
}
|
|
1386
|
+
this.emit("dispatch", payload, this.name);
|
|
1387
|
+
}
|
|
1388
|
+
#onClose(code, reason) {
|
|
1389
|
+
this.#cleanup();
|
|
1390
|
+
const shouldStop = this.#manualDisconnect || this.#reconnectLimit >= 0 && this.#reconnectAttempts >= this.#reconnectLimit;
|
|
1391
|
+
if (shouldStop) {
|
|
1392
|
+
this.#stopReconnecting();
|
|
1393
|
+
delete this.#socketConfig.headers["Session-Id"];
|
|
1394
|
+
const byLocal = this.#manualDisconnect;
|
|
1395
|
+
this.#manualDisconnect = false;
|
|
1396
|
+
this.emit("disconnect", code, reason, byLocal, this.name);
|
|
1397
|
+
return;
|
|
1398
|
+
}
|
|
1399
|
+
if (this.#reconnectCycle) {
|
|
1400
|
+
this.#reconnect();
|
|
1401
|
+
this.emit("close", code, reason, this.name);
|
|
1402
|
+
return;
|
|
1403
|
+
}
|
|
1404
|
+
setTimeout$1(() => {
|
|
1405
|
+
this.#reconnectCycle = true;
|
|
1406
|
+
void this.connect();
|
|
1407
|
+
}, 0);
|
|
1408
|
+
}
|
|
1409
|
+
/**
|
|
1410
|
+
* Set SponsorBlock segments for a player
|
|
1411
|
+
*/
|
|
1412
|
+
async setSponsorBlock(player, segments) {
|
|
1413
|
+
return this.rest.setSponsorBlock(player.guildId, segments);
|
|
1414
|
+
}
|
|
1415
|
+
/**
|
|
1416
|
+
* Get current SponsorBlock segments for a player
|
|
1417
|
+
*/
|
|
1418
|
+
async getSponsorBlock(player) {
|
|
1419
|
+
return this.rest.getSponsorBlock(player.guildId);
|
|
1420
|
+
}
|
|
1421
|
+
/**
|
|
1422
|
+
* Delete SponsorBlock configuration for a player
|
|
1423
|
+
*/
|
|
1424
|
+
async deleteSponsorBlock(player) {
|
|
1425
|
+
return this.rest.deleteSponsorBlock(player.guildId);
|
|
1426
|
+
}
|
|
1427
|
+
toString() {
|
|
1428
|
+
return this.name;
|
|
1429
|
+
}
|
|
1430
|
+
};
|
|
1431
|
+
var NodeManager = class extends EventEmitter {
|
|
1432
|
+
#player;
|
|
1433
|
+
#nodes = /* @__PURE__ */ new Map();
|
|
1434
|
+
info = /* @__PURE__ */ new Map();
|
|
1435
|
+
// LavalinkInfo cache
|
|
1436
|
+
constructor(player) {
|
|
1437
|
+
super({ captureRejections: false });
|
|
1438
|
+
this.#player = player;
|
|
1439
|
+
const immutable = {
|
|
1440
|
+
writable: false,
|
|
1441
|
+
configurable: false
|
|
1442
|
+
};
|
|
1443
|
+
Object.defineProperties(this, {
|
|
1444
|
+
info: immutable
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1447
|
+
/**
|
|
1448
|
+
* Get a node by name
|
|
1449
|
+
*/
|
|
1450
|
+
get(name) {
|
|
1451
|
+
return this.#nodes.get(name);
|
|
1452
|
+
}
|
|
1453
|
+
/**
|
|
1454
|
+
* Check if a node exists
|
|
1455
|
+
*/
|
|
1456
|
+
has(name) {
|
|
1457
|
+
return this.#nodes.has(name);
|
|
1458
|
+
}
|
|
1459
|
+
/**
|
|
1460
|
+
* Get all nodes
|
|
1461
|
+
*/
|
|
1462
|
+
get all() {
|
|
1463
|
+
return Array.from(this.#nodes.values());
|
|
1464
|
+
}
|
|
1465
|
+
/**
|
|
1466
|
+
* Get number of nodes
|
|
1467
|
+
*/
|
|
1468
|
+
get size() {
|
|
1469
|
+
return this.#nodes.size;
|
|
1470
|
+
}
|
|
1471
|
+
/**
|
|
1472
|
+
* Create a new node
|
|
1473
|
+
*/
|
|
1474
|
+
create(options) {
|
|
1475
|
+
if (this.#nodes.has(options.name)) {
|
|
1476
|
+
throw new Error(`Node '${options.name}' already exists`);
|
|
1477
|
+
}
|
|
1478
|
+
const node = new LavalinkNode({
|
|
1479
|
+
...options,
|
|
1480
|
+
clientId: this.#player.clientId ?? ""
|
|
1481
|
+
});
|
|
1482
|
+
node.on("connect", (_attempts, _name) => {
|
|
1483
|
+
this.emit("connect", node);
|
|
1484
|
+
});
|
|
1485
|
+
node.on("ready", (resumed, sessionId, _name) => {
|
|
1486
|
+
this.emit("ready", node, resumed, sessionId);
|
|
1487
|
+
if (!this.info.has(node.name)) {
|
|
1488
|
+
this.fetchInfo(node.name).catch(() => {
|
|
1489
|
+
});
|
|
1490
|
+
}
|
|
1491
|
+
if (resumed) {
|
|
1492
|
+
void this.#handleResumed(node);
|
|
1493
|
+
}
|
|
1494
|
+
});
|
|
1495
|
+
node.on("disconnect", (code, reason, _byLocal, _name) => {
|
|
1496
|
+
this.emit("disconnect", node, { code, reason });
|
|
1497
|
+
});
|
|
1498
|
+
node.on("close", (_code, _reason, _name) => {
|
|
1499
|
+
this.emit("reconnecting", node);
|
|
1500
|
+
});
|
|
1501
|
+
node.on("error", (error, _name) => {
|
|
1502
|
+
this.emit("error", node, error);
|
|
1503
|
+
});
|
|
1504
|
+
node.on("dispatch", (payload, _name) => {
|
|
1505
|
+
this.emit("raw", node, payload);
|
|
1506
|
+
});
|
|
1507
|
+
this.#nodes.set(options.name, node);
|
|
1508
|
+
this.emit("create", node);
|
|
1509
|
+
return node;
|
|
1510
|
+
}
|
|
1511
|
+
/**
|
|
1512
|
+
* Delete a node
|
|
1513
|
+
*/
|
|
1514
|
+
async delete(name) {
|
|
1515
|
+
const node = this.#nodes.get(name);
|
|
1516
|
+
if (!node) {
|
|
1517
|
+
return false;
|
|
1518
|
+
}
|
|
1519
|
+
const activeQueues = this.#player.queues.all.filter((q) => q.node?.name === name);
|
|
1520
|
+
if (activeQueues.length > 0) {
|
|
1521
|
+
throw new Error(`Cannot delete node '${name}' with ${activeQueues.length} active queue(s)`);
|
|
1522
|
+
}
|
|
1523
|
+
await node.disconnect();
|
|
1524
|
+
this.#nodes.delete(name);
|
|
1525
|
+
this.info.delete(name);
|
|
1526
|
+
this.emit("destroy", node);
|
|
1527
|
+
return true;
|
|
1528
|
+
}
|
|
1529
|
+
/**
|
|
1530
|
+
* Connect all nodes
|
|
1531
|
+
*/
|
|
1532
|
+
async connect() {
|
|
1533
|
+
const promises = Array.from(this.#nodes.values()).map((node) => node.connect());
|
|
1534
|
+
await Promise.allSettled(promises);
|
|
1535
|
+
}
|
|
1536
|
+
/**
|
|
1537
|
+
* Disconnect all nodes
|
|
1538
|
+
*/
|
|
1539
|
+
async disconnect() {
|
|
1540
|
+
const promises = Array.from(this.#nodes.values()).map((node) => node.disconnect());
|
|
1541
|
+
await Promise.allSettled(promises);
|
|
1542
|
+
}
|
|
1543
|
+
/**
|
|
1544
|
+
* Get relevant nodes sorted by load and availability
|
|
1545
|
+
* Returns nodes that are ready and have lowest load
|
|
1546
|
+
*/
|
|
1547
|
+
relevant() {
|
|
1548
|
+
const readyNodes = Array.from(this.#nodes.values()).filter((node) => node.ready);
|
|
1549
|
+
if (readyNodes.length === 0) {
|
|
1550
|
+
return [];
|
|
1551
|
+
}
|
|
1552
|
+
return readyNodes.sort((a, b) => {
|
|
1553
|
+
const aStats = a.stats;
|
|
1554
|
+
const bStats = b.stats;
|
|
1555
|
+
if (!aStats) {
|
|
1556
|
+
return 1;
|
|
1557
|
+
}
|
|
1558
|
+
if (!bStats) {
|
|
1559
|
+
return -1;
|
|
1560
|
+
}
|
|
1561
|
+
const aLoad = aStats.playingPlayers / (aStats.players || 1);
|
|
1562
|
+
const bLoad = bStats.playingPlayers / (bStats.players || 1);
|
|
1563
|
+
if (aLoad !== bLoad) {
|
|
1564
|
+
return aLoad - bLoad;
|
|
1565
|
+
}
|
|
1566
|
+
const aCpu = aStats.cpu.lavalinkLoad;
|
|
1567
|
+
const bCpu = bStats.cpu.lavalinkLoad;
|
|
1568
|
+
if (aCpu !== bCpu) {
|
|
1569
|
+
return aCpu - bCpu;
|
|
1570
|
+
}
|
|
1571
|
+
const aMemory = aStats.memory.used / aStats.memory.allocated;
|
|
1572
|
+
const bMemory = bStats.memory.used / bStats.memory.allocated;
|
|
1573
|
+
return aMemory - bMemory;
|
|
1574
|
+
});
|
|
1575
|
+
}
|
|
1576
|
+
/**
|
|
1577
|
+
* Get node metrics
|
|
1578
|
+
*/
|
|
1579
|
+
get metrics() {
|
|
1580
|
+
const nodes = Array.from(this.#nodes.values());
|
|
1581
|
+
return {
|
|
1582
|
+
total: nodes.length,
|
|
1583
|
+
connected: nodes.filter((n) => n.connected).length,
|
|
1584
|
+
ready: nodes.filter((n) => n.ready).length,
|
|
1585
|
+
reconnecting: nodes.filter((n) => n.reconnecting).length,
|
|
1586
|
+
players: nodes.reduce((sum, n) => sum + (n.stats?.players ?? 0), 0),
|
|
1587
|
+
playingPlayers: nodes.reduce((sum, n) => sum + (n.stats?.playingPlayers ?? 0), 0)
|
|
1588
|
+
};
|
|
1589
|
+
}
|
|
1590
|
+
/**
|
|
1591
|
+
* Get all node names
|
|
1592
|
+
*/
|
|
1593
|
+
keys() {
|
|
1594
|
+
return this.#nodes.keys();
|
|
1595
|
+
}
|
|
1596
|
+
/**
|
|
1597
|
+
* Get all nodes
|
|
1598
|
+
*/
|
|
1599
|
+
values() {
|
|
1600
|
+
return this.#nodes.values();
|
|
1601
|
+
}
|
|
1602
|
+
/**
|
|
1603
|
+
* Get all entries
|
|
1604
|
+
*/
|
|
1605
|
+
entries() {
|
|
1606
|
+
return this.#nodes.entries();
|
|
1607
|
+
}
|
|
1608
|
+
/**
|
|
1609
|
+
* Fetch and cache node info
|
|
1610
|
+
* @param name Node name
|
|
1611
|
+
*/
|
|
1612
|
+
async fetchInfo(name) {
|
|
1613
|
+
if (this.info.has(name)) {
|
|
1614
|
+
return this.info.get(name);
|
|
1615
|
+
}
|
|
1616
|
+
const node = this.#nodes.get(name);
|
|
1617
|
+
if (!node) {
|
|
1618
|
+
throw new Error(`Node '${name}' not found`);
|
|
1619
|
+
}
|
|
1620
|
+
const info = await node.rest.fetchInfo();
|
|
1621
|
+
this.info.set(name, info);
|
|
1622
|
+
return info;
|
|
1623
|
+
}
|
|
1624
|
+
/**
|
|
1625
|
+
* Check if a feature is supported by a node
|
|
1626
|
+
* @param type Feature type (filter, source, plugin)
|
|
1627
|
+
* @param value Feature value (name)
|
|
1628
|
+
* @param nodeName Optional node name (checks all if not specified)
|
|
1629
|
+
*/
|
|
1630
|
+
supports(type, value, nodeName) {
|
|
1631
|
+
if (nodeName) {
|
|
1632
|
+
const node = this.#nodes.get(nodeName);
|
|
1633
|
+
if (!node) {
|
|
1634
|
+
return false;
|
|
1635
|
+
}
|
|
1636
|
+
return this.#checkNodeSupport(node.name, type, value);
|
|
1637
|
+
}
|
|
1638
|
+
return Array.from(this.#nodes.values()).some((node) => this.#checkNodeSupport(node.name, type, value));
|
|
1639
|
+
}
|
|
1640
|
+
/**
|
|
1641
|
+
* Check if a specific node supports a feature
|
|
1642
|
+
*/
|
|
1643
|
+
#checkNodeSupport(nodeName, type, value) {
|
|
1644
|
+
const info = this.info.get(nodeName);
|
|
1645
|
+
if (!info) {
|
|
1646
|
+
return false;
|
|
1647
|
+
}
|
|
1648
|
+
switch (type) {
|
|
1649
|
+
case "filter":
|
|
1650
|
+
return info.filters?.includes(value) ?? false;
|
|
1651
|
+
case "source":
|
|
1652
|
+
return info.sourceManagers?.includes(value) ?? false;
|
|
1653
|
+
case "plugin":
|
|
1654
|
+
return info.plugins?.some((p) => p.name === value) ?? false;
|
|
1655
|
+
default:
|
|
1656
|
+
return false;
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
/**
|
|
1660
|
+
* Handle node resumption
|
|
1661
|
+
* Sync all queues when a node resumes
|
|
1662
|
+
*/
|
|
1663
|
+
async #handleResumed(node) {
|
|
1664
|
+
try {
|
|
1665
|
+
if (this.#player.options.autoSync) {
|
|
1666
|
+
await this.#player.queues.syncAll();
|
|
1667
|
+
}
|
|
1668
|
+
} catch (error) {
|
|
1669
|
+
this.emit("error", node, error);
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
};
|
|
1673
|
+
|
|
1674
|
+
// src/voice/VoiceSession.ts
|
|
1675
|
+
var VoiceState = class {
|
|
1676
|
+
#changePromise = null;
|
|
1677
|
+
#node;
|
|
1678
|
+
#state;
|
|
1679
|
+
#player;
|
|
1680
|
+
guildId;
|
|
1681
|
+
player;
|
|
1682
|
+
constructor(player, node, guildId) {
|
|
1683
|
+
if (player.voices.has(guildId)) {
|
|
1684
|
+
throw new Error("An identical voice state already exists");
|
|
1685
|
+
}
|
|
1686
|
+
const _node = player.nodes.get(node);
|
|
1687
|
+
if (!_node) {
|
|
1688
|
+
throw new Error(`Node '${node}' not found`);
|
|
1689
|
+
}
|
|
1690
|
+
if (!_node.ready) {
|
|
1691
|
+
throw new Error(`Node '${node}' not ready`);
|
|
1692
|
+
}
|
|
1693
|
+
const state = player.voices[LookupSymbol](guildId);
|
|
1694
|
+
if (!state) {
|
|
1695
|
+
throw new Error(`No connection found for guild '${guildId}'`);
|
|
1696
|
+
}
|
|
1697
|
+
const _player = player.queues[LookupSymbol](guildId);
|
|
1698
|
+
if (!_player) {
|
|
1699
|
+
throw new Error(`No player found for guild '${guildId}'`);
|
|
1700
|
+
}
|
|
1701
|
+
this.#node = _node;
|
|
1702
|
+
this.#state = state;
|
|
1703
|
+
this.#player = _player;
|
|
1704
|
+
this.guildId = guildId;
|
|
1705
|
+
this.player = player;
|
|
1706
|
+
const immutable = {
|
|
1707
|
+
writable: false,
|
|
1708
|
+
configurable: false
|
|
1709
|
+
};
|
|
1710
|
+
Object.defineProperties(this, {
|
|
1711
|
+
guildId: immutable,
|
|
1712
|
+
player: { ...immutable, enumerable: false }
|
|
1713
|
+
});
|
|
1714
|
+
}
|
|
1715
|
+
/**
|
|
1716
|
+
* Current node for this voice connection
|
|
1717
|
+
*/
|
|
1718
|
+
get node() {
|
|
1719
|
+
return this.#node;
|
|
1720
|
+
}
|
|
1721
|
+
/**
|
|
1722
|
+
* Ping to the voice server in milliseconds
|
|
1723
|
+
*/
|
|
1724
|
+
get ping() {
|
|
1725
|
+
return this.#player.state.ping;
|
|
1726
|
+
}
|
|
1727
|
+
/**
|
|
1728
|
+
* Voice region ID (e.g., "us-west", "eu-central")
|
|
1729
|
+
*/
|
|
1730
|
+
get regionId() {
|
|
1731
|
+
return this.#state.region_id;
|
|
1732
|
+
}
|
|
1733
|
+
/**
|
|
1734
|
+
* Voice channel ID
|
|
1735
|
+
*/
|
|
1736
|
+
get channelId() {
|
|
1737
|
+
return this.#state.channel_id;
|
|
1738
|
+
}
|
|
1739
|
+
/**
|
|
1740
|
+
* Whether the bot is self-deafened
|
|
1741
|
+
*/
|
|
1742
|
+
get selfDeaf() {
|
|
1743
|
+
return this.#state.self_deaf;
|
|
1744
|
+
}
|
|
1745
|
+
/**
|
|
1746
|
+
* Whether the bot is self-muted
|
|
1747
|
+
*/
|
|
1748
|
+
get selfMute() {
|
|
1749
|
+
return this.#state.self_mute;
|
|
1750
|
+
}
|
|
1751
|
+
/**
|
|
1752
|
+
* Whether the bot is server-deafened
|
|
1753
|
+
*/
|
|
1754
|
+
get serverDeaf() {
|
|
1755
|
+
return this.#state.deaf;
|
|
1756
|
+
}
|
|
1757
|
+
/**
|
|
1758
|
+
* Whether the bot is server-muted
|
|
1759
|
+
*/
|
|
1760
|
+
get serverMute() {
|
|
1761
|
+
return this.#state.mute;
|
|
1762
|
+
}
|
|
1763
|
+
/**
|
|
1764
|
+
* Whether the bot is suppressed (priority speaker)
|
|
1765
|
+
*/
|
|
1766
|
+
get suppressed() {
|
|
1767
|
+
return this.#state.suppress;
|
|
1768
|
+
}
|
|
1769
|
+
/**
|
|
1770
|
+
* Whether this voice state has been destroyed
|
|
1771
|
+
*/
|
|
1772
|
+
get destroyed() {
|
|
1773
|
+
return this.player.voices.get(this.guildId) !== this;
|
|
1774
|
+
}
|
|
1775
|
+
/**
|
|
1776
|
+
* Whether the voice connection is fully connected
|
|
1777
|
+
*/
|
|
1778
|
+
get connected() {
|
|
1779
|
+
if (!this.#player.state.connected) {
|
|
1780
|
+
return false;
|
|
1781
|
+
}
|
|
1782
|
+
return this.#state.connected && this.#state.node_session_id === this.#node.sessionId;
|
|
1783
|
+
}
|
|
1784
|
+
/**
|
|
1785
|
+
* Whether the voice connection is reconnecting
|
|
1786
|
+
*/
|
|
1787
|
+
get reconnecting() {
|
|
1788
|
+
return this.#state.reconnecting;
|
|
1789
|
+
}
|
|
1790
|
+
/**
|
|
1791
|
+
* Whether the voice connection is disconnected
|
|
1792
|
+
*/
|
|
1793
|
+
get disconnected() {
|
|
1794
|
+
return !this.connected && !this.reconnecting;
|
|
1795
|
+
}
|
|
1796
|
+
/**
|
|
1797
|
+
* Whether the voice connection is changing nodes
|
|
1798
|
+
*/
|
|
1799
|
+
get changingNode() {
|
|
1800
|
+
return this.#changePromise !== null;
|
|
1801
|
+
}
|
|
1802
|
+
/**
|
|
1803
|
+
* Session ID for the voice connection
|
|
1804
|
+
*/
|
|
1805
|
+
get sessionId() {
|
|
1806
|
+
return this.#state.session_id;
|
|
1807
|
+
}
|
|
1808
|
+
/**
|
|
1809
|
+
* Voice server token
|
|
1810
|
+
*/
|
|
1811
|
+
get token() {
|
|
1812
|
+
return this.#state.token;
|
|
1813
|
+
}
|
|
1814
|
+
/**
|
|
1815
|
+
* Voice server endpoint
|
|
1816
|
+
*/
|
|
1817
|
+
get endpoint() {
|
|
1818
|
+
return this.#state.endpoint;
|
|
1819
|
+
}
|
|
1820
|
+
/**
|
|
1821
|
+
* Node session ID this voice state is connected to
|
|
1822
|
+
*/
|
|
1823
|
+
get nodeSessionId() {
|
|
1824
|
+
return this.#state.node_session_id;
|
|
1825
|
+
}
|
|
1826
|
+
/**
|
|
1827
|
+
* Destroy this voice connection
|
|
1828
|
+
* @param reason Optional reason for destruction
|
|
1829
|
+
*/
|
|
1830
|
+
async destroy(reason) {
|
|
1831
|
+
return this.player.voices.destroy(this.guildId, reason);
|
|
1832
|
+
}
|
|
1833
|
+
/**
|
|
1834
|
+
* Connect to a voice channel
|
|
1835
|
+
* @param channelId Voice channel ID (defaults to current channel)
|
|
1836
|
+
*/
|
|
1837
|
+
async connect(channelId = this.#state.channel_id) {
|
|
1838
|
+
await this.player.voices.connect(this.guildId, channelId);
|
|
1839
|
+
}
|
|
1840
|
+
/**
|
|
1841
|
+
* Disconnect from the voice channel
|
|
1842
|
+
*/
|
|
1843
|
+
async disconnect() {
|
|
1844
|
+
return this.player.voices.disconnect(this.guildId);
|
|
1845
|
+
}
|
|
1846
|
+
/**
|
|
1847
|
+
* Change to a different Lavalink node
|
|
1848
|
+
* Preserves playback state and filters
|
|
1849
|
+
* @param name Name of the node to change to
|
|
1850
|
+
*/
|
|
1851
|
+
async changeNode(name) {
|
|
1852
|
+
const node = this.player.nodes.get(name);
|
|
1853
|
+
if (!node) {
|
|
1854
|
+
throw new Error(`Node '${name}' not found`);
|
|
1855
|
+
}
|
|
1856
|
+
if (!node.ready) {
|
|
1857
|
+
throw new Error(`Node '${name}' not ready`);
|
|
1858
|
+
}
|
|
1859
|
+
if (this.#changePromise !== null) {
|
|
1860
|
+
return this.#changePromise;
|
|
1861
|
+
}
|
|
1862
|
+
if (name === this.#node.name) {
|
|
1863
|
+
throw new Error(`Already on node '${name}'`);
|
|
1864
|
+
}
|
|
1865
|
+
let resolve;
|
|
1866
|
+
let reject;
|
|
1867
|
+
const promise = new Promise((res, rej) => {
|
|
1868
|
+
resolve = res;
|
|
1869
|
+
reject = rej;
|
|
1870
|
+
});
|
|
1871
|
+
const resolver = { promise, resolve, reject };
|
|
1872
|
+
this.#changePromise = resolver.promise;
|
|
1873
|
+
const request = {
|
|
1874
|
+
voice: {
|
|
1875
|
+
channelId: this.#state.channel_id,
|
|
1876
|
+
endpoint: this.#state.endpoint,
|
|
1877
|
+
sessionId: this.#state.session_id,
|
|
1878
|
+
token: this.#state.token
|
|
1879
|
+
},
|
|
1880
|
+
filters: this.#player.filters,
|
|
1881
|
+
paused: this.#player.paused,
|
|
1882
|
+
volume: this.#player.volume
|
|
1883
|
+
};
|
|
1884
|
+
const track = this.#player.track;
|
|
1885
|
+
const wasPlaying = !this.#player.paused && track !== null;
|
|
1886
|
+
if (wasPlaying && this.player.nodes.supports("source", track.info.sourceName, node.name)) {
|
|
1887
|
+
request.track = { encoded: track.encoded, userData: track.userData };
|
|
1888
|
+
request.position = this.#player.state.position;
|
|
1889
|
+
}
|
|
1890
|
+
await this.#node.rest.destroyPlayer(this.guildId).catch(noop);
|
|
1891
|
+
const previousNode = this.#node;
|
|
1892
|
+
this.#node = node;
|
|
1893
|
+
try {
|
|
1894
|
+
const player = await node.rest.updatePlayer(this.guildId, request);
|
|
1895
|
+
this.#state.node_session_id = node.sessionId ?? "";
|
|
1896
|
+
Object.assign(this.#player, player);
|
|
1897
|
+
this.player.emit("voiceChange", this, previousNode, wasPlaying);
|
|
1898
|
+
resolver.resolve();
|
|
1899
|
+
} catch (err) {
|
|
1900
|
+
resolver.reject(err);
|
|
1901
|
+
throw err;
|
|
1902
|
+
} finally {
|
|
1903
|
+
this.#changePromise = null;
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
/**
|
|
1907
|
+
* String representation of the voice state
|
|
1908
|
+
*/
|
|
1909
|
+
toString() {
|
|
1910
|
+
return `VoiceState<${this.guildId}>`;
|
|
1911
|
+
}
|
|
1912
|
+
};
|
|
1913
|
+
|
|
1914
|
+
// src/voice/RegionSelector.ts
|
|
1915
|
+
var VoiceRegion = class {
|
|
1916
|
+
#pings = /* @__PURE__ */ new Map();
|
|
1917
|
+
id;
|
|
1918
|
+
player;
|
|
1919
|
+
constructor(player, regionId) {
|
|
1920
|
+
if (player.voices.regions.has(regionId)) {
|
|
1921
|
+
throw new Error("An identical voice region already exists");
|
|
1922
|
+
}
|
|
1923
|
+
this.id = regionId;
|
|
1924
|
+
this.player = player;
|
|
1925
|
+
const immutable = {
|
|
1926
|
+
writable: false,
|
|
1927
|
+
configurable: false
|
|
1928
|
+
};
|
|
1929
|
+
Object.defineProperties(this, {
|
|
1930
|
+
id: immutable,
|
|
1931
|
+
player: { ...immutable, enumerable: false }
|
|
1932
|
+
});
|
|
1933
|
+
}
|
|
1934
|
+
/**
|
|
1935
|
+
* Check if all ready nodes have ping data for this region
|
|
1936
|
+
* @returns `true` if all nodes are synced, `false` otherwise
|
|
1937
|
+
*/
|
|
1938
|
+
inSync() {
|
|
1939
|
+
return !Array.from(this.player.nodes.values()).some((n) => n.ready && !this.#pings.has(n.name));
|
|
1940
|
+
}
|
|
1941
|
+
/**
|
|
1942
|
+
* Remove ping data for a node
|
|
1943
|
+
* @param name Node name
|
|
1944
|
+
* @returns `true` if data was removed, `false` if it didn't exist
|
|
1945
|
+
*/
|
|
1946
|
+
forgetNode(name) {
|
|
1947
|
+
return this.#pings.delete(name);
|
|
1948
|
+
}
|
|
1949
|
+
/**
|
|
1950
|
+
* Get the average ping for a node in this region
|
|
1951
|
+
* @param name Node name
|
|
1952
|
+
* @returns Average ping in milliseconds, or `null` if no data
|
|
1953
|
+
*/
|
|
1954
|
+
getAveragePing(name) {
|
|
1955
|
+
const pings = this.#pings.get(name)?.history;
|
|
1956
|
+
if (!pings?.length) {
|
|
1957
|
+
return null;
|
|
1958
|
+
}
|
|
1959
|
+
return Math.round(pings.reduce((total, current) => total + current, 0) / pings.length);
|
|
1960
|
+
}
|
|
1961
|
+
/**
|
|
1962
|
+
* Get all nodes with their average pings
|
|
1963
|
+
* @returns Array of [nodeName, averagePing] tuples
|
|
1964
|
+
*/
|
|
1965
|
+
getAllPings() {
|
|
1966
|
+
return Array.from(this.player.nodes.values()).map((node) => [node.name, this.getAveragePing(node.name)]);
|
|
1967
|
+
}
|
|
1968
|
+
/**
|
|
1969
|
+
* Get the most relevant node for this region
|
|
1970
|
+
* Selects based on lowest average ping
|
|
1971
|
+
* @returns The node with lowest ping, or first relevant node if no ping data
|
|
1972
|
+
*/
|
|
1973
|
+
getRelevantNode() {
|
|
1974
|
+
return this.player.nodes.relevant().sort((a, b) => {
|
|
1975
|
+
const pingA = this.getAveragePing(a.name) ?? Number.MAX_SAFE_INTEGER;
|
|
1976
|
+
const pingB = this.getAveragePing(b.name) ?? Number.MAX_SAFE_INTEGER;
|
|
1977
|
+
return pingA - pingB;
|
|
1978
|
+
})[0];
|
|
1979
|
+
}
|
|
1980
|
+
/**
|
|
1981
|
+
* Internal method to update ping statistics
|
|
1982
|
+
* Called by the voice manager when player state updates
|
|
1983
|
+
* @param name Node name
|
|
1984
|
+
* @param state Player state with ping information
|
|
1985
|
+
* @internal
|
|
1986
|
+
*/
|
|
1987
|
+
[OnPingUpdateSymbol](name, state) {
|
|
1988
|
+
if (!state.connected) {
|
|
1989
|
+
return;
|
|
1990
|
+
}
|
|
1991
|
+
if (state.ping <= 0 || state.time <= 0) {
|
|
1992
|
+
return;
|
|
1993
|
+
}
|
|
1994
|
+
const pings = this.#pings.get(name);
|
|
1995
|
+
if (!pings) {
|
|
1996
|
+
this.#pings.set(name, {
|
|
1997
|
+
history: [state.ping],
|
|
1998
|
+
lastPingTime: state.time
|
|
1999
|
+
});
|
|
2000
|
+
return;
|
|
2001
|
+
}
|
|
2002
|
+
if (state.time - pings.lastPingTime < 12e3) {
|
|
2003
|
+
return;
|
|
2004
|
+
}
|
|
2005
|
+
pings.lastPingTime = state.time;
|
|
2006
|
+
pings.history.push(state.ping);
|
|
2007
|
+
if (pings.history.length > 5) {
|
|
2008
|
+
pings.history.shift();
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
/**
|
|
2012
|
+
* Clear all ping data for this region
|
|
2013
|
+
*/
|
|
2014
|
+
clear() {
|
|
2015
|
+
this.#pings.clear();
|
|
2016
|
+
}
|
|
2017
|
+
/**
|
|
2018
|
+
* Get the number of nodes with ping data
|
|
2019
|
+
*/
|
|
2020
|
+
get nodeCount() {
|
|
2021
|
+
return this.#pings.size;
|
|
2022
|
+
}
|
|
2023
|
+
/**
|
|
2024
|
+
* String representation of the voice region
|
|
2025
|
+
*/
|
|
2026
|
+
toString() {
|
|
2027
|
+
return `VoiceRegion<${this.id}>`;
|
|
2028
|
+
}
|
|
2029
|
+
/**
|
|
2030
|
+
* JSON representation of the voice region
|
|
2031
|
+
*/
|
|
2032
|
+
toJSON() {
|
|
2033
|
+
return {
|
|
2034
|
+
id: this.id,
|
|
2035
|
+
inSync: this.inSync(),
|
|
2036
|
+
nodeCount: this.nodeCount,
|
|
2037
|
+
pings: Array.from(this.#pings.entries()).map(([node, stats]) => ({
|
|
2038
|
+
node,
|
|
2039
|
+
averagePing: this.getAveragePing(node),
|
|
2040
|
+
samples: stats.history.length
|
|
2041
|
+
}))
|
|
2042
|
+
};
|
|
2043
|
+
}
|
|
2044
|
+
};
|
|
2045
|
+
|
|
2046
|
+
// src/voice/VoiceConnection.ts
|
|
2047
|
+
var VoiceManager = class {
|
|
2048
|
+
#cache = /* @__PURE__ */ new Map();
|
|
2049
|
+
#voices = /* @__PURE__ */ new Map();
|
|
2050
|
+
#joins = /* @__PURE__ */ new Map();
|
|
2051
|
+
#destroys = /* @__PURE__ */ new Map();
|
|
2052
|
+
regions = /* @__PURE__ */ new Map();
|
|
2053
|
+
player;
|
|
2054
|
+
constructor(player) {
|
|
2055
|
+
if (player.voices === void 0) {
|
|
2056
|
+
this.player = player;
|
|
2057
|
+
} else {
|
|
2058
|
+
throw new Error("Manager already exists for this Player");
|
|
2059
|
+
}
|
|
2060
|
+
const immutable = {
|
|
2061
|
+
writable: false,
|
|
2062
|
+
configurable: false
|
|
2063
|
+
};
|
|
2064
|
+
Object.defineProperties(this, {
|
|
2065
|
+
regions: immutable,
|
|
2066
|
+
player: { ...immutable, enumerable: false }
|
|
2067
|
+
});
|
|
2068
|
+
}
|
|
2069
|
+
/**
|
|
2070
|
+
* Number of voice connections
|
|
2071
|
+
*/
|
|
2072
|
+
get size() {
|
|
2073
|
+
return this.#voices.size;
|
|
2074
|
+
}
|
|
2075
|
+
/**
|
|
2076
|
+
* Get voice state for a guild
|
|
2077
|
+
*/
|
|
2078
|
+
get(guildId) {
|
|
2079
|
+
return this.#voices.get(guildId);
|
|
2080
|
+
}
|
|
2081
|
+
/**
|
|
2082
|
+
* Check if voice state exists
|
|
2083
|
+
*/
|
|
2084
|
+
has(guildId) {
|
|
2085
|
+
return this.#voices.has(guildId);
|
|
2086
|
+
}
|
|
2087
|
+
/**
|
|
2088
|
+
* Get all connected guild IDs
|
|
2089
|
+
*/
|
|
2090
|
+
keys() {
|
|
2091
|
+
return this.#voices.keys();
|
|
2092
|
+
}
|
|
2093
|
+
/**
|
|
2094
|
+
* Get all voice connections
|
|
2095
|
+
*/
|
|
2096
|
+
values() {
|
|
2097
|
+
return this.#voices.values();
|
|
2098
|
+
}
|
|
2099
|
+
/**
|
|
2100
|
+
* Get all voice connections as entries
|
|
2101
|
+
*/
|
|
2102
|
+
entries() {
|
|
2103
|
+
return this.#voices.entries();
|
|
2104
|
+
}
|
|
2105
|
+
/**
|
|
2106
|
+
* Destroy a voice connection
|
|
2107
|
+
* @param guildId Guild ID
|
|
2108
|
+
* @param reason Reason for destruction
|
|
2109
|
+
*/
|
|
2110
|
+
async destroy(guildId, reason = "destroyed") {
|
|
2111
|
+
if (this.player.queues.has(guildId)) {
|
|
2112
|
+
return this.player.queues.destroy(guildId, reason);
|
|
2113
|
+
}
|
|
2114
|
+
if (this.#destroys.has(guildId)) {
|
|
2115
|
+
return this.#destroys.get(guildId) ?? Promise.resolve();
|
|
2116
|
+
}
|
|
2117
|
+
const voice = this.#voices.get(guildId);
|
|
2118
|
+
if (!voice) {
|
|
2119
|
+
return;
|
|
2120
|
+
}
|
|
2121
|
+
let resolve;
|
|
2122
|
+
let reject;
|
|
2123
|
+
const promise = new Promise((res, rej) => {
|
|
2124
|
+
resolve = res;
|
|
2125
|
+
reject = rej;
|
|
2126
|
+
});
|
|
2127
|
+
const resolver = { promise, resolve, reject };
|
|
2128
|
+
this.#destroys.set(guildId, resolver.promise);
|
|
2129
|
+
if (this[LookupSymbol](guildId)?.connected) {
|
|
2130
|
+
await voice.disconnect().catch(noop);
|
|
2131
|
+
}
|
|
2132
|
+
this.#cache.delete(guildId);
|
|
2133
|
+
this.#voices.delete(guildId);
|
|
2134
|
+
this.player.emit("voiceDestroy", voice, reason);
|
|
2135
|
+
resolver.resolve();
|
|
2136
|
+
this.#destroys.delete(guildId);
|
|
2137
|
+
}
|
|
2138
|
+
/**
|
|
2139
|
+
* Connect to a voice channel
|
|
2140
|
+
* @param guildId Guild ID
|
|
2141
|
+
* @param voiceId Voice channel ID
|
|
2142
|
+
* @param options Connection options
|
|
2143
|
+
*/
|
|
2144
|
+
async connect(guildId, voiceId, options) {
|
|
2145
|
+
if (!isString(guildId, SnowflakeRegex)) {
|
|
2146
|
+
throw new Error("Guild Id is not a valid Discord Id");
|
|
2147
|
+
}
|
|
2148
|
+
if (!isString(voiceId, SnowflakeRegex)) {
|
|
2149
|
+
throw new Error("Voice Id is not a valid Discord Id");
|
|
2150
|
+
}
|
|
2151
|
+
const currentRequest = this.#joins.get(guildId);
|
|
2152
|
+
if (currentRequest) {
|
|
2153
|
+
if (currentRequest.voiceId === voiceId) {
|
|
2154
|
+
return currentRequest.promise;
|
|
2155
|
+
}
|
|
2156
|
+
currentRequest.reject(new Error("Connection request was replaced"));
|
|
2157
|
+
}
|
|
2158
|
+
this.#joins.delete(guildId);
|
|
2159
|
+
let resolve;
|
|
2160
|
+
let reject;
|
|
2161
|
+
const promise = new Promise((res, rej) => {
|
|
2162
|
+
resolve = res;
|
|
2163
|
+
reject = rej;
|
|
2164
|
+
});
|
|
2165
|
+
const request = {
|
|
2166
|
+
promise,
|
|
2167
|
+
resolve,
|
|
2168
|
+
reject,
|
|
2169
|
+
voiceId,
|
|
2170
|
+
node: options?.node,
|
|
2171
|
+
context: options?.context,
|
|
2172
|
+
config: options ? { filters: options.filters, volume: options.volume } : void 0
|
|
2173
|
+
};
|
|
2174
|
+
this.#joins.set(guildId, request);
|
|
2175
|
+
await this.player.options.forwardVoiceUpdate(guildId, {
|
|
2176
|
+
op: 4,
|
|
2177
|
+
d: {
|
|
2178
|
+
guild_id: guildId,
|
|
2179
|
+
channel_id: voiceId,
|
|
2180
|
+
self_deaf: false,
|
|
2181
|
+
self_mute: false
|
|
2182
|
+
}
|
|
2183
|
+
});
|
|
2184
|
+
const timeout = setTimeout$2(15e3, void 0, { ref: false });
|
|
2185
|
+
const result = await Promise.race([request.promise, timeout]);
|
|
2186
|
+
if (result === void 0) {
|
|
2187
|
+
this.#joins.delete(guildId);
|
|
2188
|
+
throw new Error("Voice connection timed out");
|
|
2189
|
+
}
|
|
2190
|
+
return result;
|
|
2191
|
+
}
|
|
2192
|
+
/**
|
|
2193
|
+
* Disconnect from voice channel
|
|
2194
|
+
* @param guildId Guild ID
|
|
2195
|
+
*/
|
|
2196
|
+
async disconnect(guildId) {
|
|
2197
|
+
const state = this.#cache.get(guildId);
|
|
2198
|
+
if (!state) {
|
|
2199
|
+
return;
|
|
2200
|
+
}
|
|
2201
|
+
await this.player.options.forwardVoiceUpdate(guildId, {
|
|
2202
|
+
op: 4,
|
|
2203
|
+
d: {
|
|
2204
|
+
guild_id: guildId,
|
|
2205
|
+
channel_id: null,
|
|
2206
|
+
self_deaf: false,
|
|
2207
|
+
self_mute: false
|
|
2208
|
+
}
|
|
2209
|
+
});
|
|
2210
|
+
state.channel_id = "";
|
|
2211
|
+
state.connected = false;
|
|
2212
|
+
}
|
|
2213
|
+
/**
|
|
2214
|
+
* Handle Discord dispatch events
|
|
2215
|
+
* @param payload Discord gateway payload
|
|
2216
|
+
*/
|
|
2217
|
+
handleDispatch(payload) {
|
|
2218
|
+
if (payload.t === "READY") {
|
|
2219
|
+
this.#handleReady(payload);
|
|
2220
|
+
} else if (payload.t === "VOICE_STATE_UPDATE") {
|
|
2221
|
+
this.#handleVoiceStateUpdate(payload);
|
|
2222
|
+
} else if (payload.t === "VOICE_SERVER_UPDATE") {
|
|
2223
|
+
this.#handleVoiceServerUpdate(payload);
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
/**
|
|
2227
|
+
* Internal lookup for voice state
|
|
2228
|
+
* @internal
|
|
2229
|
+
*/
|
|
2230
|
+
[LookupSymbol](guildId) {
|
|
2231
|
+
return this.#cache.get(guildId);
|
|
2232
|
+
}
|
|
2233
|
+
/**
|
|
2234
|
+
* Internal update for voice state
|
|
2235
|
+
* @internal
|
|
2236
|
+
*/
|
|
2237
|
+
[UpdateSymbol](guildId, partial) {
|
|
2238
|
+
const state = this.#cache.get(guildId);
|
|
2239
|
+
if (state) {
|
|
2240
|
+
Object.assign(state, partial);
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
/**
|
|
2244
|
+
* Internal voice close handler
|
|
2245
|
+
* @internal
|
|
2246
|
+
*/
|
|
2247
|
+
[OnVoiceCloseSymbol](guildId, code, reason, byRemote) {
|
|
2248
|
+
const voice = this.#voices.get(guildId);
|
|
2249
|
+
if (!voice) {
|
|
2250
|
+
return;
|
|
2251
|
+
}
|
|
2252
|
+
this.player.emit("voiceClose", voice, code, reason, byRemote);
|
|
2253
|
+
const shouldDestroy = code === 4014 /* Disconnected */ || code === 4021 /* DisconnectedRateLimited */ || code === 4022 /* DisconnectedCallTerminated */;
|
|
2254
|
+
if (shouldDestroy) {
|
|
2255
|
+
this.destroy(guildId, `Voice closed: ${reason} (${code})`).catch(noop);
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
/**
|
|
2259
|
+
* Handle READY event
|
|
2260
|
+
*/
|
|
2261
|
+
#handleReady(_payload) {
|
|
2262
|
+
this.#cache.clear();
|
|
2263
|
+
this.#voices.clear();
|
|
2264
|
+
this.#joins.clear();
|
|
2265
|
+
}
|
|
2266
|
+
/**
|
|
2267
|
+
* Handle VOICE_STATE_UPDATE event
|
|
2268
|
+
*/
|
|
2269
|
+
#handleVoiceStateUpdate(payload) {
|
|
2270
|
+
const { d: data } = payload;
|
|
2271
|
+
const guildId = data.guild_id;
|
|
2272
|
+
if (!guildId || data.user_id !== this.player.clientId) {
|
|
2273
|
+
return;
|
|
2274
|
+
}
|
|
2275
|
+
let state = this.#cache.get(guildId);
|
|
2276
|
+
if (data.channel_id === null) {
|
|
2277
|
+
if (state) {
|
|
2278
|
+
state.channel_id = "";
|
|
2279
|
+
state.connected = false;
|
|
2280
|
+
}
|
|
2281
|
+
return;
|
|
2282
|
+
}
|
|
2283
|
+
if (!state) {
|
|
2284
|
+
state = {
|
|
2285
|
+
channel_id: data.channel_id,
|
|
2286
|
+
session_id: data.session_id,
|
|
2287
|
+
deaf: data.deaf,
|
|
2288
|
+
mute: data.mute,
|
|
2289
|
+
self_deaf: data.self_deaf,
|
|
2290
|
+
self_mute: data.self_mute,
|
|
2291
|
+
suppress: data.suppress,
|
|
2292
|
+
token: "",
|
|
2293
|
+
endpoint: "",
|
|
2294
|
+
connected: false,
|
|
2295
|
+
node_session_id: "",
|
|
2296
|
+
reconnecting: false,
|
|
2297
|
+
region_id: ""
|
|
2298
|
+
};
|
|
2299
|
+
this.#cache.set(guildId, state);
|
|
2300
|
+
} else {
|
|
2301
|
+
state.channel_id = data.channel_id;
|
|
2302
|
+
state.session_id = data.session_id;
|
|
2303
|
+
state.deaf = data.deaf;
|
|
2304
|
+
state.mute = data.mute;
|
|
2305
|
+
state.self_deaf = data.self_deaf;
|
|
2306
|
+
state.self_mute = data.self_mute;
|
|
2307
|
+
state.suppress = data.suppress;
|
|
2308
|
+
}
|
|
2309
|
+
void this.#tryConnect(guildId);
|
|
2310
|
+
}
|
|
2311
|
+
/**
|
|
2312
|
+
* Handle VOICE_SERVER_UPDATE event
|
|
2313
|
+
*/
|
|
2314
|
+
#handleVoiceServerUpdate(payload) {
|
|
2315
|
+
const { d: data } = payload;
|
|
2316
|
+
const guildId = data.guild_id;
|
|
2317
|
+
let state = this.#cache.get(guildId);
|
|
2318
|
+
if (!state) {
|
|
2319
|
+
state = {
|
|
2320
|
+
channel_id: "",
|
|
2321
|
+
session_id: "",
|
|
2322
|
+
deaf: false,
|
|
2323
|
+
mute: false,
|
|
2324
|
+
self_deaf: false,
|
|
2325
|
+
self_mute: false,
|
|
2326
|
+
suppress: false,
|
|
2327
|
+
token: data.token,
|
|
2328
|
+
endpoint: data.endpoint ?? "",
|
|
2329
|
+
connected: false,
|
|
2330
|
+
node_session_id: "",
|
|
2331
|
+
reconnecting: false,
|
|
2332
|
+
region_id: ""
|
|
2333
|
+
};
|
|
2334
|
+
this.#cache.set(guildId, state);
|
|
2335
|
+
} else {
|
|
2336
|
+
state.token = data.token;
|
|
2337
|
+
state.endpoint = data.endpoint ?? "";
|
|
2338
|
+
}
|
|
2339
|
+
if (state.endpoint) {
|
|
2340
|
+
const match = state.endpoint.match(VoiceRegionIdRegex);
|
|
2341
|
+
if (match?.[1]) {
|
|
2342
|
+
state.region_id = match[1];
|
|
2343
|
+
if (!this.regions.has(state.region_id)) {
|
|
2344
|
+
this.regions.set(state.region_id, new VoiceRegion(this.player, state.region_id));
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
void this.#tryConnect(guildId);
|
|
2349
|
+
}
|
|
2350
|
+
/**
|
|
2351
|
+
* Try to establish voice connection
|
|
2352
|
+
*/
|
|
2353
|
+
async #tryConnect(guildId) {
|
|
2354
|
+
const state = this.#cache.get(guildId);
|
|
2355
|
+
const request = this.#joins.get(guildId);
|
|
2356
|
+
if (!state || !request) {
|
|
2357
|
+
return;
|
|
2358
|
+
}
|
|
2359
|
+
if (!state.channel_id || !state.session_id || !state.token || !state.endpoint) {
|
|
2360
|
+
return;
|
|
2361
|
+
}
|
|
2362
|
+
this.#joins.delete(guildId);
|
|
2363
|
+
try {
|
|
2364
|
+
const nodeName = request.node ?? (state.region_id && this.regions.get(state.region_id)?.getRelevantNode()?.name) ?? this.player.nodes.relevant()[0]?.name;
|
|
2365
|
+
if (!nodeName) {
|
|
2366
|
+
throw new Error("No nodes available");
|
|
2367
|
+
}
|
|
2368
|
+
let queue = this.player.queues.get(guildId);
|
|
2369
|
+
if (!queue) {
|
|
2370
|
+
queue = await this.player.queues.create({
|
|
2371
|
+
guildId,
|
|
2372
|
+
voiceId: state.channel_id,
|
|
2373
|
+
node: nodeName,
|
|
2374
|
+
context: request.context,
|
|
2375
|
+
...request.config
|
|
2376
|
+
});
|
|
2377
|
+
}
|
|
2378
|
+
const voice = new VoiceState(this.player, nodeName, guildId);
|
|
2379
|
+
this.#voices.set(guildId, voice);
|
|
2380
|
+
state.connected = true;
|
|
2381
|
+
state.node_session_id = voice.node.sessionId ?? "";
|
|
2382
|
+
this.player.emit("voiceConnect", voice);
|
|
2383
|
+
request.resolve(voice);
|
|
2384
|
+
} catch (error) {
|
|
2385
|
+
request.reject(error instanceof Error ? error : new Error(String(error)));
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
};
|
|
2389
|
+
|
|
2390
|
+
// src/audio/AudioTrack.ts
|
|
2391
|
+
var Track = class _Track {
|
|
2392
|
+
/**
|
|
2393
|
+
* Unique identifier of the track
|
|
2394
|
+
*/
|
|
2395
|
+
id;
|
|
2396
|
+
/**
|
|
2397
|
+
* Title of the track
|
|
2398
|
+
*/
|
|
2399
|
+
title = "Unknown Track";
|
|
2400
|
+
/**
|
|
2401
|
+
* Author/artist of the track
|
|
2402
|
+
*/
|
|
2403
|
+
author = "Unknown Author";
|
|
2404
|
+
/**
|
|
2405
|
+
* Whether the track is a live stream
|
|
2406
|
+
*/
|
|
2407
|
+
isLive = false;
|
|
2408
|
+
/**
|
|
2409
|
+
* Whether the track is seekable
|
|
2410
|
+
*/
|
|
2411
|
+
isSeekable = false;
|
|
2412
|
+
/**
|
|
2413
|
+
* Duration of the track in milliseconds
|
|
2414
|
+
*/
|
|
2415
|
+
duration = 0;
|
|
2416
|
+
/**
|
|
2417
|
+
* Formatted duration string (hh:mm:ss or mm:ss)
|
|
2418
|
+
*/
|
|
2419
|
+
formattedDuration = "00:00";
|
|
2420
|
+
/**
|
|
2421
|
+
* Uniform Resource Identifier of the track
|
|
2422
|
+
*/
|
|
2423
|
+
uri = null;
|
|
2424
|
+
/**
|
|
2425
|
+
* International Standard Recording Code
|
|
2426
|
+
*/
|
|
2427
|
+
isrc = null;
|
|
2428
|
+
/**
|
|
2429
|
+
* URL of the track (validated URI)
|
|
2430
|
+
*/
|
|
2431
|
+
url = null;
|
|
2432
|
+
/**
|
|
2433
|
+
* Artwork/thumbnail URL
|
|
2434
|
+
*/
|
|
2435
|
+
artworkUrl = null;
|
|
2436
|
+
/**
|
|
2437
|
+
* Custom user data attached to the track
|
|
2438
|
+
*/
|
|
2439
|
+
userData = {};
|
|
2440
|
+
/**
|
|
2441
|
+
* Additional info from plugins
|
|
2442
|
+
*/
|
|
2443
|
+
pluginInfo = {};
|
|
2444
|
+
/**
|
|
2445
|
+
* Encoded string representation (Lavalink format)
|
|
2446
|
+
*/
|
|
2447
|
+
encoded;
|
|
2448
|
+
/**
|
|
2449
|
+
* Source name (youtube, spotify, soundcloud, etc.)
|
|
2450
|
+
*/
|
|
2451
|
+
sourceName = "unknown";
|
|
2452
|
+
get identifier() {
|
|
2453
|
+
return this.id;
|
|
2454
|
+
}
|
|
2455
|
+
get stream() {
|
|
2456
|
+
return this.isLive;
|
|
2457
|
+
}
|
|
2458
|
+
get seekable() {
|
|
2459
|
+
return this.isSeekable;
|
|
2460
|
+
}
|
|
2461
|
+
get durationFormatted() {
|
|
2462
|
+
return this.formattedDuration;
|
|
2463
|
+
}
|
|
2464
|
+
get source() {
|
|
2465
|
+
return this.sourceName;
|
|
2466
|
+
}
|
|
2467
|
+
get thumbnail() {
|
|
2468
|
+
return this.artworkUrl;
|
|
2469
|
+
}
|
|
2470
|
+
get info() {
|
|
2471
|
+
return {
|
|
2472
|
+
identifier: this.id,
|
|
2473
|
+
position: 0,
|
|
2474
|
+
title: this.title,
|
|
2475
|
+
author: this.author,
|
|
2476
|
+
length: this.duration,
|
|
2477
|
+
isStream: this.isLive,
|
|
2478
|
+
isSeekable: this.isSeekable,
|
|
2479
|
+
uri: this.uri,
|
|
2480
|
+
isrc: this.isrc,
|
|
2481
|
+
artworkUrl: this.artworkUrl,
|
|
2482
|
+
sourceName: this.sourceName
|
|
2483
|
+
};
|
|
2484
|
+
}
|
|
2485
|
+
constructor(data) {
|
|
2486
|
+
if (!isRecord(data)) {
|
|
2487
|
+
throw new Error("Track data must be an object");
|
|
2488
|
+
}
|
|
2489
|
+
if (!isRecord(data.info)) {
|
|
2490
|
+
throw new Error("Track info is not an object");
|
|
2491
|
+
}
|
|
2492
|
+
if (isString(data.info.identifier, "non-empty")) {
|
|
2493
|
+
this.id = data.info.identifier;
|
|
2494
|
+
} else {
|
|
2495
|
+
throw new Error("Track does not have an identifier");
|
|
2496
|
+
}
|
|
2497
|
+
if (isString(data.encoded, "non-empty")) {
|
|
2498
|
+
this.encoded = data.encoded;
|
|
2499
|
+
} else {
|
|
2500
|
+
throw new Error("Track does not have an encoded data string");
|
|
2501
|
+
}
|
|
2502
|
+
if (isString(data.info.title, "non-empty")) {
|
|
2503
|
+
this.title = data.info.title;
|
|
2504
|
+
}
|
|
2505
|
+
if (isString(data.info.author, "non-empty")) {
|
|
2506
|
+
this.author = data.info.author;
|
|
2507
|
+
}
|
|
2508
|
+
if (data.info.isStream) {
|
|
2509
|
+
this.isLive = true;
|
|
2510
|
+
}
|
|
2511
|
+
if (data.info.isSeekable) {
|
|
2512
|
+
this.isSeekable = true;
|
|
2513
|
+
}
|
|
2514
|
+
if (this.isLive) {
|
|
2515
|
+
this.duration = Number.POSITIVE_INFINITY;
|
|
2516
|
+
this.formattedDuration = "Live";
|
|
2517
|
+
} else if (isNumber(data.info.length, "whole")) {
|
|
2518
|
+
this.duration = data.info.length;
|
|
2519
|
+
this.formattedDuration = formatDuration(this.duration);
|
|
2520
|
+
}
|
|
2521
|
+
if (isString(data.info.uri, "non-empty")) {
|
|
2522
|
+
this.uri = data.info.uri;
|
|
2523
|
+
}
|
|
2524
|
+
if (isString(data.info.isrc, "non-empty")) {
|
|
2525
|
+
this.isrc = data.info.isrc;
|
|
2526
|
+
}
|
|
2527
|
+
if (isString(this.uri, "url")) {
|
|
2528
|
+
this.url = this.uri;
|
|
2529
|
+
}
|
|
2530
|
+
if (isString(data.info.artworkUrl, "url")) {
|
|
2531
|
+
this.artworkUrl = data.info.artworkUrl;
|
|
2532
|
+
}
|
|
2533
|
+
if (isRecord(data.userData, "non-empty")) {
|
|
2534
|
+
this.userData = data.userData;
|
|
2535
|
+
}
|
|
2536
|
+
if (isRecord(data.pluginInfo, "non-empty")) {
|
|
2537
|
+
this.pluginInfo = data.pluginInfo;
|
|
2538
|
+
}
|
|
2539
|
+
if (isString(data.info.sourceName, "non-empty")) {
|
|
2540
|
+
this.sourceName = data.info.sourceName;
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
/**
|
|
2544
|
+
* String representation of the track
|
|
2545
|
+
*/
|
|
2546
|
+
toString() {
|
|
2547
|
+
return this.title;
|
|
2548
|
+
}
|
|
2549
|
+
/**
|
|
2550
|
+
* JSON representation of the track
|
|
2551
|
+
*/
|
|
2552
|
+
toJSON() {
|
|
2553
|
+
return {
|
|
2554
|
+
identifier: this.id,
|
|
2555
|
+
title: this.title,
|
|
2556
|
+
author: this.author,
|
|
2557
|
+
duration: this.duration,
|
|
2558
|
+
uri: this.uri,
|
|
2559
|
+
thumbnail: this.artworkUrl,
|
|
2560
|
+
source: this.sourceName,
|
|
2561
|
+
isSeekable: this.isSeekable,
|
|
2562
|
+
isStream: this.isLive,
|
|
2563
|
+
encoded: this.encoded,
|
|
2564
|
+
userData: this.userData,
|
|
2565
|
+
pluginInfo: this.pluginInfo
|
|
2566
|
+
};
|
|
2567
|
+
}
|
|
2568
|
+
/**
|
|
2569
|
+
* Create a clone of the track
|
|
2570
|
+
*/
|
|
2571
|
+
clone() {
|
|
2572
|
+
return new _Track({
|
|
2573
|
+
encoded: this.encoded,
|
|
2574
|
+
info: {
|
|
2575
|
+
...this.info,
|
|
2576
|
+
length: this.duration,
|
|
2577
|
+
isStream: this.isLive
|
|
2578
|
+
},
|
|
2579
|
+
userData: this.userData,
|
|
2580
|
+
pluginInfo: this.pluginInfo
|
|
2581
|
+
});
|
|
2582
|
+
}
|
|
2583
|
+
};
|
|
2584
|
+
|
|
2585
|
+
// src/audio/TrackCollection.ts
|
|
2586
|
+
var Playlist = class {
|
|
2587
|
+
/**
|
|
2588
|
+
* Name of the playlist
|
|
2589
|
+
*/
|
|
2590
|
+
name = "Unknown Playlist";
|
|
2591
|
+
/**
|
|
2592
|
+
* Index of the track that was selected (from URL)
|
|
2593
|
+
*/
|
|
2594
|
+
selectedTrack = -1;
|
|
2595
|
+
/**
|
|
2596
|
+
* List of tracks in the playlist
|
|
2597
|
+
*/
|
|
2598
|
+
tracks = [];
|
|
2599
|
+
/**
|
|
2600
|
+
* Additional info from plugins
|
|
2601
|
+
*/
|
|
2602
|
+
pluginInfo = {};
|
|
2603
|
+
/**
|
|
2604
|
+
* Total duration of all tracks in milliseconds
|
|
2605
|
+
*/
|
|
2606
|
+
duration = 0;
|
|
2607
|
+
/**
|
|
2608
|
+
* Formatted total duration string
|
|
2609
|
+
*/
|
|
2610
|
+
formattedDuration = "00:00";
|
|
2611
|
+
constructor(data) {
|
|
2612
|
+
if (!isRecord(data)) {
|
|
2613
|
+
throw new Error("Playlist data must be an object");
|
|
2614
|
+
}
|
|
2615
|
+
if (!isRecord(data.info)) {
|
|
2616
|
+
throw new Error("Playlist info is not an object");
|
|
2617
|
+
}
|
|
2618
|
+
if (!isArray(data.tracks)) {
|
|
2619
|
+
throw new Error("Playlist tracks must be an array");
|
|
2620
|
+
}
|
|
2621
|
+
for (let i = 0; i < data.tracks.length; i++) {
|
|
2622
|
+
const track = new Track(data.tracks[i]);
|
|
2623
|
+
if (!track.isLive) {
|
|
2624
|
+
this.duration += track.duration;
|
|
2625
|
+
}
|
|
2626
|
+
this.tracks.push(track);
|
|
2627
|
+
}
|
|
2628
|
+
if (isString(data.info.name, "non-empty")) {
|
|
2629
|
+
this.name = data.info.name;
|
|
2630
|
+
} else if (data.info.name === "") {
|
|
2631
|
+
this.name = "";
|
|
2632
|
+
}
|
|
2633
|
+
if (isNumber(data.info.selectedTrack, "whole")) {
|
|
2634
|
+
this.selectedTrack = data.info.selectedTrack;
|
|
2635
|
+
}
|
|
2636
|
+
if (isRecord(data.pluginInfo, "non-empty")) {
|
|
2637
|
+
this.pluginInfo = data.pluginInfo;
|
|
2638
|
+
}
|
|
2639
|
+
if (this.duration > 0) {
|
|
2640
|
+
this.formattedDuration = formatDuration(this.duration);
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
get selected() {
|
|
2644
|
+
if (this.selectedTrack < 0 || this.selectedTrack >= this.tracks.length) {
|
|
2645
|
+
return null;
|
|
2646
|
+
}
|
|
2647
|
+
return this.tracks[this.selectedTrack] ?? null;
|
|
2648
|
+
}
|
|
2649
|
+
/**
|
|
2650
|
+
* Get the selected track (if any)
|
|
2651
|
+
* @returns The selected track or undefined if none/invalid
|
|
2652
|
+
*/
|
|
2653
|
+
getSelectedTrack() {
|
|
2654
|
+
return this.selected ?? void 0;
|
|
2655
|
+
}
|
|
2656
|
+
/**
|
|
2657
|
+
* Get the number of tracks
|
|
2658
|
+
*/
|
|
2659
|
+
get length() {
|
|
2660
|
+
return this.tracks.length;
|
|
2661
|
+
}
|
|
2662
|
+
/**
|
|
2663
|
+
* Get the number of tracks (alias for length)
|
|
2664
|
+
*/
|
|
2665
|
+
get trackCount() {
|
|
2666
|
+
return this.tracks.length;
|
|
2667
|
+
}
|
|
2668
|
+
/**
|
|
2669
|
+
* Formatted total duration (alias for formattedDuration)
|
|
2670
|
+
*/
|
|
2671
|
+
get durationFormatted() {
|
|
2672
|
+
return this.formattedDuration;
|
|
2673
|
+
}
|
|
2674
|
+
/**
|
|
2675
|
+
* String representation of the playlist
|
|
2676
|
+
*/
|
|
2677
|
+
toString() {
|
|
2678
|
+
return this.name;
|
|
2679
|
+
}
|
|
2680
|
+
/**
|
|
2681
|
+
* JSON representation of the playlist
|
|
2682
|
+
*/
|
|
2683
|
+
toJSON() {
|
|
2684
|
+
return {
|
|
2685
|
+
name: this.name,
|
|
2686
|
+
selectedTrack: this.selectedTrack,
|
|
2687
|
+
tracks: this.tracks.map((t) => t.toJSON()),
|
|
2688
|
+
duration: this.duration,
|
|
2689
|
+
formattedDuration: this.formattedDuration,
|
|
2690
|
+
pluginInfo: this.pluginInfo
|
|
2691
|
+
};
|
|
2692
|
+
}
|
|
2693
|
+
};
|
|
2694
|
+
|
|
2695
|
+
// src/audio/AudioFilters.ts
|
|
2696
|
+
var FilterManager = class {
|
|
2697
|
+
#player;
|
|
2698
|
+
#guildId;
|
|
2699
|
+
constructor(player, guildId) {
|
|
2700
|
+
this.#player = player;
|
|
2701
|
+
this.#guildId = guildId;
|
|
2702
|
+
}
|
|
2703
|
+
/**
|
|
2704
|
+
* Get current filter data
|
|
2705
|
+
*/
|
|
2706
|
+
get data() {
|
|
2707
|
+
const queue = this.#player.queues?.get(this.#guildId);
|
|
2708
|
+
if (!queue) {
|
|
2709
|
+
return {};
|
|
2710
|
+
}
|
|
2711
|
+
return queue["#player"]?.filters || {};
|
|
2712
|
+
}
|
|
2713
|
+
/**
|
|
2714
|
+
* Get a specific filter
|
|
2715
|
+
*/
|
|
2716
|
+
get(key) {
|
|
2717
|
+
if (key === "pluginFilters") {
|
|
2718
|
+
return void 0;
|
|
2719
|
+
}
|
|
2720
|
+
return this.data[key];
|
|
2721
|
+
}
|
|
2722
|
+
/**
|
|
2723
|
+
* Set a filter
|
|
2724
|
+
*/
|
|
2725
|
+
async set(key, value) {
|
|
2726
|
+
const queue = this.#player.queues?.get(this.#guildId);
|
|
2727
|
+
if (!queue) {
|
|
2728
|
+
throw new Error("Queue not found");
|
|
2729
|
+
}
|
|
2730
|
+
const filters = { ...this.data, [key]: value };
|
|
2731
|
+
await queue["#update"]({ filters });
|
|
2732
|
+
}
|
|
2733
|
+
/**
|
|
2734
|
+
* Set volume
|
|
2735
|
+
*/
|
|
2736
|
+
async setVolume(volume) {
|
|
2737
|
+
const clamped = Math.max(0, Math.min(1, volume));
|
|
2738
|
+
await this.set("volume", clamped);
|
|
2739
|
+
}
|
|
2740
|
+
/**
|
|
2741
|
+
* Get volume
|
|
2742
|
+
*/
|
|
2743
|
+
get volume() {
|
|
2744
|
+
return this.get("volume") ?? 1;
|
|
2745
|
+
}
|
|
2746
|
+
/**
|
|
2747
|
+
* Set equalizer bands
|
|
2748
|
+
*/
|
|
2749
|
+
async setEqualizer(bands) {
|
|
2750
|
+
await this.set("equalizer", bands);
|
|
2751
|
+
}
|
|
2752
|
+
/**
|
|
2753
|
+
* Get equalizer bands
|
|
2754
|
+
*/
|
|
2755
|
+
get equalizer() {
|
|
2756
|
+
return this.get("equalizer") ?? [];
|
|
2757
|
+
}
|
|
2758
|
+
/**
|
|
2759
|
+
* Set karaoke filter
|
|
2760
|
+
*/
|
|
2761
|
+
async setKaraoke(karaoke) {
|
|
2762
|
+
await this.set("karaoke", karaoke);
|
|
2763
|
+
}
|
|
2764
|
+
/**
|
|
2765
|
+
* Get karaoke filter
|
|
2766
|
+
*/
|
|
2767
|
+
get karaoke() {
|
|
2768
|
+
return this.get("karaoke");
|
|
2769
|
+
}
|
|
2770
|
+
/**
|
|
2771
|
+
* Set timescale filter
|
|
2772
|
+
*/
|
|
2773
|
+
async setTimescale(timescale) {
|
|
2774
|
+
await this.set("timescale", timescale);
|
|
2775
|
+
}
|
|
2776
|
+
/**
|
|
2777
|
+
* Get timescale filter
|
|
2778
|
+
*/
|
|
2779
|
+
get timescale() {
|
|
2780
|
+
return this.get("timescale");
|
|
2781
|
+
}
|
|
2782
|
+
/**
|
|
2783
|
+
* Set tremolo filter
|
|
2784
|
+
*/
|
|
2785
|
+
async setTremolo(tremolo) {
|
|
2786
|
+
await this.set("tremolo", tremolo);
|
|
2787
|
+
}
|
|
2788
|
+
/**
|
|
2789
|
+
* Get tremolo filter
|
|
2790
|
+
*/
|
|
2791
|
+
get tremolo() {
|
|
2792
|
+
return this.get("tremolo");
|
|
2793
|
+
}
|
|
2794
|
+
/**
|
|
2795
|
+
* Set vibrato filter
|
|
2796
|
+
*/
|
|
2797
|
+
async setVibrato(vibrato) {
|
|
2798
|
+
await this.set("vibrato", vibrato);
|
|
2799
|
+
}
|
|
2800
|
+
/**
|
|
2801
|
+
* Get vibrato filter
|
|
2802
|
+
*/
|
|
2803
|
+
get vibrato() {
|
|
2804
|
+
return this.get("vibrato");
|
|
2805
|
+
}
|
|
2806
|
+
/**
|
|
2807
|
+
* Set rotation filter
|
|
2808
|
+
*/
|
|
2809
|
+
async setRotation(rotation) {
|
|
2810
|
+
await this.set("rotation", rotation);
|
|
2811
|
+
}
|
|
2812
|
+
/**
|
|
2813
|
+
* Get rotation filter
|
|
2814
|
+
*/
|
|
2815
|
+
get rotation() {
|
|
2816
|
+
return this.get("rotation");
|
|
2817
|
+
}
|
|
2818
|
+
/**
|
|
2819
|
+
* Clear specific equalizer filter
|
|
2820
|
+
*/
|
|
2821
|
+
async clearEqualizer() {
|
|
2822
|
+
await this.delete("equalizer");
|
|
2823
|
+
}
|
|
2824
|
+
/**
|
|
2825
|
+
* Clear specific karaoke filter
|
|
2826
|
+
*/
|
|
2827
|
+
async clearKaraoke() {
|
|
2828
|
+
await this.delete("karaoke");
|
|
2829
|
+
}
|
|
2830
|
+
/**
|
|
2831
|
+
* Clear specific timescale filter
|
|
2832
|
+
*/
|
|
2833
|
+
async clearTimescale() {
|
|
2834
|
+
await this.delete("timescale");
|
|
2835
|
+
}
|
|
2836
|
+
/**
|
|
2837
|
+
* Clear specific tremolo filter
|
|
2838
|
+
*/
|
|
2839
|
+
async clearTremolo() {
|
|
2840
|
+
await this.delete("tremolo");
|
|
2841
|
+
}
|
|
2842
|
+
/**
|
|
2843
|
+
* Clear specific vibrato filter
|
|
2844
|
+
*/
|
|
2845
|
+
async clearVibrato() {
|
|
2846
|
+
await this.delete("vibrato");
|
|
2847
|
+
}
|
|
2848
|
+
/**
|
|
2849
|
+
* Clear specific rotation filter
|
|
2850
|
+
*/
|
|
2851
|
+
async clearRotation() {
|
|
2852
|
+
await this.delete("rotation");
|
|
2853
|
+
}
|
|
2854
|
+
/**
|
|
2855
|
+
* Clear all filters
|
|
2856
|
+
*/
|
|
2857
|
+
async clearAll() {
|
|
2858
|
+
await this.clear();
|
|
2859
|
+
}
|
|
2860
|
+
/**
|
|
2861
|
+
* Check if a filter is set
|
|
2862
|
+
*/
|
|
2863
|
+
has(key) {
|
|
2864
|
+
const value = this.data[key];
|
|
2865
|
+
return value !== void 0 && value !== null;
|
|
2866
|
+
}
|
|
2867
|
+
/**
|
|
2868
|
+
* Delete a specific filter
|
|
2869
|
+
*/
|
|
2870
|
+
async delete(key) {
|
|
2871
|
+
if (!this.has(key)) {
|
|
2872
|
+
return false;
|
|
2873
|
+
}
|
|
2874
|
+
const queue = this.#player.queues?.get(this.#guildId);
|
|
2875
|
+
if (!queue) {
|
|
2876
|
+
throw new Error("Queue not found");
|
|
2877
|
+
}
|
|
2878
|
+
const filters = { ...this.data };
|
|
2879
|
+
delete filters[key];
|
|
2880
|
+
await queue["#update"]({ filters });
|
|
2881
|
+
return true;
|
|
2882
|
+
}
|
|
2883
|
+
/**
|
|
2884
|
+
* Clear filters
|
|
2885
|
+
* @param type - "native" to clear only native filters, "plugin" to clear only plugin filters, undefined to clear all
|
|
2886
|
+
*/
|
|
2887
|
+
async clear(type) {
|
|
2888
|
+
const queue = this.#player.queues?.get(this.#guildId);
|
|
2889
|
+
if (!queue) {
|
|
2890
|
+
throw new Error("Queue not found");
|
|
2891
|
+
}
|
|
2892
|
+
let filters = {};
|
|
2893
|
+
if (type === "native") {
|
|
2894
|
+
if (this.data.pluginFilters) {
|
|
2895
|
+
filters.pluginFilters = this.data.pluginFilters;
|
|
2896
|
+
}
|
|
2897
|
+
} else if (type === "plugin") {
|
|
2898
|
+
const { pluginFilters: _, ...nativeFilters } = this.data;
|
|
2899
|
+
filters = nativeFilters;
|
|
2900
|
+
}
|
|
2901
|
+
await queue["#update"]({ filters });
|
|
2902
|
+
}
|
|
2903
|
+
/**
|
|
2904
|
+
* Apply multiple filters at once
|
|
2905
|
+
*/
|
|
2906
|
+
async apply(filters) {
|
|
2907
|
+
const queue = this.#player.queues?.get(this.#guildId);
|
|
2908
|
+
if (!queue) {
|
|
2909
|
+
throw new Error("Queue not found");
|
|
2910
|
+
}
|
|
2911
|
+
const newFilters = { ...this.data, ...filters };
|
|
2912
|
+
await queue["#update"]({ filters: newFilters });
|
|
2913
|
+
}
|
|
2914
|
+
/**
|
|
2915
|
+
* Set an EQ preset by name
|
|
2916
|
+
* @param preset The preset name
|
|
2917
|
+
* @returns Promise that resolves when the preset is applied
|
|
2918
|
+
*/
|
|
2919
|
+
async setEQPreset(preset) {
|
|
2920
|
+
const bands = EQPresets[preset];
|
|
2921
|
+
if (!bands) {
|
|
2922
|
+
throw new Error(`Invalid EQ preset: ${preset}`);
|
|
2923
|
+
}
|
|
2924
|
+
await this.set("equalizer", bands);
|
|
2925
|
+
}
|
|
2926
|
+
/**
|
|
2927
|
+
* Get all available EQ preset names
|
|
2928
|
+
* @returns Array of preset names
|
|
2929
|
+
*/
|
|
2930
|
+
getEQPresetNames() {
|
|
2931
|
+
return Object.keys(EQPresets);
|
|
2932
|
+
}
|
|
2933
|
+
/**
|
|
2934
|
+
* Check if a preset name is valid
|
|
2935
|
+
* @param name The preset name to check
|
|
2936
|
+
* @returns True if the preset exists
|
|
2937
|
+
*/
|
|
2938
|
+
isValidEQPreset(name) {
|
|
2939
|
+
return name in EQPresets;
|
|
2940
|
+
}
|
|
2941
|
+
};
|
|
2942
|
+
|
|
2943
|
+
// src/audio/AudioQueue.ts
|
|
2944
|
+
var Queue = class {
|
|
2945
|
+
#player;
|
|
2946
|
+
#autoplay = false;
|
|
2947
|
+
#repeatMode = "none";
|
|
2948
|
+
#tracks = [];
|
|
2949
|
+
#previousTracks = [];
|
|
2950
|
+
context = {};
|
|
2951
|
+
voice;
|
|
2952
|
+
filters;
|
|
2953
|
+
player;
|
|
2954
|
+
constructor(player, guildId, context) {
|
|
2955
|
+
if (player.queues.has(guildId)) {
|
|
2956
|
+
throw new Error("An identical queue already exists");
|
|
2957
|
+
}
|
|
2958
|
+
const _player = player.queues[LookupSymbol](guildId);
|
|
2959
|
+
if (!_player) {
|
|
2960
|
+
throw new Error(`No player found for guild '${guildId}'`);
|
|
2961
|
+
}
|
|
2962
|
+
const voice = player.voices.get(guildId);
|
|
2963
|
+
if (!voice) {
|
|
2964
|
+
throw new Error(`No connection found for guild '${guildId}'`);
|
|
2965
|
+
}
|
|
2966
|
+
this.#player = _player;
|
|
2967
|
+
if (context !== void 0) {
|
|
2968
|
+
this.context = context;
|
|
2969
|
+
}
|
|
2970
|
+
this.voice = voice;
|
|
2971
|
+
this.filters = new FilterManager(player, guildId);
|
|
2972
|
+
this.player = player;
|
|
2973
|
+
const immutable = {
|
|
2974
|
+
writable: false,
|
|
2975
|
+
configurable: false
|
|
2976
|
+
};
|
|
2977
|
+
Object.defineProperties(this, {
|
|
2978
|
+
voice: immutable,
|
|
2979
|
+
filters: immutable,
|
|
2980
|
+
player: { ...immutable, enumerable: false }
|
|
2981
|
+
});
|
|
2982
|
+
}
|
|
2983
|
+
get node() {
|
|
2984
|
+
return this.voice.nodeSessionId ? this.player.nodes.all.find((n) => n.sessionId === this.voice.nodeSessionId) : this.player.nodes.relevant()[0];
|
|
2985
|
+
}
|
|
2986
|
+
get rest() {
|
|
2987
|
+
return this.node?.rest;
|
|
2988
|
+
}
|
|
2989
|
+
get guildId() {
|
|
2990
|
+
return this.voice.guildId;
|
|
2991
|
+
}
|
|
2992
|
+
get volume() {
|
|
2993
|
+
return this.#player.volume;
|
|
2994
|
+
}
|
|
2995
|
+
get paused() {
|
|
2996
|
+
return this.#player.paused;
|
|
2997
|
+
}
|
|
2998
|
+
get stopped() {
|
|
2999
|
+
return this.track !== null && this.#player.track === null;
|
|
3000
|
+
}
|
|
3001
|
+
get empty() {
|
|
3002
|
+
return this.finished && !this.hasPrevious;
|
|
3003
|
+
}
|
|
3004
|
+
get playing() {
|
|
3005
|
+
return !this.paused && this.track !== null && this.#player.track !== null;
|
|
3006
|
+
}
|
|
3007
|
+
get autoplay() {
|
|
3008
|
+
return this.#autoplay;
|
|
3009
|
+
}
|
|
3010
|
+
get finished() {
|
|
3011
|
+
return this.#tracks.length === 0;
|
|
3012
|
+
}
|
|
3013
|
+
get destroyed() {
|
|
3014
|
+
return this.player.queues.get(this.guildId) !== this;
|
|
3015
|
+
}
|
|
3016
|
+
get repeatMode() {
|
|
3017
|
+
return this.#repeatMode;
|
|
3018
|
+
}
|
|
3019
|
+
get hasNext() {
|
|
3020
|
+
return this.#tracks.length > 1;
|
|
3021
|
+
}
|
|
3022
|
+
get hasPrevious() {
|
|
3023
|
+
return this.#previousTracks.length !== 0;
|
|
3024
|
+
}
|
|
3025
|
+
get track() {
|
|
3026
|
+
return this.#tracks[0] ?? null;
|
|
3027
|
+
}
|
|
3028
|
+
get previousTrack() {
|
|
3029
|
+
return this.#previousTracks[this.#previousTracks.length - 1] ?? null;
|
|
3030
|
+
}
|
|
3031
|
+
get tracks() {
|
|
3032
|
+
return this.#tracks;
|
|
3033
|
+
}
|
|
3034
|
+
get previousTracks() {
|
|
3035
|
+
return this.#previousTracks;
|
|
3036
|
+
}
|
|
3037
|
+
get length() {
|
|
3038
|
+
return this.#tracks.length;
|
|
3039
|
+
}
|
|
3040
|
+
get totalLength() {
|
|
3041
|
+
return this.length + this.#previousTracks.length;
|
|
3042
|
+
}
|
|
3043
|
+
get duration() {
|
|
3044
|
+
return this.#tracks.reduce((time, track) => time + (track.isLive ? 0 : track.duration), 0);
|
|
3045
|
+
}
|
|
3046
|
+
get formattedDuration() {
|
|
3047
|
+
return this.#formatDuration(this.duration);
|
|
3048
|
+
}
|
|
3049
|
+
get currentTime() {
|
|
3050
|
+
if (this.#player.paused || !this.#player.state.connected) {
|
|
3051
|
+
return this.#player.state.position;
|
|
3052
|
+
}
|
|
3053
|
+
if (this.#player.state.position === 0) {
|
|
3054
|
+
return 0;
|
|
3055
|
+
}
|
|
3056
|
+
return this.#player.state.position + (Date.now() - this.#player.state.time);
|
|
3057
|
+
}
|
|
3058
|
+
get formattedCurrentTime() {
|
|
3059
|
+
return this.#formatDuration(this.currentTime);
|
|
3060
|
+
}
|
|
3061
|
+
#formatDuration(ms) {
|
|
3062
|
+
const seconds = Math.floor(ms / 1e3);
|
|
3063
|
+
const minutes = Math.floor(seconds / 60);
|
|
3064
|
+
const hours = Math.floor(minutes / 60);
|
|
3065
|
+
if (hours > 0) {
|
|
3066
|
+
return `${hours}:${(minutes % 60).toString().padStart(2, "0")}:${(seconds % 60).toString().padStart(2, "0")}`;
|
|
3067
|
+
}
|
|
3068
|
+
return `${minutes}:${(seconds % 60).toString().padStart(2, "0")}`;
|
|
3069
|
+
}
|
|
3070
|
+
#error(data) {
|
|
3071
|
+
const explicit = typeof data === "string";
|
|
3072
|
+
const message = explicit ? data : data.message ?? data.cause ?? "Unknown error";
|
|
3073
|
+
const error = new Error(message);
|
|
3074
|
+
error.name = `Error [${this.constructor.name}]`;
|
|
3075
|
+
if (!explicit && data.severity) {
|
|
3076
|
+
error.severity = data.severity;
|
|
3077
|
+
}
|
|
3078
|
+
return error;
|
|
3079
|
+
}
|
|
3080
|
+
async #update(data, params) {
|
|
3081
|
+
const node = this.node;
|
|
3082
|
+
if (!node) {
|
|
3083
|
+
throw this.#error("No node available");
|
|
3084
|
+
}
|
|
3085
|
+
const player = await node.rest.updatePlayer(this.guildId, data, params);
|
|
3086
|
+
Object.assign(this.#player, player);
|
|
3087
|
+
}
|
|
3088
|
+
/**
|
|
3089
|
+
* Sync queue state with Lavalink
|
|
3090
|
+
* @param target - "local" to pull from Lavalink, "remote" to push to Lavalink
|
|
3091
|
+
*/
|
|
3092
|
+
async sync(target = "local") {
|
|
3093
|
+
const node = this.node;
|
|
3094
|
+
if (!node) {
|
|
3095
|
+
throw this.#error("No node available");
|
|
3096
|
+
}
|
|
3097
|
+
if (target === "local") {
|
|
3098
|
+
const player = await node.rest.fetchPlayer(this.guildId);
|
|
3099
|
+
Object.assign(this.#player, player);
|
|
3100
|
+
return;
|
|
3101
|
+
}
|
|
3102
|
+
if (target !== "remote") {
|
|
3103
|
+
throw this.#error("Target must be 'local' or 'remote'");
|
|
3104
|
+
}
|
|
3105
|
+
const voice = this.player.voices[LookupSymbol](this.guildId);
|
|
3106
|
+
if (!voice) {
|
|
3107
|
+
return;
|
|
3108
|
+
}
|
|
3109
|
+
const request = {
|
|
3110
|
+
voice: {
|
|
3111
|
+
token: voice.token,
|
|
3112
|
+
endpoint: voice.endpoint,
|
|
3113
|
+
sessionId: voice.session_id,
|
|
3114
|
+
channelId: voice.channel_id
|
|
3115
|
+
},
|
|
3116
|
+
filters: this.#player.filters,
|
|
3117
|
+
paused: this.#player.paused,
|
|
3118
|
+
volume: this.#player.volume
|
|
3119
|
+
};
|
|
3120
|
+
if (this.#player.track !== null) {
|
|
3121
|
+
request.track = {
|
|
3122
|
+
encoded: this.#player.track.encoded,
|
|
3123
|
+
userData: this.#player.track.userData
|
|
3124
|
+
};
|
|
3125
|
+
request.position = this.#player.state.position;
|
|
3126
|
+
}
|
|
3127
|
+
await this.#update(request);
|
|
3128
|
+
const nodeSessionId = this.node?.sessionId ?? "";
|
|
3129
|
+
this.player.voices[UpdateSymbol](this.guildId, {
|
|
3130
|
+
node_session_id: nodeSessionId
|
|
3131
|
+
});
|
|
3132
|
+
}
|
|
3133
|
+
/**
|
|
3134
|
+
* Search for tracks
|
|
3135
|
+
*/
|
|
3136
|
+
async search(query, prefix = this.player.options.queryPrefix) {
|
|
3137
|
+
return this.player.search(query, { prefix, node: this.node?.name });
|
|
3138
|
+
}
|
|
3139
|
+
/**
|
|
3140
|
+
* Add tracks to the queue
|
|
3141
|
+
*/
|
|
3142
|
+
add(source, userData) {
|
|
3143
|
+
const added = [];
|
|
3144
|
+
if (source instanceof Track) {
|
|
3145
|
+
Object.assign(source.userData, userData);
|
|
3146
|
+
this.#tracks.push(source);
|
|
3147
|
+
added.push(source);
|
|
3148
|
+
} else if (source instanceof Playlist) {
|
|
3149
|
+
for (const track of source.tracks) {
|
|
3150
|
+
Object.assign(track.userData, userData);
|
|
3151
|
+
this.#tracks.push(track);
|
|
3152
|
+
added.push(track);
|
|
3153
|
+
}
|
|
3154
|
+
} else if (Array.isArray(source)) {
|
|
3155
|
+
for (const track of source) {
|
|
3156
|
+
if (track instanceof Track) {
|
|
3157
|
+
Object.assign(track.userData, userData);
|
|
3158
|
+
this.#tracks.push(track);
|
|
3159
|
+
added.push(track);
|
|
3160
|
+
}
|
|
3161
|
+
}
|
|
3162
|
+
} else {
|
|
3163
|
+
throw this.#error("Source must be a track, playlist, or array of tracks");
|
|
3164
|
+
}
|
|
3165
|
+
this.player.emit("trackAdd", this.player, this.guildId, added);
|
|
3166
|
+
return this;
|
|
3167
|
+
}
|
|
3168
|
+
/**
|
|
3169
|
+
* Add related tracks (for autoplay)
|
|
3170
|
+
*/
|
|
3171
|
+
async addRelated(refTrack) {
|
|
3172
|
+
refTrack ??= this.track ?? this.previousTrack ?? void 0;
|
|
3173
|
+
if (!refTrack) {
|
|
3174
|
+
throw this.#error("The queue is empty and there is no track to refer");
|
|
3175
|
+
}
|
|
3176
|
+
if (!this.node) {
|
|
3177
|
+
return [];
|
|
3178
|
+
}
|
|
3179
|
+
const relatedTracks = await this.player.options.fetchRelatedTracks?.(this, refTrack);
|
|
3180
|
+
this.add(relatedTracks);
|
|
3181
|
+
return relatedTracks;
|
|
3182
|
+
}
|
|
3183
|
+
remove(input) {
|
|
3184
|
+
if (typeof input === "number") {
|
|
3185
|
+
if (input === 0 && !this.stopped) {
|
|
3186
|
+
return;
|
|
3187
|
+
}
|
|
3188
|
+
if (input < 0) {
|
|
3189
|
+
return this.#previousTracks.splice(input, 1)[0];
|
|
3190
|
+
}
|
|
3191
|
+
return this.#tracks.splice(input, 1)[0];
|
|
3192
|
+
}
|
|
3193
|
+
if (Array.isArray(input)) {
|
|
3194
|
+
if (input.length === 0) {
|
|
3195
|
+
return [];
|
|
3196
|
+
}
|
|
3197
|
+
const tracks = [];
|
|
3198
|
+
const indices = input.toSorted((a, b) => a - b);
|
|
3199
|
+
for (let i = 0; i < indices.length; i++) {
|
|
3200
|
+
const index = indices[i] - i;
|
|
3201
|
+
if (index === 0 && !this.stopped) {
|
|
3202
|
+
continue;
|
|
3203
|
+
}
|
|
3204
|
+
if (index < 0) {
|
|
3205
|
+
tracks.push(...this.#previousTracks.splice(index, 1));
|
|
3206
|
+
} else if (index < this.#tracks.length) {
|
|
3207
|
+
tracks.push(...this.#tracks.splice(index, 1));
|
|
3208
|
+
}
|
|
3209
|
+
}
|
|
3210
|
+
return tracks;
|
|
3211
|
+
}
|
|
3212
|
+
throw this.#error("Input must be an index or array of indices");
|
|
3213
|
+
}
|
|
3214
|
+
/**
|
|
3215
|
+
* Clear tracks from the queue
|
|
3216
|
+
*/
|
|
3217
|
+
clear(type) {
|
|
3218
|
+
switch (type) {
|
|
3219
|
+
case "current":
|
|
3220
|
+
if (!this.finished) {
|
|
3221
|
+
this.#tracks.length = this.stopped ? 0 : 1;
|
|
3222
|
+
}
|
|
3223
|
+
break;
|
|
3224
|
+
case "previous":
|
|
3225
|
+
this.#previousTracks.length = 0;
|
|
3226
|
+
break;
|
|
3227
|
+
default:
|
|
3228
|
+
if (!this.finished) {
|
|
3229
|
+
this.#tracks.length = this.stopped ? 0 : 1;
|
|
3230
|
+
}
|
|
3231
|
+
this.#previousTracks.length = 0;
|
|
3232
|
+
}
|
|
3233
|
+
}
|
|
3234
|
+
/**
|
|
3235
|
+
* Jump to a specific track
|
|
3236
|
+
*/
|
|
3237
|
+
async jump(index) {
|
|
3238
|
+
if (this.empty) {
|
|
3239
|
+
throw this.#error("The queue is empty at the moment");
|
|
3240
|
+
}
|
|
3241
|
+
if (!Number.isInteger(index)) {
|
|
3242
|
+
throw this.#error("Index must be an integer");
|
|
3243
|
+
}
|
|
3244
|
+
const track = index < 0 ? this.#previousTracks[this.#previousTracks.length + index] : this.#tracks[index];
|
|
3245
|
+
if (!track) {
|
|
3246
|
+
throw this.#error("Specified index is out of range");
|
|
3247
|
+
}
|
|
3248
|
+
if (index < 0) {
|
|
3249
|
+
this.#tracks.unshift(...this.#previousTracks.splice(index));
|
|
3250
|
+
} else {
|
|
3251
|
+
this.#previousTracks.push(...this.#tracks.splice(0, index));
|
|
3252
|
+
}
|
|
3253
|
+
await this.#update({
|
|
3254
|
+
paused: false,
|
|
3255
|
+
track: { encoded: track.encoded, userData: track.userData }
|
|
3256
|
+
});
|
|
3257
|
+
return track;
|
|
3258
|
+
}
|
|
3259
|
+
/**
|
|
3260
|
+
* Pause playback
|
|
3261
|
+
*/
|
|
3262
|
+
async pause() {
|
|
3263
|
+
await this.#update({ paused: true });
|
|
3264
|
+
return this.#player.paused;
|
|
3265
|
+
}
|
|
3266
|
+
/**
|
|
3267
|
+
* Resume playback
|
|
3268
|
+
*/
|
|
3269
|
+
async resume() {
|
|
3270
|
+
if (this.stopped) {
|
|
3271
|
+
await this.jump(0);
|
|
3272
|
+
} else {
|
|
3273
|
+
await this.#update({ paused: false });
|
|
3274
|
+
}
|
|
3275
|
+
return !this.#player.paused;
|
|
3276
|
+
}
|
|
3277
|
+
/**
|
|
3278
|
+
* Seek to a position
|
|
3279
|
+
*/
|
|
3280
|
+
async seek(ms) {
|
|
3281
|
+
if (this.track === null) {
|
|
3282
|
+
throw this.#error("No track is playing at the moment");
|
|
3283
|
+
}
|
|
3284
|
+
if (!this.track.isSeekable) {
|
|
3285
|
+
throw this.#error("Current track is not seekable");
|
|
3286
|
+
}
|
|
3287
|
+
if (!Number.isInteger(ms) || ms < 0) {
|
|
3288
|
+
throw this.#error("Seek time must be a positive integer");
|
|
3289
|
+
}
|
|
3290
|
+
if (ms > this.track.duration) {
|
|
3291
|
+
throw this.#error("Specified time to seek is out of range");
|
|
3292
|
+
}
|
|
3293
|
+
const body = { paused: false, position: ms };
|
|
3294
|
+
if (this.#player.track?.info.identifier !== this.track.id) {
|
|
3295
|
+
body.track = { encoded: this.track.encoded, userData: this.track.userData };
|
|
3296
|
+
}
|
|
3297
|
+
await this.#update(body);
|
|
3298
|
+
return this.#player.state.position;
|
|
3299
|
+
}
|
|
3300
|
+
/**
|
|
3301
|
+
* Play next track
|
|
3302
|
+
*/
|
|
3303
|
+
async next() {
|
|
3304
|
+
if (this.hasNext) {
|
|
3305
|
+
return this.jump(1);
|
|
3306
|
+
}
|
|
3307
|
+
if (this.hasPrevious && this.#repeatMode === "queue") {
|
|
3308
|
+
const track = this.#previousTracks.shift();
|
|
3309
|
+
if (track) {
|
|
3310
|
+
this.#tracks.push(track);
|
|
3311
|
+
}
|
|
3312
|
+
return this.jump(this.hasNext ? 1 : 0);
|
|
3313
|
+
}
|
|
3314
|
+
if (!this.empty && this.#autoplay) {
|
|
3315
|
+
const related = await this.addRelated();
|
|
3316
|
+
if (related.length > 0) {
|
|
3317
|
+
return this.jump(this.length - related.length);
|
|
3318
|
+
}
|
|
3319
|
+
}
|
|
3320
|
+
if (!this.finished) {
|
|
3321
|
+
const track = this.#tracks.shift();
|
|
3322
|
+
if (track) {
|
|
3323
|
+
this.#previousTracks.push(track);
|
|
3324
|
+
}
|
|
3325
|
+
await this.stop();
|
|
3326
|
+
}
|
|
3327
|
+
return null;
|
|
3328
|
+
}
|
|
3329
|
+
/**
|
|
3330
|
+
* Play previous track
|
|
3331
|
+
*/
|
|
3332
|
+
async previous() {
|
|
3333
|
+
if (this.hasPrevious) {
|
|
3334
|
+
return this.jump(-1);
|
|
3335
|
+
}
|
|
3336
|
+
return null;
|
|
3337
|
+
}
|
|
3338
|
+
/**
|
|
3339
|
+
* Shuffle tracks
|
|
3340
|
+
*/
|
|
3341
|
+
shuffle(includePrevious = false) {
|
|
3342
|
+
if (includePrevious === true) {
|
|
3343
|
+
this.#tracks.push(...this.#previousTracks.splice(0));
|
|
3344
|
+
}
|
|
3345
|
+
if (this.#tracks.length < 3) {
|
|
3346
|
+
return this;
|
|
3347
|
+
}
|
|
3348
|
+
for (let i = this.#tracks.length - 1; i > 1; --i) {
|
|
3349
|
+
const j = Math.floor(Math.random() * i) + 1;
|
|
3350
|
+
[this.#tracks[i], this.#tracks[j]] = [this.#tracks[j], this.#tracks[i]];
|
|
3351
|
+
}
|
|
3352
|
+
return this;
|
|
3353
|
+
}
|
|
3354
|
+
/**
|
|
3355
|
+
* Set volume
|
|
3356
|
+
*/
|
|
3357
|
+
async setVolume(volume) {
|
|
3358
|
+
if (!Number.isInteger(volume) || volume < 0) {
|
|
3359
|
+
throw this.#error("Volume must be a positive integer");
|
|
3360
|
+
}
|
|
3361
|
+
if (volume > 1e3) {
|
|
3362
|
+
throw this.#error("Volume cannot be more than 1000");
|
|
3363
|
+
}
|
|
3364
|
+
await this.#update({ volume });
|
|
3365
|
+
return this.#player.volume;
|
|
3366
|
+
}
|
|
3367
|
+
/**
|
|
3368
|
+
* Set autoplay
|
|
3369
|
+
*/
|
|
3370
|
+
setAutoplay(autoplay = false) {
|
|
3371
|
+
if (typeof autoplay !== "boolean") {
|
|
3372
|
+
throw this.#error("Autoplay must be a boolean value");
|
|
3373
|
+
}
|
|
3374
|
+
this.#autoplay = autoplay;
|
|
3375
|
+
return this.#autoplay;
|
|
3376
|
+
}
|
|
3377
|
+
/**
|
|
3378
|
+
* Set repeat mode
|
|
3379
|
+
*/
|
|
3380
|
+
setRepeatMode(repeatMode = "none") {
|
|
3381
|
+
if (repeatMode !== "track" && repeatMode !== "queue" && repeatMode !== "none") {
|
|
3382
|
+
throw this.#error("Repeat mode can only be set to track, queue, or none");
|
|
3383
|
+
}
|
|
3384
|
+
this.#repeatMode = repeatMode;
|
|
3385
|
+
return this.#repeatMode;
|
|
3386
|
+
}
|
|
3387
|
+
/**
|
|
3388
|
+
* Stop playback
|
|
3389
|
+
*/
|
|
3390
|
+
async stop() {
|
|
3391
|
+
return this.#update({ track: { encoded: null } });
|
|
3392
|
+
}
|
|
3393
|
+
/**
|
|
3394
|
+
* Destroy the queue
|
|
3395
|
+
*/
|
|
3396
|
+
async destroy(reason) {
|
|
3397
|
+
return this.player.queues.destroy(this.guildId, reason);
|
|
3398
|
+
}
|
|
3399
|
+
/**
|
|
3400
|
+
* Move a track from one position to another
|
|
3401
|
+
* @param from - Current position of the track
|
|
3402
|
+
* @param to - New position for the track
|
|
3403
|
+
*/
|
|
3404
|
+
move(from, to) {
|
|
3405
|
+
if (from < 0 || from >= this.#tracks.length || to < 0 || to >= this.#tracks.length || from === to) {
|
|
3406
|
+
return null;
|
|
3407
|
+
}
|
|
3408
|
+
const track = this.#tracks[from];
|
|
3409
|
+
if (!track) {
|
|
3410
|
+
return null;
|
|
3411
|
+
}
|
|
3412
|
+
this.#tracks.splice(from, 1);
|
|
3413
|
+
this.#tracks.splice(to, 0, track);
|
|
3414
|
+
return track;
|
|
3415
|
+
}
|
|
3416
|
+
/**
|
|
3417
|
+
* Splice tracks - remove and/or add tracks at a specific position
|
|
3418
|
+
* @param index - Position to start
|
|
3419
|
+
* @param amount - Number of tracks to remove
|
|
3420
|
+
* @param tracks - Tracks to add at the position
|
|
3421
|
+
*/
|
|
3422
|
+
splice(index, amount, tracks) {
|
|
3423
|
+
if (!this.#tracks.length && tracks) {
|
|
3424
|
+
void this.add(tracks);
|
|
3425
|
+
return [];
|
|
3426
|
+
}
|
|
3427
|
+
const removed = tracks ? this.#tracks.splice(index, amount, ...Array.isArray(tracks) ? tracks : [tracks]) : this.#tracks.splice(index, amount);
|
|
3428
|
+
return removed;
|
|
3429
|
+
}
|
|
3430
|
+
/**
|
|
3431
|
+
* Sort tracks by a property or custom function
|
|
3432
|
+
* @param sortBy - Property name or comparator function
|
|
3433
|
+
* @param order - Sort order (asc/desc)
|
|
3434
|
+
*/
|
|
3435
|
+
sortBy(sortBy, order = "asc") {
|
|
3436
|
+
if (typeof sortBy === "function") {
|
|
3437
|
+
this.#tracks.sort(sortBy);
|
|
3438
|
+
} else {
|
|
3439
|
+
this.#tracks.sort((a, b) => {
|
|
3440
|
+
let comparison = 0;
|
|
3441
|
+
switch (sortBy) {
|
|
3442
|
+
case "duration":
|
|
3443
|
+
comparison = a.duration - b.duration;
|
|
3444
|
+
break;
|
|
3445
|
+
case "title":
|
|
3446
|
+
comparison = a.info.title.localeCompare(b.info.title);
|
|
3447
|
+
break;
|
|
3448
|
+
case "author":
|
|
3449
|
+
comparison = a.info.author.localeCompare(b.info.author);
|
|
3450
|
+
break;
|
|
3451
|
+
default:
|
|
3452
|
+
return 0;
|
|
3453
|
+
}
|
|
3454
|
+
return order === "desc" ? -comparison : comparison;
|
|
3455
|
+
});
|
|
3456
|
+
}
|
|
3457
|
+
return this;
|
|
3458
|
+
}
|
|
3459
|
+
/**
|
|
3460
|
+
* Get a sorted copy without modifying the original queue
|
|
3461
|
+
*/
|
|
3462
|
+
toSortedBy(sortBy, order = "asc") {
|
|
3463
|
+
const copy = [...this.#tracks];
|
|
3464
|
+
if (typeof sortBy === "function") {
|
|
3465
|
+
return copy.sort(sortBy);
|
|
3466
|
+
}
|
|
3467
|
+
return copy.sort((a, b) => {
|
|
3468
|
+
let comparison = 0;
|
|
3469
|
+
switch (sortBy) {
|
|
3470
|
+
case "duration":
|
|
3471
|
+
comparison = a.duration - b.duration;
|
|
3472
|
+
break;
|
|
3473
|
+
case "title":
|
|
3474
|
+
comparison = a.info.title.localeCompare(b.info.title);
|
|
3475
|
+
break;
|
|
3476
|
+
case "author":
|
|
3477
|
+
comparison = a.info.author.localeCompare(b.info.author);
|
|
3478
|
+
break;
|
|
3479
|
+
default:
|
|
3480
|
+
return 0;
|
|
3481
|
+
}
|
|
3482
|
+
return order === "desc" ? -comparison : comparison;
|
|
3483
|
+
});
|
|
3484
|
+
}
|
|
3485
|
+
/**
|
|
3486
|
+
* Filter tracks by predicate or criteria
|
|
3487
|
+
*/
|
|
3488
|
+
filterTracks(predicate) {
|
|
3489
|
+
if (typeof predicate === "function") {
|
|
3490
|
+
return this.#tracks.map((track, index) => ({ track, index })).filter(({ track, index }) => predicate(track, index));
|
|
3491
|
+
}
|
|
3492
|
+
return this.#tracks.map((track, index) => ({ track, index })).filter(({ track }) => {
|
|
3493
|
+
if (predicate.title && !track.info.title.toLowerCase().includes(predicate.title.toLowerCase())) {
|
|
3494
|
+
return false;
|
|
3495
|
+
}
|
|
3496
|
+
if (predicate.author && !track.info.author.toLowerCase().includes(predicate.author.toLowerCase())) {
|
|
3497
|
+
return false;
|
|
3498
|
+
}
|
|
3499
|
+
if (predicate.duration) {
|
|
3500
|
+
if (typeof predicate.duration === "number") {
|
|
3501
|
+
if (track.duration !== predicate.duration) {
|
|
3502
|
+
return false;
|
|
3503
|
+
}
|
|
3504
|
+
} else {
|
|
3505
|
+
if (predicate.duration.min && track.duration < predicate.duration.min) {
|
|
3506
|
+
return false;
|
|
3507
|
+
}
|
|
3508
|
+
if (predicate.duration.max && track.duration > predicate.duration.max) {
|
|
3509
|
+
return false;
|
|
3510
|
+
}
|
|
3511
|
+
}
|
|
3512
|
+
}
|
|
3513
|
+
if (predicate.uri && track.info.uri !== predicate.uri) {
|
|
3514
|
+
return false;
|
|
3515
|
+
}
|
|
3516
|
+
if (predicate.identifier && track.info.identifier !== predicate.identifier) {
|
|
3517
|
+
return false;
|
|
3518
|
+
}
|
|
3519
|
+
if (predicate.sourceName && track.info.sourceName !== predicate.sourceName) {
|
|
3520
|
+
return false;
|
|
3521
|
+
}
|
|
3522
|
+
if (predicate.isStream !== void 0 && track.isLive !== predicate.isStream) {
|
|
3523
|
+
return false;
|
|
3524
|
+
}
|
|
3525
|
+
if (predicate.isSeekable !== void 0 && track.isSeekable !== predicate.isSeekable) {
|
|
3526
|
+
return false;
|
|
3527
|
+
}
|
|
3528
|
+
return true;
|
|
3529
|
+
});
|
|
3530
|
+
}
|
|
3531
|
+
/**
|
|
3532
|
+
* Find a track by predicate or criteria
|
|
3533
|
+
*/
|
|
3534
|
+
findTrack(predicate) {
|
|
3535
|
+
const results = this.filterTracks(predicate);
|
|
3536
|
+
return results[0] ?? null;
|
|
3537
|
+
}
|
|
3538
|
+
/**
|
|
3539
|
+
* Get a range of tracks
|
|
3540
|
+
*/
|
|
3541
|
+
getTracks(start, end) {
|
|
3542
|
+
return this.#tracks.slice(start, end);
|
|
3543
|
+
}
|
|
3544
|
+
/**
|
|
3545
|
+
* Shift from previous tracks
|
|
3546
|
+
*/
|
|
3547
|
+
shiftPrevious() {
|
|
3548
|
+
return this.#previousTracks.shift() ?? null;
|
|
3549
|
+
}
|
|
3550
|
+
};
|
|
3551
|
+
var QueueManager = class extends EventEmitter {
|
|
3552
|
+
#player;
|
|
3553
|
+
#queues = /* @__PURE__ */ new Map();
|
|
3554
|
+
#players = /* @__PURE__ */ new Map();
|
|
3555
|
+
constructor(player) {
|
|
3556
|
+
super({ captureRejections: false });
|
|
3557
|
+
this.#player = player;
|
|
3558
|
+
}
|
|
3559
|
+
get(guildId) {
|
|
3560
|
+
return this.#queues.get(guildId);
|
|
3561
|
+
}
|
|
3562
|
+
has(guildId) {
|
|
3563
|
+
return this.#queues.has(guildId);
|
|
3564
|
+
}
|
|
3565
|
+
get all() {
|
|
3566
|
+
return Array.from(this.#queues.values());
|
|
3567
|
+
}
|
|
3568
|
+
get size() {
|
|
3569
|
+
return this.#queues.size;
|
|
3570
|
+
}
|
|
3571
|
+
keys() {
|
|
3572
|
+
return this.#queues.keys();
|
|
3573
|
+
}
|
|
3574
|
+
values() {
|
|
3575
|
+
return this.#queues.values();
|
|
3576
|
+
}
|
|
3577
|
+
entries() {
|
|
3578
|
+
return this.#queues.entries();
|
|
3579
|
+
}
|
|
3580
|
+
async create(options) {
|
|
3581
|
+
if (this.#queues.has(options.guildId)) {
|
|
3582
|
+
throw new Error(`Queue already exists for guild '${options.guildId}'`);
|
|
3583
|
+
}
|
|
3584
|
+
let voice = this.#player.voices.get(options.guildId);
|
|
3585
|
+
if (!voice) {
|
|
3586
|
+
voice = await this.#player.voices.connect(options.guildId, options.voiceId, {
|
|
3587
|
+
node: options.node,
|
|
3588
|
+
context: options.context,
|
|
3589
|
+
filters: options.filters,
|
|
3590
|
+
volume: options.volume
|
|
3591
|
+
});
|
|
3592
|
+
}
|
|
3593
|
+
const playerState = {
|
|
3594
|
+
guildId: options.guildId,
|
|
3595
|
+
track: null,
|
|
3596
|
+
volume: options.volume ?? 100,
|
|
3597
|
+
paused: false,
|
|
3598
|
+
state: {
|
|
3599
|
+
time: Date.now(),
|
|
3600
|
+
position: 0,
|
|
3601
|
+
connected: false,
|
|
3602
|
+
ping: -1
|
|
3603
|
+
},
|
|
3604
|
+
voice: {
|
|
3605
|
+
token: "",
|
|
3606
|
+
endpoint: "",
|
|
3607
|
+
sessionId: "",
|
|
3608
|
+
channelId: options.voiceId
|
|
3609
|
+
},
|
|
3610
|
+
filters: options.filters ?? {}
|
|
3611
|
+
};
|
|
3612
|
+
this.#players.set(options.guildId, playerState);
|
|
3613
|
+
const queue = new Queue(this.#player, options.guildId, options.context);
|
|
3614
|
+
this.#queues.set(options.guildId, queue);
|
|
3615
|
+
this.#player.emit("queueCreate", queue);
|
|
3616
|
+
return queue;
|
|
3617
|
+
}
|
|
3618
|
+
async destroy(guildId, reason = "destroyed") {
|
|
3619
|
+
const queue = this.#queues.get(guildId);
|
|
3620
|
+
if (!queue) {
|
|
3621
|
+
return;
|
|
3622
|
+
}
|
|
3623
|
+
const voice = this.#player.voices.get(guildId);
|
|
3624
|
+
if (voice?.node) {
|
|
3625
|
+
await voice.node.rest.destroyPlayer(guildId).catch(noop);
|
|
3626
|
+
}
|
|
3627
|
+
await this.#player.voices.destroy(guildId, reason);
|
|
3628
|
+
this.#queues.delete(guildId);
|
|
3629
|
+
this.#players.delete(guildId);
|
|
3630
|
+
this.#player.emit("queueDestroy", queue, reason);
|
|
3631
|
+
}
|
|
3632
|
+
async relocate(guildId, nodeName) {
|
|
3633
|
+
const queue = this.#queues.get(guildId);
|
|
3634
|
+
if (!queue) {
|
|
3635
|
+
throw new Error(`No queue found for guild '${guildId}'`);
|
|
3636
|
+
}
|
|
3637
|
+
const voice = this.#player.voices.get(guildId);
|
|
3638
|
+
if (!voice) {
|
|
3639
|
+
throw new Error(`No voice connection found for guild '${guildId}'`);
|
|
3640
|
+
}
|
|
3641
|
+
await voice.changeNode(nodeName);
|
|
3642
|
+
}
|
|
3643
|
+
async syncAll() {
|
|
3644
|
+
const promises = [];
|
|
3645
|
+
for (const queue of this.#queues.values()) {
|
|
3646
|
+
promises.push(queue.sync("remote").catch(noop));
|
|
3647
|
+
}
|
|
3648
|
+
await Promise.all(promises);
|
|
3649
|
+
}
|
|
3650
|
+
[LookupSymbol](guildId) {
|
|
3651
|
+
return this.#players.get(guildId);
|
|
3652
|
+
}
|
|
3653
|
+
[OnStateUpdateSymbol](guildId, state) {
|
|
3654
|
+
const player = this.#players.get(guildId);
|
|
3655
|
+
if (!player) {
|
|
3656
|
+
return;
|
|
3657
|
+
}
|
|
3658
|
+
player.state = state;
|
|
3659
|
+
const queue = this.#queues.get(guildId);
|
|
3660
|
+
if (queue) {
|
|
3661
|
+
this.#player.emit("queueUpdate", queue, state);
|
|
3662
|
+
}
|
|
3663
|
+
const voice = this.#player.voices.get(guildId);
|
|
3664
|
+
if (voice?.regionId) {
|
|
3665
|
+
const region = this.#player.voices.regions.get(voice.regionId);
|
|
3666
|
+
if (region) {
|
|
3667
|
+
region[OnPingUpdateSymbol]?.(voice.node.name, state);
|
|
3668
|
+
}
|
|
3669
|
+
}
|
|
3670
|
+
}
|
|
3671
|
+
[OnEventUpdateSymbol](guildId, event) {
|
|
3672
|
+
const queue = this.#queues.get(guildId);
|
|
3673
|
+
if (!queue) {
|
|
3674
|
+
return;
|
|
3675
|
+
}
|
|
3676
|
+
switch (event.type) {
|
|
3677
|
+
case "TrackStartEvent" /* TrackStart */:
|
|
3678
|
+
this.#handleTrackStart(queue, event);
|
|
3679
|
+
break;
|
|
3680
|
+
case "TrackEndEvent" /* TrackEnd */:
|
|
3681
|
+
void this.#handleTrackEnd(queue, event);
|
|
3682
|
+
break;
|
|
3683
|
+
case "TrackExceptionEvent" /* TrackException */:
|
|
3684
|
+
this.#handleTrackException(queue, event);
|
|
3685
|
+
break;
|
|
3686
|
+
case "TrackStuckEvent" /* TrackStuck */:
|
|
3687
|
+
this.#handleTrackStuck(queue, event);
|
|
3688
|
+
break;
|
|
3689
|
+
case "WebSocketClosedEvent" /* WebSocketClosed */:
|
|
3690
|
+
this.#handleWebSocketClosed(queue, event);
|
|
3691
|
+
break;
|
|
3692
|
+
}
|
|
3693
|
+
}
|
|
3694
|
+
#handleTrackStart(queue, event) {
|
|
3695
|
+
const track = new Track(event.track);
|
|
3696
|
+
this.#player.emit("trackStart", queue, track);
|
|
3697
|
+
}
|
|
3698
|
+
async #handleTrackEnd(queue, event) {
|
|
3699
|
+
const track = new Track(event.track);
|
|
3700
|
+
const reason = event.reason;
|
|
3701
|
+
this.#player.emit("trackFinish", queue, track, reason);
|
|
3702
|
+
const shouldAdvance = reason === "finished" /* Finished */ || reason === "loadFailed" /* LoadFailed */;
|
|
3703
|
+
if (shouldAdvance) {
|
|
3704
|
+
const nextTrack = await queue.next().catch(noop);
|
|
3705
|
+
if (!nextTrack && queue.finished) {
|
|
3706
|
+
this.#player.emit("queueFinish", queue);
|
|
3707
|
+
}
|
|
3708
|
+
}
|
|
3709
|
+
}
|
|
3710
|
+
#handleTrackException(queue, event) {
|
|
3711
|
+
const track = new Track(event.track);
|
|
3712
|
+
this.#player.emit("trackError", queue, track, event.exception);
|
|
3713
|
+
}
|
|
3714
|
+
#handleTrackStuck(queue, event) {
|
|
3715
|
+
const track = new Track(event.track);
|
|
3716
|
+
this.#player.emit("trackStuck", queue, track, event.thresholdMs);
|
|
3717
|
+
}
|
|
3718
|
+
#handleWebSocketClosed(queue, event) {
|
|
3719
|
+
const voice = this.#player.voices.get(queue.guildId);
|
|
3720
|
+
if (voice) {
|
|
3721
|
+
this.#player.voices[OnVoiceCloseSymbol]?.(queue.guildId, event.code, event.reason, event.byRemote);
|
|
3722
|
+
}
|
|
3723
|
+
}
|
|
3724
|
+
};
|
|
3725
|
+
|
|
3726
|
+
// src/core/PluginSystem.ts
|
|
3727
|
+
var PlayerPlugin = class {
|
|
3728
|
+
};
|
|
3729
|
+
|
|
3730
|
+
// src/extensions/AutoplayExtension.ts
|
|
3731
|
+
var AutoplayPlugin = class extends PlayerPlugin {
|
|
3732
|
+
name = "autoplay";
|
|
3733
|
+
#player;
|
|
3734
|
+
#adding = /* @__PURE__ */ new Set();
|
|
3735
|
+
// Track which guilds are currently adding
|
|
3736
|
+
init(player) {
|
|
3737
|
+
this.#player = player;
|
|
3738
|
+
player.on("queueFinish", (queue) => {
|
|
3739
|
+
void this.#handleQueueFinish(queue);
|
|
3740
|
+
});
|
|
3741
|
+
}
|
|
3742
|
+
async #handleQueueFinish(queue) {
|
|
3743
|
+
const lastTrack = queue.previousTrack;
|
|
3744
|
+
if (!lastTrack) {
|
|
3745
|
+
return;
|
|
3746
|
+
}
|
|
3747
|
+
const config = queue.player.get?.(`autoplay_config_${queue.guildId}`) ?? {
|
|
3748
|
+
enabled: true,
|
|
3749
|
+
minPlayTime: 1e4,
|
|
3750
|
+
sources: {
|
|
3751
|
+
spotify: true,
|
|
3752
|
+
youtube: true,
|
|
3753
|
+
youtubemusic: true,
|
|
3754
|
+
soundcloud: false
|
|
3755
|
+
},
|
|
3756
|
+
limit: 5,
|
|
3757
|
+
minDuration: 2e4,
|
|
3758
|
+
maxDuration: 9e5,
|
|
3759
|
+
excludeKeywords: ["nightcore", "bass boosted", "8d audio", "slowed", "reverb"]
|
|
3760
|
+
};
|
|
3761
|
+
if (!config.enabled) {
|
|
3762
|
+
return;
|
|
3763
|
+
}
|
|
3764
|
+
if (this.#adding.has(queue.guildId)) {
|
|
3765
|
+
return;
|
|
3766
|
+
}
|
|
3767
|
+
this.#adding.add(queue.guildId);
|
|
3768
|
+
try {
|
|
3769
|
+
const playedData = this.#buildPlayedData(queue);
|
|
3770
|
+
const relatedTracks = await this.#fetchRelatedTracks(queue, lastTrack, config, playedData);
|
|
3771
|
+
if (relatedTracks.length > 0) {
|
|
3772
|
+
for (const relatedTrack of relatedTracks) {
|
|
3773
|
+
if (!relatedTrack.pluginInfo) {
|
|
3774
|
+
relatedTrack.pluginInfo = {};
|
|
3775
|
+
}
|
|
3776
|
+
relatedTrack.pluginInfo.fromAutoplay = true;
|
|
3777
|
+
relatedTrack.userData.requester = {
|
|
3778
|
+
id: this.#player.clientId ?? "autoplay",
|
|
3779
|
+
username: "Autoplay"
|
|
3780
|
+
};
|
|
3781
|
+
queue.add(relatedTrack);
|
|
3782
|
+
}
|
|
3783
|
+
if (queue.stopped && queue.tracks.length > 0) {
|
|
3784
|
+
await queue.resume();
|
|
3785
|
+
}
|
|
3786
|
+
}
|
|
3787
|
+
} catch (error) {
|
|
3788
|
+
this.#player.emit("debug", "autoplay", {
|
|
3789
|
+
message: `Autoplay failed: ${error.message}`,
|
|
3790
|
+
state: "error",
|
|
3791
|
+
error,
|
|
3792
|
+
functionLayer: "AutoplayPlugin"
|
|
3793
|
+
});
|
|
3794
|
+
} finally {
|
|
3795
|
+
this.#adding.delete(queue.guildId);
|
|
3796
|
+
}
|
|
3797
|
+
}
|
|
3798
|
+
#buildPlayedData(queue) {
|
|
3799
|
+
const playedIds = /* @__PURE__ */ new Set();
|
|
3800
|
+
const playedTracks = /* @__PURE__ */ new Set();
|
|
3801
|
+
const addTrack = (track) => {
|
|
3802
|
+
if (track.info.identifier) {
|
|
3803
|
+
playedIds.add(track.info.identifier);
|
|
3804
|
+
}
|
|
3805
|
+
if (track.info.isrc) {
|
|
3806
|
+
playedIds.add(track.info.isrc);
|
|
3807
|
+
}
|
|
3808
|
+
if (track.info.title && track.info.author) {
|
|
3809
|
+
const key = `${track.info.title.toLowerCase()}|${track.info.author.toLowerCase()}`;
|
|
3810
|
+
playedTracks.add(key);
|
|
3811
|
+
}
|
|
3812
|
+
};
|
|
3813
|
+
if (queue.track) {
|
|
3814
|
+
addTrack(queue.track);
|
|
3815
|
+
}
|
|
3816
|
+
queue.previousTracks.forEach(addTrack);
|
|
3817
|
+
queue.tracks.forEach(addTrack);
|
|
3818
|
+
return { playedIds, playedTracks };
|
|
3819
|
+
}
|
|
3820
|
+
async #fetchRelatedTracks(queue, lastTrack, config, playedData) {
|
|
3821
|
+
const tracks = [];
|
|
3822
|
+
const source = lastTrack.info.sourceName?.toLowerCase();
|
|
3823
|
+
if (config.sources.spotify && source?.includes("spotify")) {
|
|
3824
|
+
const spotifyTracks = await this.#getSpotifyRecommendations(queue, lastTrack);
|
|
3825
|
+
tracks.push(...spotifyTracks);
|
|
3826
|
+
if (tracks.length < config.limit) {
|
|
3827
|
+
const artistTracks = await this.#getSpotifyArtistSearch(queue, lastTrack);
|
|
3828
|
+
tracks.push(...artistTracks);
|
|
3829
|
+
}
|
|
3830
|
+
}
|
|
3831
|
+
if (tracks.length < config.limit && config.sources.youtube && source?.includes("youtube")) {
|
|
3832
|
+
const youtubeTracks = await this.#getYouTubeSimilar(queue, lastTrack);
|
|
3833
|
+
tracks.push(...youtubeTracks);
|
|
3834
|
+
if (tracks.length < config.limit) {
|
|
3835
|
+
const artistTracks = await this.#getYouTubeArtist(queue, lastTrack);
|
|
3836
|
+
tracks.push(...artistTracks);
|
|
3837
|
+
}
|
|
3838
|
+
}
|
|
3839
|
+
if (tracks.length === 0 && config.sources.youtube) {
|
|
3840
|
+
const youtubeTracks = await this.#getYouTubeSimilar(queue, lastTrack);
|
|
3841
|
+
tracks.push(...youtubeTracks);
|
|
3842
|
+
}
|
|
3843
|
+
return this.#filterAutoplayTracks(tracks, playedData, config);
|
|
3844
|
+
}
|
|
3845
|
+
#filterAutoplayTracks(tracks, playedData, config) {
|
|
3846
|
+
return tracks.filter((track) => {
|
|
3847
|
+
if (!track.info) {
|
|
3848
|
+
return false;
|
|
3849
|
+
}
|
|
3850
|
+
if (playedData.playedIds.has(track.info.identifier)) {
|
|
3851
|
+
return false;
|
|
3852
|
+
}
|
|
3853
|
+
if (track.info.isrc && playedData.playedIds.has(track.info.isrc)) {
|
|
3854
|
+
return false;
|
|
3855
|
+
}
|
|
3856
|
+
const key = `${track.info.title.toLowerCase()}|${track.info.author.toLowerCase()}`;
|
|
3857
|
+
if (playedData.playedTracks.has(key)) {
|
|
3858
|
+
return false;
|
|
3859
|
+
}
|
|
3860
|
+
if (track.info.length) {
|
|
3861
|
+
if (track.info.length < config.minDuration) {
|
|
3862
|
+
return false;
|
|
3863
|
+
}
|
|
3864
|
+
if (track.info.length > config.maxDuration) {
|
|
3865
|
+
return false;
|
|
3866
|
+
}
|
|
3867
|
+
}
|
|
3868
|
+
const title = track.info.title.toLowerCase();
|
|
3869
|
+
for (const keyword of config.excludeKeywords) {
|
|
3870
|
+
if (title.includes(keyword.toLowerCase())) {
|
|
3871
|
+
return false;
|
|
3872
|
+
}
|
|
3873
|
+
}
|
|
3874
|
+
return true;
|
|
3875
|
+
}).sort(() => Math.random() - 0.5).slice(0, config.limit);
|
|
3876
|
+
}
|
|
3877
|
+
async #getSpotifyRecommendations(queue, track) {
|
|
3878
|
+
try {
|
|
3879
|
+
const query = `sprec:seed_tracks=${track.info.identifier}`;
|
|
3880
|
+
const result = await queue.search(query);
|
|
3881
|
+
if (result.loadType === "track" /* Track */ || result.loadType === "playlist" /* Playlist */) {
|
|
3882
|
+
if (result.loadType === "track" /* Track */) {
|
|
3883
|
+
return [result.data];
|
|
3884
|
+
}
|
|
3885
|
+
return result.data.tracks;
|
|
3886
|
+
}
|
|
3887
|
+
} catch (error) {
|
|
3888
|
+
}
|
|
3889
|
+
return [];
|
|
3890
|
+
}
|
|
3891
|
+
async #getSpotifyArtistSearch(queue, track) {
|
|
3892
|
+
try {
|
|
3893
|
+
const query = `spsearch:${track.info.author}`;
|
|
3894
|
+
const result = await queue.search(query);
|
|
3895
|
+
if (result.loadType === "search" /* Search */ && Array.isArray(result.data)) {
|
|
3896
|
+
return result.data.slice(0, 5);
|
|
3897
|
+
}
|
|
3898
|
+
} catch (error) {
|
|
3899
|
+
}
|
|
3900
|
+
return [];
|
|
3901
|
+
}
|
|
3902
|
+
async #getYouTubeSimilar(queue, track) {
|
|
3903
|
+
try {
|
|
3904
|
+
const query = `https://www.youtube.com/watch?v=${track.info.identifier}&list=RD${track.info.identifier}`;
|
|
3905
|
+
const result = await queue.search(query);
|
|
3906
|
+
if (result.loadType === "playlist" /* Playlist */ && result.data && "tracks" in result.data) {
|
|
3907
|
+
return result.data.tracks.slice(0, 10);
|
|
3908
|
+
}
|
|
3909
|
+
} catch (error) {
|
|
3910
|
+
}
|
|
3911
|
+
return [];
|
|
3912
|
+
}
|
|
3913
|
+
async #getYouTubeArtist(queue, track) {
|
|
3914
|
+
try {
|
|
3915
|
+
const query = `ytsearch:${track.info.author}`;
|
|
3916
|
+
const result = await queue.search(query);
|
|
3917
|
+
if (result.loadType === "search" /* Search */ && Array.isArray(result.data)) {
|
|
3918
|
+
return result.data.slice(0, 5);
|
|
3919
|
+
}
|
|
3920
|
+
} catch (error) {
|
|
3921
|
+
}
|
|
3922
|
+
return [];
|
|
3923
|
+
}
|
|
3924
|
+
};
|
|
3925
|
+
|
|
3926
|
+
// src/extensions/LyricsExtension.ts
|
|
3927
|
+
var LyricsPlugin = class extends PlayerPlugin {
|
|
3928
|
+
name = "lyrics";
|
|
3929
|
+
#player;
|
|
3930
|
+
init(player) {
|
|
3931
|
+
this.#player = player;
|
|
3932
|
+
}
|
|
3933
|
+
/**
|
|
3934
|
+
* Fetch lyrics for a track
|
|
3935
|
+
* @param track - The track to fetch lyrics for
|
|
3936
|
+
* @param config - Configuration for lyrics sources
|
|
3937
|
+
*/
|
|
3938
|
+
async getLyrics(track, config) {
|
|
3939
|
+
const defaultConfig = {
|
|
3940
|
+
sources: {
|
|
3941
|
+
lavalink: true,
|
|
3942
|
+
lrclib: true,
|
|
3943
|
+
musixmatch: false,
|
|
3944
|
+
genius: false
|
|
3945
|
+
},
|
|
3946
|
+
...config
|
|
3947
|
+
};
|
|
3948
|
+
const title = track.info.title;
|
|
3949
|
+
const artist = track.info.author;
|
|
3950
|
+
if (defaultConfig.sources.lavalink) {
|
|
3951
|
+
const lavalinkLyrics = await this.#fetchFromLavalink(track);
|
|
3952
|
+
if (lavalinkLyrics) {
|
|
3953
|
+
this.#player.emit("lyricsFound", this.#player, track, lavalinkLyrics);
|
|
3954
|
+
return lavalinkLyrics;
|
|
3955
|
+
}
|
|
3956
|
+
}
|
|
3957
|
+
if (defaultConfig.sources.lrclib) {
|
|
3958
|
+
const lrclibLyrics = await this.#fetchFromLRCLib(title, artist);
|
|
3959
|
+
if (lrclibLyrics) {
|
|
3960
|
+
this.#player.emit("lyricsFound", this.#player, track, lrclibLyrics);
|
|
3961
|
+
return lrclibLyrics;
|
|
3962
|
+
}
|
|
3963
|
+
}
|
|
3964
|
+
if (defaultConfig.sources.musixmatch && defaultConfig.apiKeys?.musixmatch) {
|
|
3965
|
+
const musixmatchLyrics = await this.#fetchFromMusixmatch(title, artist, defaultConfig.apiKeys.musixmatch);
|
|
3966
|
+
if (musixmatchLyrics) {
|
|
3967
|
+
this.#player.emit("lyricsFound", this.#player, track, musixmatchLyrics);
|
|
3968
|
+
return musixmatchLyrics;
|
|
3969
|
+
}
|
|
3970
|
+
}
|
|
3971
|
+
if (defaultConfig.sources.genius && defaultConfig.apiKeys?.genius) {
|
|
3972
|
+
const geniusLyrics = await this.#fetchFromGenius(title, artist, defaultConfig.apiKeys.genius);
|
|
3973
|
+
if (geniusLyrics) {
|
|
3974
|
+
this.#player.emit("lyricsFound", this.#player, track, geniusLyrics);
|
|
3975
|
+
return geniusLyrics;
|
|
3976
|
+
}
|
|
3977
|
+
}
|
|
3978
|
+
this.#player.emit("lyricsNotFound", this.#player, track);
|
|
3979
|
+
return null;
|
|
3980
|
+
}
|
|
3981
|
+
/**
|
|
3982
|
+
* Get the current lyric line based on playback position
|
|
3983
|
+
*/
|
|
3984
|
+
getCurrentLine(lyrics, position) {
|
|
3985
|
+
if (!lyrics.lines || lyrics.lines.length === 0) {
|
|
3986
|
+
return null;
|
|
3987
|
+
}
|
|
3988
|
+
for (let i = lyrics.lines.length - 1; i >= 0; i--) {
|
|
3989
|
+
if (lyrics.lines[i].timestamp <= position) {
|
|
3990
|
+
return lyrics.lines[i];
|
|
3991
|
+
}
|
|
3992
|
+
}
|
|
3993
|
+
return null;
|
|
3994
|
+
}
|
|
3995
|
+
/**
|
|
3996
|
+
* Format lyrics for display
|
|
3997
|
+
*/
|
|
3998
|
+
formatLyrics(lyrics, maxLength = 2e3) {
|
|
3999
|
+
if (lyrics.text) {
|
|
4000
|
+
return lyrics.text.slice(0, maxLength);
|
|
4001
|
+
}
|
|
4002
|
+
if (lyrics.lines && lyrics.lines.length > 0) {
|
|
4003
|
+
let formatted = "";
|
|
4004
|
+
for (const line of lyrics.lines) {
|
|
4005
|
+
const timestamp = this.#formatTimestamp(line.timestamp);
|
|
4006
|
+
formatted += `[${timestamp}] ${line.line}
|
|
4007
|
+
`;
|
|
4008
|
+
if (formatted.length > maxLength) {
|
|
4009
|
+
return `${formatted.slice(0, maxLength)}...`;
|
|
4010
|
+
}
|
|
4011
|
+
}
|
|
4012
|
+
return formatted;
|
|
4013
|
+
}
|
|
4014
|
+
return "No lyrics available";
|
|
4015
|
+
}
|
|
4016
|
+
#formatTimestamp(ms) {
|
|
4017
|
+
const minutes = Math.floor(ms / 6e4);
|
|
4018
|
+
const seconds = Math.floor(ms % 6e4 / 1e3);
|
|
4019
|
+
return `${minutes}:${seconds.toString().padStart(2, "0")}`;
|
|
4020
|
+
}
|
|
4021
|
+
async #fetchFromLavalink(_track) {
|
|
4022
|
+
return await Promise.resolve(null);
|
|
4023
|
+
}
|
|
4024
|
+
async #fetchFromLRCLib(_title, _artist) {
|
|
4025
|
+
return await Promise.resolve(null);
|
|
4026
|
+
}
|
|
4027
|
+
async #fetchFromMusixmatch(_title, _artist, _apiKey) {
|
|
4028
|
+
return await Promise.resolve(null);
|
|
4029
|
+
}
|
|
4030
|
+
async #fetchFromGenius(_title, _artist, _apiToken) {
|
|
4031
|
+
return await Promise.resolve(null);
|
|
4032
|
+
}
|
|
4033
|
+
};
|
|
4034
|
+
|
|
4035
|
+
// src/extensions/SponsorBlockExtension.ts
|
|
4036
|
+
var SponsorBlockPlugin = class extends PlayerPlugin {
|
|
4037
|
+
name = "sponsorblock";
|
|
4038
|
+
#player;
|
|
4039
|
+
init(player) {
|
|
4040
|
+
this.#player = player;
|
|
4041
|
+
player.on("segmentsLoaded", this.#handleSegmentsLoaded.bind(this));
|
|
4042
|
+
player.on("segmentSkipped", this.#handleSegmentSkipped.bind(this));
|
|
4043
|
+
}
|
|
4044
|
+
/**
|
|
4045
|
+
* Set which segments to skip for a player
|
|
4046
|
+
*/
|
|
4047
|
+
async setSegments(queue, segments = ["sponsor", "selfpromo"]) {
|
|
4048
|
+
const node = queue.node;
|
|
4049
|
+
if (!node) {
|
|
4050
|
+
throw new Error("No node available");
|
|
4051
|
+
}
|
|
4052
|
+
await node.setSponsorBlock(queue, segments);
|
|
4053
|
+
}
|
|
4054
|
+
/**
|
|
4055
|
+
* Get current SponsorBlock segments for a player
|
|
4056
|
+
*/
|
|
4057
|
+
async getSegments(queue) {
|
|
4058
|
+
const node = queue.node;
|
|
4059
|
+
if (!node) {
|
|
4060
|
+
throw new Error("No node available");
|
|
4061
|
+
}
|
|
4062
|
+
return await node.getSponsorBlock(queue);
|
|
4063
|
+
}
|
|
4064
|
+
/**
|
|
4065
|
+
* Delete SponsorBlock configuration for a player
|
|
4066
|
+
*/
|
|
4067
|
+
async deleteSegments(queue) {
|
|
4068
|
+
const node = queue.node;
|
|
4069
|
+
if (!node) {
|
|
4070
|
+
throw new Error("No node available");
|
|
4071
|
+
}
|
|
4072
|
+
await node.deleteSponsorBlock(queue);
|
|
4073
|
+
}
|
|
4074
|
+
#handleSegmentsLoaded(queue, track, segments) {
|
|
4075
|
+
const _segments = segments || [];
|
|
4076
|
+
this.#player.emit("segmentsLoaded", queue, track, _segments);
|
|
4077
|
+
}
|
|
4078
|
+
#handleSegmentSkipped(queue, track, segment) {
|
|
4079
|
+
const _segment = segment;
|
|
4080
|
+
this.#player.emit("segmentSkipped", queue, track, _segment);
|
|
4081
|
+
}
|
|
4082
|
+
};
|
|
4083
|
+
|
|
4084
|
+
// src/extensions/FairPlayExtension.ts
|
|
4085
|
+
var FairPlayPlugin = class extends PlayerPlugin {
|
|
4086
|
+
name = "fairplay";
|
|
4087
|
+
#player;
|
|
4088
|
+
config = {
|
|
4089
|
+
enabled: true,
|
|
4090
|
+
minTracks: 5,
|
|
4091
|
+
maxConsecutive: 3
|
|
4092
|
+
};
|
|
4093
|
+
init(player) {
|
|
4094
|
+
this.#player = player;
|
|
4095
|
+
const config = player.get?.("fairplay_config");
|
|
4096
|
+
if (config) {
|
|
4097
|
+
this.config = { ...this.config, ...config };
|
|
4098
|
+
}
|
|
4099
|
+
player.on("trackAdd", (p, g, t) => {
|
|
4100
|
+
void this.#handleTrackAdd(p, g, t);
|
|
4101
|
+
});
|
|
4102
|
+
}
|
|
4103
|
+
#handleTrackAdd(_player, guildId, _tracks) {
|
|
4104
|
+
if (!this.config.enabled) {
|
|
4105
|
+
return;
|
|
4106
|
+
}
|
|
4107
|
+
const queue = this.#player.queues.get(guildId);
|
|
4108
|
+
if (!queue || queue.length < this.config.minTracks) {
|
|
4109
|
+
return;
|
|
4110
|
+
}
|
|
4111
|
+
this.applyFairPlay(queue.guildId);
|
|
4112
|
+
}
|
|
4113
|
+
/**
|
|
4114
|
+
* Apply fair play algorithm to a queue
|
|
4115
|
+
*/
|
|
4116
|
+
applyFairPlay(guildId) {
|
|
4117
|
+
const queue = this.#player.queues.get(guildId);
|
|
4118
|
+
if (!queue) {
|
|
4119
|
+
return;
|
|
4120
|
+
}
|
|
4121
|
+
const playerConfig = this.#player.get?.("fairplay_config");
|
|
4122
|
+
const config = playerConfig ? { ...this.config, ...playerConfig } : this.config;
|
|
4123
|
+
if (queue.length < config.minTracks) {
|
|
4124
|
+
return;
|
|
4125
|
+
}
|
|
4126
|
+
const tracks = queue.tracks;
|
|
4127
|
+
const fairQueue = [];
|
|
4128
|
+
const requesterQueues = /* @__PURE__ */ new Map();
|
|
4129
|
+
for (const track of tracks) {
|
|
4130
|
+
const requesterId = this.#getRequesterId(track);
|
|
4131
|
+
let requesterQueue = requesterQueues.get(requesterId);
|
|
4132
|
+
if (!requesterQueue) {
|
|
4133
|
+
requesterQueue = [];
|
|
4134
|
+
requesterQueues.set(requesterId, requesterQueue);
|
|
4135
|
+
}
|
|
4136
|
+
requesterQueue.push(track);
|
|
4137
|
+
}
|
|
4138
|
+
let hasMore = true;
|
|
4139
|
+
let consecutiveCount = 0;
|
|
4140
|
+
let lastRequesterId = null;
|
|
4141
|
+
while (hasMore) {
|
|
4142
|
+
hasMore = false;
|
|
4143
|
+
for (const [requesterId, requesterTracks] of requesterQueues) {
|
|
4144
|
+
if (requesterTracks.length === 0) {
|
|
4145
|
+
continue;
|
|
4146
|
+
}
|
|
4147
|
+
hasMore = true;
|
|
4148
|
+
if (lastRequesterId === requesterId) {
|
|
4149
|
+
consecutiveCount++;
|
|
4150
|
+
if (consecutiveCount >= config.maxConsecutive) {
|
|
4151
|
+
continue;
|
|
4152
|
+
}
|
|
4153
|
+
} else {
|
|
4154
|
+
consecutiveCount = 1;
|
|
4155
|
+
lastRequesterId = requesterId;
|
|
4156
|
+
}
|
|
4157
|
+
const track = requesterTracks.shift();
|
|
4158
|
+
if (track) {
|
|
4159
|
+
fairQueue.push(track);
|
|
4160
|
+
}
|
|
4161
|
+
}
|
|
4162
|
+
if (!hasMore) {
|
|
4163
|
+
lastRequesterId = null;
|
|
4164
|
+
consecutiveCount = 0;
|
|
4165
|
+
hasMore = Array.from(requesterQueues.values()).some((q) => q.length > 0);
|
|
4166
|
+
}
|
|
4167
|
+
}
|
|
4168
|
+
if (fairQueue.length > 0) {
|
|
4169
|
+
queue.clear("current");
|
|
4170
|
+
queue.add(fairQueue);
|
|
4171
|
+
this.#player.emit("fairPlayApplied", this.#player, guildId, fairQueue.length);
|
|
4172
|
+
}
|
|
4173
|
+
}
|
|
4174
|
+
#getRequesterId(track) {
|
|
4175
|
+
const requester = track.userData.requester;
|
|
4176
|
+
if (requester && typeof requester === "object") {
|
|
4177
|
+
return requester.id || "unknown";
|
|
4178
|
+
}
|
|
4179
|
+
return "unknown";
|
|
4180
|
+
}
|
|
4181
|
+
};
|
|
4182
|
+
|
|
4183
|
+
// src/extensions/PersistenceExtension.ts
|
|
4184
|
+
var MemoryQueueStore = class {
|
|
4185
|
+
#data = /* @__PURE__ */ new Map();
|
|
4186
|
+
get(guildId) {
|
|
4187
|
+
return this.#data.get(guildId) ?? null;
|
|
4188
|
+
}
|
|
4189
|
+
set(guildId, data) {
|
|
4190
|
+
this.#data.set(guildId, data);
|
|
4191
|
+
}
|
|
4192
|
+
delete(guildId) {
|
|
4193
|
+
this.#data.delete(guildId);
|
|
4194
|
+
}
|
|
4195
|
+
};
|
|
4196
|
+
var QueuePersistencePlugin = class extends PlayerPlugin {
|
|
4197
|
+
name = "queue-persistence";
|
|
4198
|
+
#player;
|
|
4199
|
+
#store;
|
|
4200
|
+
#autoSave;
|
|
4201
|
+
constructor(store, autoSave = true) {
|
|
4202
|
+
super();
|
|
4203
|
+
this.#store = store ?? new MemoryQueueStore();
|
|
4204
|
+
this.#autoSave = autoSave;
|
|
4205
|
+
}
|
|
4206
|
+
init(player) {
|
|
4207
|
+
this.#player = player;
|
|
4208
|
+
if (this.#autoSave) {
|
|
4209
|
+
player.on("trackStart", (queue, _track) => {
|
|
4210
|
+
this.saveQueue(queue.guildId).catch(() => {
|
|
4211
|
+
});
|
|
4212
|
+
});
|
|
4213
|
+
player.on("trackFinish", (queue, _track, _reason) => {
|
|
4214
|
+
this.saveQueue(queue.guildId).catch(() => {
|
|
4215
|
+
});
|
|
4216
|
+
});
|
|
4217
|
+
player.on("queueDestroy", (queue) => {
|
|
4218
|
+
this.deleteQueue(queue.guildId).catch(() => {
|
|
4219
|
+
});
|
|
4220
|
+
});
|
|
4221
|
+
}
|
|
4222
|
+
}
|
|
4223
|
+
/**
|
|
4224
|
+
* Save queue state
|
|
4225
|
+
*/
|
|
4226
|
+
async saveQueue(guildId) {
|
|
4227
|
+
const queue = this.#player.queues.get(guildId);
|
|
4228
|
+
if (!queue) {
|
|
4229
|
+
return;
|
|
4230
|
+
}
|
|
4231
|
+
const data = {
|
|
4232
|
+
guildId,
|
|
4233
|
+
tracks: queue.tracks.map((track) => ({
|
|
4234
|
+
encoded: track.encoded,
|
|
4235
|
+
info: track.info,
|
|
4236
|
+
pluginInfo: track.pluginInfo,
|
|
4237
|
+
userData: track.userData
|
|
4238
|
+
})),
|
|
4239
|
+
previousTracks: queue.previousTracks.map((t) => ({
|
|
4240
|
+
encoded: t.encoded,
|
|
4241
|
+
info: t.info,
|
|
4242
|
+
pluginInfo: t.pluginInfo,
|
|
4243
|
+
userData: t.userData
|
|
4244
|
+
})),
|
|
4245
|
+
currentTrack: queue.track ? {
|
|
4246
|
+
encoded: queue.track.encoded,
|
|
4247
|
+
info: queue.track.info,
|
|
4248
|
+
position: queue.currentTime
|
|
4249
|
+
} : void 0,
|
|
4250
|
+
volume: queue.volume,
|
|
4251
|
+
repeatMode: queue.repeatMode,
|
|
4252
|
+
autoplay: queue.autoplay,
|
|
4253
|
+
paused: queue.paused,
|
|
4254
|
+
timestamp: Date.now()
|
|
4255
|
+
};
|
|
4256
|
+
await this.#store.set(guildId, data);
|
|
4257
|
+
this.#player.emit("queueSaved", guildId);
|
|
4258
|
+
}
|
|
4259
|
+
/**
|
|
4260
|
+
* Load queue state
|
|
4261
|
+
*/
|
|
4262
|
+
async loadQueue(guildId) {
|
|
4263
|
+
const data = await this.#store.get(guildId);
|
|
4264
|
+
if (!data) {
|
|
4265
|
+
return null;
|
|
4266
|
+
}
|
|
4267
|
+
const queue = this.#player.queues.get(guildId);
|
|
4268
|
+
if (!queue) {
|
|
4269
|
+
return null;
|
|
4270
|
+
}
|
|
4271
|
+
this.#player.emit("queueLoaded", guildId, data.tracks.length);
|
|
4272
|
+
return data;
|
|
4273
|
+
}
|
|
4274
|
+
/**
|
|
4275
|
+
* Delete saved queue
|
|
4276
|
+
*/
|
|
4277
|
+
async deleteQueue(guildId) {
|
|
4278
|
+
await this.#store.delete(guildId);
|
|
4279
|
+
}
|
|
4280
|
+
/**
|
|
4281
|
+
* Get all saved queues
|
|
4282
|
+
*/
|
|
4283
|
+
async getAllQueues() {
|
|
4284
|
+
return await Promise.resolve([]);
|
|
4285
|
+
}
|
|
4286
|
+
};
|
|
4287
|
+
var Player = class extends EventEmitter {
|
|
4288
|
+
#initialized = false;
|
|
4289
|
+
#initPromise = null;
|
|
4290
|
+
#clientId = null;
|
|
4291
|
+
#nodes = null;
|
|
4292
|
+
options;
|
|
4293
|
+
plugins;
|
|
4294
|
+
nodes;
|
|
4295
|
+
voices;
|
|
4296
|
+
queues;
|
|
4297
|
+
constructor(options) {
|
|
4298
|
+
super({ captureRejections: false });
|
|
4299
|
+
const _options = { ...DefaultPlayerOptions, ...options };
|
|
4300
|
+
if (_options.nodes.length === 0) {
|
|
4301
|
+
throw new Error("Missing node create options");
|
|
4302
|
+
}
|
|
4303
|
+
if (typeof _options.forwardVoiceUpdate !== "function") {
|
|
4304
|
+
throw new Error("Missing voice update function");
|
|
4305
|
+
}
|
|
4306
|
+
this.#nodes = _options.nodes;
|
|
4307
|
+
delete _options.nodes;
|
|
4308
|
+
this.options = _options;
|
|
4309
|
+
this.plugins = {};
|
|
4310
|
+
if (_options.plugins !== void 0) {
|
|
4311
|
+
for (const plugin of _options.plugins) {
|
|
4312
|
+
if (!(plugin instanceof PlayerPlugin)) {
|
|
4313
|
+
throw new Error("Invalid plugin(s)");
|
|
4314
|
+
}
|
|
4315
|
+
this.plugins[plugin.name] = plugin;
|
|
4316
|
+
}
|
|
4317
|
+
delete _options.plugins;
|
|
4318
|
+
}
|
|
4319
|
+
this.nodes = new NodeManager(this);
|
|
4320
|
+
this.voices = new VoiceManager(this);
|
|
4321
|
+
this.queues = new QueueManager(this);
|
|
4322
|
+
const immutable = {
|
|
4323
|
+
writable: false,
|
|
4324
|
+
configurable: false
|
|
4325
|
+
};
|
|
4326
|
+
Object.defineProperties(this, {
|
|
4327
|
+
options: immutable,
|
|
4328
|
+
plugins: immutable,
|
|
4329
|
+
nodes: immutable,
|
|
4330
|
+
voices: immutable,
|
|
4331
|
+
queues: immutable
|
|
4332
|
+
});
|
|
4333
|
+
}
|
|
4334
|
+
/**
|
|
4335
|
+
* Whether the player is initialized and ready
|
|
4336
|
+
*/
|
|
4337
|
+
get ready() {
|
|
4338
|
+
return this.#initialized;
|
|
4339
|
+
}
|
|
4340
|
+
/**
|
|
4341
|
+
* The bot's client ID
|
|
4342
|
+
*/
|
|
4343
|
+
get clientId() {
|
|
4344
|
+
return this.#clientId;
|
|
4345
|
+
}
|
|
4346
|
+
/**
|
|
4347
|
+
* Initialize the player
|
|
4348
|
+
* @param clientId Bot client ID
|
|
4349
|
+
*/
|
|
4350
|
+
async init(clientId) {
|
|
4351
|
+
if (this.#initialized) {
|
|
4352
|
+
return;
|
|
4353
|
+
}
|
|
4354
|
+
if (this.#initPromise !== null) {
|
|
4355
|
+
return this.#initPromise;
|
|
4356
|
+
}
|
|
4357
|
+
let resolve;
|
|
4358
|
+
let reject;
|
|
4359
|
+
const promise = new Promise((res, rej) => {
|
|
4360
|
+
resolve = res;
|
|
4361
|
+
reject = rej;
|
|
4362
|
+
});
|
|
4363
|
+
this.#initPromise = promise;
|
|
4364
|
+
this.#clientId = clientId;
|
|
4365
|
+
try {
|
|
4366
|
+
const nodes = this.#nodes ?? [];
|
|
4367
|
+
for (const node of nodes) {
|
|
4368
|
+
this.nodes.create({ ...node, clientId });
|
|
4369
|
+
}
|
|
4370
|
+
for (const name in this.plugins) {
|
|
4371
|
+
this.plugins[name].init(this);
|
|
4372
|
+
}
|
|
4373
|
+
await this.nodes.connect();
|
|
4374
|
+
this.#initialized = true;
|
|
4375
|
+
this.#nodes = null;
|
|
4376
|
+
this.emit("init");
|
|
4377
|
+
resolve();
|
|
4378
|
+
} catch (err) {
|
|
4379
|
+
reject(err);
|
|
4380
|
+
throw err;
|
|
4381
|
+
} finally {
|
|
4382
|
+
this.#initPromise = null;
|
|
4383
|
+
}
|
|
4384
|
+
}
|
|
4385
|
+
/**
|
|
4386
|
+
* Returns the queue of a guild
|
|
4387
|
+
* @param guildId Id of the guild
|
|
4388
|
+
*/
|
|
4389
|
+
getQueue(guildId) {
|
|
4390
|
+
return this.queues.get(guildId);
|
|
4391
|
+
}
|
|
4392
|
+
/**
|
|
4393
|
+
* Creates a queue from options
|
|
4394
|
+
* @param options Options to create from
|
|
4395
|
+
*/
|
|
4396
|
+
async createQueue(options) {
|
|
4397
|
+
return this.queues.create(options);
|
|
4398
|
+
}
|
|
4399
|
+
/**
|
|
4400
|
+
* Destroys the queue of a guild
|
|
4401
|
+
* @param guildId Id of the guild
|
|
4402
|
+
* @param reason Reason for destroying
|
|
4403
|
+
*/
|
|
4404
|
+
async destroyQueue(guildId, reason) {
|
|
4405
|
+
return this.queues.destroy(guildId, reason);
|
|
4406
|
+
}
|
|
4407
|
+
/**
|
|
4408
|
+
* Searches for results based on query and options
|
|
4409
|
+
* @param query Query (or URL as well)
|
|
4410
|
+
* @param options Options for customization
|
|
4411
|
+
*/
|
|
4412
|
+
async search(query, options) {
|
|
4413
|
+
if (!isString(query, "non-empty")) {
|
|
4414
|
+
throw new Error("Query must be a non-empty string");
|
|
4415
|
+
}
|
|
4416
|
+
const node = options?.node !== void 0 ? this.nodes.get(options.node) : this.nodes.relevant()[0];
|
|
4417
|
+
if (!node) {
|
|
4418
|
+
if (options?.node === void 0) {
|
|
4419
|
+
throw new Error("No nodes available");
|
|
4420
|
+
}
|
|
4421
|
+
throw new Error(`Node '${options.node}' not found`);
|
|
4422
|
+
}
|
|
4423
|
+
const prefix = options?.prefix ?? this.options.queryPrefix;
|
|
4424
|
+
query = isString(query, "url") ? query : `${String(prefix)}:${String(query)} `;
|
|
4425
|
+
const result = await node.rest.loadTracks(query);
|
|
4426
|
+
switch (result.loadType) {
|
|
4427
|
+
case "empty" /* Empty */:
|
|
4428
|
+
return { type: "empty", data: [] };
|
|
4429
|
+
case "error" /* Error */:
|
|
4430
|
+
return { type: "error", data: result.data };
|
|
4431
|
+
case "playlist" /* Playlist */:
|
|
4432
|
+
return { type: "playlist", data: new Playlist(result.data) };
|
|
4433
|
+
case "search" /* Search */:
|
|
4434
|
+
return { type: "query", data: result.data.map((t) => new Track(t)) };
|
|
4435
|
+
case "track" /* Track */:
|
|
4436
|
+
return { type: "track", data: new Track(result.data) };
|
|
4437
|
+
default:
|
|
4438
|
+
throw new Error(`Unexpected load result type from node '${node.name}'`);
|
|
4439
|
+
}
|
|
4440
|
+
}
|
|
4441
|
+
/**
|
|
4442
|
+
* Adds or searches if source is query and resumes the queue if stopped
|
|
4443
|
+
* @param source Source to play from
|
|
4444
|
+
* @param options Options for customization
|
|
4445
|
+
*/
|
|
4446
|
+
async play(source, options) {
|
|
4447
|
+
let queue = this.queues.get(options.guildId);
|
|
4448
|
+
if (typeof source === "string") {
|
|
4449
|
+
let result;
|
|
4450
|
+
if (!queue) {
|
|
4451
|
+
result = await this.search(source, options);
|
|
4452
|
+
} else {
|
|
4453
|
+
result = await queue.search(source, options.prefix);
|
|
4454
|
+
}
|
|
4455
|
+
if (result.type === "empty") {
|
|
4456
|
+
throw new Error(`No results found for '${source}'`);
|
|
4457
|
+
}
|
|
4458
|
+
if (result.type === "error") {
|
|
4459
|
+
throw new Error(result.data.message ?? result.data.cause, { cause: result.data });
|
|
4460
|
+
}
|
|
4461
|
+
source = result.type === "query" ? result.data[0] : result.data;
|
|
4462
|
+
}
|
|
4463
|
+
queue ??= await this.queues.create(options);
|
|
4464
|
+
if (options.context !== void 0) {
|
|
4465
|
+
Object.assign(queue.context, options.context);
|
|
4466
|
+
}
|
|
4467
|
+
queue.add(source, options.userData);
|
|
4468
|
+
if (queue.stopped) {
|
|
4469
|
+
await queue.resume();
|
|
4470
|
+
}
|
|
4471
|
+
return queue;
|
|
4472
|
+
}
|
|
4473
|
+
/**
|
|
4474
|
+
* Jumps to the specified index in queue of a guild
|
|
4475
|
+
* @param guildId Id of the guild
|
|
4476
|
+
* @param index Index to jump to
|
|
4477
|
+
*/
|
|
4478
|
+
async jump(guildId, index) {
|
|
4479
|
+
const queue = this.queues.get(guildId);
|
|
4480
|
+
if (!queue) {
|
|
4481
|
+
throw new Error(`No queue found for guild '${guildId}'`);
|
|
4482
|
+
}
|
|
4483
|
+
return queue.jump(index);
|
|
4484
|
+
}
|
|
4485
|
+
/**
|
|
4486
|
+
* Pauses the queue of a guild
|
|
4487
|
+
* @param guildId Id of the guild
|
|
4488
|
+
*/
|
|
4489
|
+
async pause(guildId) {
|
|
4490
|
+
const queue = this.queues.get(guildId);
|
|
4491
|
+
if (!queue) {
|
|
4492
|
+
throw new Error(`No queue found for guild '${guildId}'`);
|
|
4493
|
+
}
|
|
4494
|
+
return queue.pause();
|
|
4495
|
+
}
|
|
4496
|
+
/**
|
|
4497
|
+
* Plays the previous track in queue of a guild
|
|
4498
|
+
* @param guildId Id of the guild
|
|
4499
|
+
*/
|
|
4500
|
+
async previous(guildId) {
|
|
4501
|
+
const queue = this.queues.get(guildId);
|
|
4502
|
+
if (!queue) {
|
|
4503
|
+
throw new Error(`No queue found for guild '${guildId}'`);
|
|
4504
|
+
}
|
|
4505
|
+
return queue.previous();
|
|
4506
|
+
}
|
|
4507
|
+
/**
|
|
4508
|
+
* Resumes the queue of a guild
|
|
4509
|
+
* @param guildId Id of the guild
|
|
4510
|
+
*/
|
|
4511
|
+
async resume(guildId) {
|
|
4512
|
+
const queue = this.queues.get(guildId);
|
|
4513
|
+
if (!queue) {
|
|
4514
|
+
throw new Error(`No queue found for guild '${guildId}'`);
|
|
4515
|
+
}
|
|
4516
|
+
return queue.resume();
|
|
4517
|
+
}
|
|
4518
|
+
/**
|
|
4519
|
+
* Seeks to a position in the current track of a guild
|
|
4520
|
+
* @param guildId Id of the guild
|
|
4521
|
+
* @param ms Position in milliseconds
|
|
4522
|
+
*/
|
|
4523
|
+
async seek(guildId, ms) {
|
|
4524
|
+
const queue = this.queues.get(guildId);
|
|
4525
|
+
if (!queue) {
|
|
4526
|
+
throw new Error(`No queue found for guild '${guildId}'`);
|
|
4527
|
+
}
|
|
4528
|
+
return queue.seek(ms);
|
|
4529
|
+
}
|
|
4530
|
+
/**
|
|
4531
|
+
* Enables or disables autoplay for the queue of a guild
|
|
4532
|
+
* @param guildId Id of the guild
|
|
4533
|
+
* @param autoplay Whether to enable autoplay
|
|
4534
|
+
*/
|
|
4535
|
+
setAutoplay(guildId, autoplay) {
|
|
4536
|
+
const queue = this.queues.get(guildId);
|
|
4537
|
+
if (!queue) {
|
|
4538
|
+
throw new Error(`No queue found for guild '${guildId}'`);
|
|
4539
|
+
}
|
|
4540
|
+
return queue.setAutoplay(autoplay);
|
|
4541
|
+
}
|
|
4542
|
+
/**
|
|
4543
|
+
* Sets the repeat mode for the queue of a guild
|
|
4544
|
+
* @param guildId Id of the guild
|
|
4545
|
+
* @param repeatMode The repeat mode
|
|
4546
|
+
*/
|
|
4547
|
+
setRepeatMode(guildId, repeatMode) {
|
|
4548
|
+
const queue = this.queues.get(guildId);
|
|
4549
|
+
if (!queue) {
|
|
4550
|
+
throw new Error(`No queue found for guild '${guildId}'`);
|
|
4551
|
+
}
|
|
4552
|
+
return queue.setRepeatMode(repeatMode);
|
|
4553
|
+
}
|
|
4554
|
+
/**
|
|
4555
|
+
* Sets the volume of the queue of a guild
|
|
4556
|
+
* @param guildId Id of the guild
|
|
4557
|
+
* @param volume The volume to set
|
|
4558
|
+
*/
|
|
4559
|
+
async setVolume(guildId, volume) {
|
|
4560
|
+
const queue = this.queues.get(guildId);
|
|
4561
|
+
if (!queue) {
|
|
4562
|
+
throw new Error(`No queue found for guild '${guildId}'`);
|
|
4563
|
+
}
|
|
4564
|
+
return queue.setVolume(volume);
|
|
4565
|
+
}
|
|
4566
|
+
/**
|
|
4567
|
+
* Shuffles tracks for the queue of a guild
|
|
4568
|
+
* @param guildId Id of the guild
|
|
4569
|
+
* @param includePrevious Whether to pull previous tracks to current
|
|
4570
|
+
*/
|
|
4571
|
+
shuffle(guildId, includePrevious) {
|
|
4572
|
+
const queue = this.queues.get(guildId);
|
|
4573
|
+
if (!queue) {
|
|
4574
|
+
throw new Error(`No queue found for guild '${guildId}'`);
|
|
4575
|
+
}
|
|
4576
|
+
return queue.shuffle(includePrevious);
|
|
4577
|
+
}
|
|
4578
|
+
/**
|
|
4579
|
+
* Plays the next track in queue of a guild
|
|
4580
|
+
* @param guildId Id of the guild
|
|
4581
|
+
*/
|
|
4582
|
+
async next(guildId) {
|
|
4583
|
+
const queue = this.queues.get(guildId);
|
|
4584
|
+
if (!queue) {
|
|
4585
|
+
throw new Error(`No queue found for guild '${guildId}'`);
|
|
4586
|
+
}
|
|
4587
|
+
return queue.next();
|
|
4588
|
+
}
|
|
4589
|
+
/**
|
|
4590
|
+
* Stops the queue of a guild
|
|
4591
|
+
* @param guildId Id of the guild
|
|
4592
|
+
*/
|
|
4593
|
+
async stop(guildId) {
|
|
4594
|
+
const queue = this.queues.get(guildId);
|
|
4595
|
+
if (!queue) {
|
|
4596
|
+
throw new Error(`No queue found for guild '${guildId}'`);
|
|
4597
|
+
}
|
|
4598
|
+
return queue.stop();
|
|
4599
|
+
}
|
|
4600
|
+
};
|
|
4601
|
+
|
|
4602
|
+
export { AmazonMusicRegex, AnghamiRegex, AppleMusicRegex, AudioFileRegex, AudiomackRegex, AudiusRegex, AutoplayPlugin, BandcampRegex, CloseCodes, DeezerRegex, DefaultFilterOptions, DefaultNodeOptions, DefaultPlayerOptions, DefaultQueueOptions, DefaultRestOptions, EQPresets, FairPlayPlugin, FilterManager, GaanaRegex, HttpStatusCodes, InstagramRegex, JioSaavnRegex, LavalinkNode, LookupSymbol, LyricsPlugin, MemoryQueueStore, MixcloudRegex, LavalinkNode as Node, NodeManager, OnEventUpdateSymbol, OnPingUpdateSymbol, OnStateUpdateSymbol, OnVoiceCloseSymbol, PACKAGE_INFO, PandoraRegex, Player, PlayerPlugin, Playlist, QobuzRegex, Queue, QueueManager, QueuePersistencePlugin, REST, Routes, ShazamRegex, SnowflakeRegex, SoundCloudRegex, SponsorBlockPlugin, SpotifyRegex, TidalRegex, Track, TwitchRegex, UpdateSymbol, UrlRegex, VoiceManager, VoiceRegion, VoiceRegionIdRegex, VoiceState, WebSocketCloseCodes, YandexMusicRegex, YoutubeRegex, assert, chunk, clamp, formatDuration, getEQPreset, getEQPresetNames, isArray, isBoolean, isError, isFunction, isNullish, isNumber, isRecord, isSnowflake, isString, isUrl, isValidEQPreset, CLIENT_NAME as name, noop, parseDuration, randomElement, retry, shuffle, sleep, unique, validateNodeOptions, validatePlayerOptions, CLIENT_VERSION as version };
|
|
4603
|
+
//# sourceMappingURL=index.mjs.map
|
|
4604
|
+
//# sourceMappingURL=index.mjs.map
|