for-modules 1.1.5 → 1.1.6

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/ff.txt ADDED
@@ -0,0 +1,3037 @@
1
+ src } index.ts {
2
+ export * from "./Structures/Manager";
3
+ export * from "./Structures/Node";
4
+ export * from "./Structures/Player";
5
+ export * from "./Structures/Queue";
6
+ export * from "./Structures/Utils";
7
+ export * from "./Structures/Filters";
8
+ export * from "./Structures/Rest";
9
+ export * from "./Utils/FiltersEqualizers";
10
+ export * from "./Utils/ManagerCheck";
11
+ export * from "./Utils/NodeCheck";
12
+ export * from "./Utils/PlayerCheck";
13
+ export * from "./Structures/TikTok";
14
+ }
15
+ src } Utils } PlayerCheck.ts {
16
+ import { PlayerOptions } from "../Structures/Player";
17
+
18
+ export default function PlayerCheck(options: PlayerOptions) {
19
+ if (!options) throw new TypeError("PlayerOptions must not be empty.");
20
+
21
+ const { guild, node, selfDeafen, selfMute, textChannel, voiceChannel, volume } = options;
22
+
23
+ if (!/^\d+$/.test(guild)) {
24
+ throw new TypeError('Player option "guild" must be present and be a non-empty string.');
25
+ }
26
+
27
+ if (node && typeof node !== "string") {
28
+ throw new TypeError('Player option "node" must be a non-empty string.');
29
+ }
30
+
31
+ if (typeof selfDeafen !== "undefined" && typeof selfDeafen !== "boolean") {
32
+ throw new TypeError('Player option "selfDeafen" must be a boolean.');
33
+ }
34
+
35
+ if (typeof selfMute !== "undefined" && typeof selfMute !== "boolean") {
36
+ throw new TypeError('Player option "selfMute" must be a boolean.');
37
+ }
38
+
39
+ if (textChannel && !/^\d+$/.test(textChannel)) {
40
+ throw new TypeError('Player option "textChannel" must be a non-empty string.');
41
+ }
42
+
43
+ if (voiceChannel && !/^\d+$/.test(voiceChannel)) {
44
+ throw new TypeError('Player option "voiceChannel" must be a non-empty string.');
45
+ }
46
+
47
+ if (typeof volume !== "undefined" && typeof volume !== "number") {
48
+ throw new TypeError('Player option "volume" must be a number.');
49
+ }
50
+ }
51
+
52
+ }
53
+
54
+ src } Utils } NodeCheck.ts {
55
+ import { NodeOptions } from "../Structures/Node";
56
+
57
+ export default function NodeCheck(options: NodeOptions) {
58
+ if (!options) throw new TypeError("NodeOptions must not be empty.");
59
+
60
+ const { host, identifier, password, port, requestTimeout, resumeStatus, resumeTimeout, retryAmount, retryDelay, secure, priority } = options;
61
+
62
+ if (typeof host !== "string" || !/.+/.test(host)) {
63
+ throw new TypeError('Node option "host" must be present and be a non-empty string.');
64
+ }
65
+
66
+ if (typeof identifier !== "undefined" && typeof identifier !== "string") {
67
+ throw new TypeError('Node option "identifier" must be a non-empty string.');
68
+ }
69
+
70
+ if (typeof password !== "undefined" && (typeof password !== "string" || !/.+/.test(password))) {
71
+ throw new TypeError('Node option "password" must be a non-empty string.');
72
+ }
73
+
74
+ if (typeof port !== "undefined" && typeof port !== "number") {
75
+ throw new TypeError('Node option "port" must be a number.');
76
+ }
77
+
78
+ if (typeof requestTimeout !== "undefined" && typeof requestTimeout !== "number") {
79
+ throw new TypeError('Node option "requestTimeout" must be a number.');
80
+ }
81
+
82
+ if (typeof resumeStatus !== "undefined" && typeof resumeStatus !== "boolean") {
83
+ throw new TypeError('Node option "resumeStatus" must be a boolean.');
84
+ }
85
+
86
+ if (typeof resumeTimeout !== "undefined" && typeof resumeTimeout !== "number") {
87
+ throw new TypeError('Node option "resumeTimeout" must be a number.');
88
+ }
89
+
90
+ if (typeof retryAmount !== "undefined" && typeof retryAmount !== "number") {
91
+ throw new TypeError('Node option "retryAmount" must be a number.');
92
+ }
93
+
94
+ if (typeof retryDelay !== "undefined" && typeof retryDelay !== "number") {
95
+ throw new TypeError('Node option "retryDelay" must be a number.');
96
+ }
97
+
98
+ if (typeof secure !== "undefined" && typeof secure !== "boolean") {
99
+ throw new TypeError('Node option "secure" must be a boolean.');
100
+ }
101
+
102
+ if (typeof priority !== "undefined" && typeof priority !== "number") {
103
+ throw new TypeError('Node option "priority" must be a number.');
104
+ }
105
+ }
106
+
107
+ }
108
+
109
+ src } Utils } ManagerCheck.ts {
110
+ import { ManagerOptions } from "../Structures/Manager";
111
+
112
+ export default function ManagerCheck(options: ManagerOptions) {
113
+ if (!options) throw new TypeError("ManagerOptions must not be empty.");
114
+
115
+ const { autoPlay, clientId, clientName, defaultSearchPlatform, nodes, plugins, send, shards, trackPartial, usePriority, useNode, replaceYouTubeCredentials, caches } =
116
+ options;
117
+
118
+ if (typeof autoPlay !== "undefined" && typeof autoPlay !== "boolean") {
119
+ throw new TypeError('Manager option "autoPlay" must be a boolean.');
120
+ }
121
+
122
+ if (typeof clientId !== "undefined" && !/^\d+$/.test(clientId)) {
123
+ throw new TypeError('Manager option "clientId" must be a non-empty string.');
124
+ }
125
+
126
+ if (typeof clientName !== "undefined" && typeof clientName !== "string") {
127
+ throw new TypeError('Manager option "clientName" must be a string.');
128
+ }
129
+ if (typeof caches !== "undefined" && typeof caches !== "object") {
130
+ throw new TypeError('Manager option "caches" must be a object.');
131
+ }
132
+ if (typeof defaultSearchPlatform !== "undefined" && typeof defaultSearchPlatform !== "string") {
133
+ throw new TypeError('Manager option "defaultSearchPlatform" must be a string.');
134
+ }
135
+
136
+ if (typeof nodes !== "undefined" && !Array.isArray(nodes)) {
137
+ throw new TypeError('Manager option "nodes" must be an array.');
138
+ }
139
+
140
+ if (typeof plugins !== "undefined" && !Array.isArray(plugins)) {
141
+ throw new TypeError('Manager option "plugins" must be a Plugin array.');
142
+ }
143
+
144
+ if (typeof send !== "function") {
145
+ throw new TypeError('Manager option "send" must be present and a function.');
146
+ }
147
+
148
+ if (typeof shards !== "undefined" && typeof shards !== "number") {
149
+ throw new TypeError('Manager option "shards" must be a number.');
150
+ }
151
+
152
+ if (typeof trackPartial !== "undefined" && !Array.isArray(trackPartial)) {
153
+ throw new TypeError('Manager option "trackPartial" must be a string array.');
154
+ }
155
+
156
+ if (typeof usePriority !== "undefined" && typeof usePriority !== "boolean") {
157
+ throw new TypeError('Manager option "usePriority" must be a boolean.');
158
+ }
159
+
160
+ if (usePriority) {
161
+ for (let index = 0; index < nodes.length; index++) {
162
+ if (!nodes[index].priority) {
163
+ throw new TypeError(`Missing node option "priority" at position ${index}`);
164
+ }
165
+ }
166
+ }
167
+
168
+ if (typeof useNode !== "undefined") {
169
+ if (typeof useNode !== "string") {
170
+ throw new TypeError('Manager option "useNode" must be a string "leastLoad" or "leastPlayers".');
171
+ }
172
+
173
+ if (useNode !== "leastLoad" && useNode !== "leastPlayers") {
174
+ throw new TypeError('Manager option must be either "leastLoad" or "leastPlayers".');
175
+ }
176
+ }
177
+
178
+ if (typeof replaceYouTubeCredentials !== "undefined" && typeof replaceYouTubeCredentials !== "boolean") {
179
+ throw new TypeError('Manager option "replaceYouTubeCredentials" must be a boolean.');
180
+ }
181
+ }
182
+
183
+ }
184
+
185
+ src } Utils } FiltersEqualizers.ts {
186
+ /** Represents an equalizer band. */
187
+ export interface Band {
188
+ /** The index of the equalizer band. */
189
+ band: number;
190
+ /** The gain value of the equalizer band. */
191
+ gain: number;
192
+ }
193
+
194
+ export const bassBoostEqualizer: Band[] = [
195
+ { band: 0, gain: 0.2 },
196
+ { band: 1, gain: 0.15 },
197
+ { band: 2, gain: 0.1 },
198
+ { band: 3, gain: 0.05 },
199
+ { band: 4, gain: 0.0 },
200
+ { band: 5, gain: -0.05 },
201
+ { band: 6, gain: -0.1 },
202
+ { band: 7, gain: -0.1 },
203
+ { band: 8, gain: -0.1 },
204
+ { band: 9, gain: -0.1 },
205
+ { band: 10, gain: -0.1 },
206
+ { band: 11, gain: -0.1 },
207
+ { band: 12, gain: -0.1 },
208
+ { band: 13, gain: -0.1 },
209
+ { band: 14, gain: -0.1 },
210
+ ];
211
+
212
+ export const softEqualizer: Band[] = [
213
+ { band: 0, gain: 0 },
214
+ { band: 1, gain: 0 },
215
+ { band: 2, gain: 0 },
216
+ { band: 3, gain: 0 },
217
+ { band: 4, gain: 0 },
218
+ { band: 5, gain: 0 },
219
+ { band: 6, gain: 0 },
220
+ { band: 7, gain: 0 },
221
+ { band: 8, gain: -0.25 },
222
+ { band: 9, gain: -0.25 },
223
+ { band: 10, gain: -0.25 },
224
+ { band: 11, gain: -0.25 },
225
+ { band: 12, gain: -0.25 },
226
+ { band: 13, gain: -0.25 },
227
+ ];
228
+
229
+ export const tvEqualizer: Band[] = [
230
+ { band: 0, gain: 0 },
231
+ { band: 1, gain: 0 },
232
+ { band: 2, gain: 0 },
233
+ { band: 3, gain: 0 },
234
+ { band: 4, gain: 0 },
235
+ { band: 5, gain: 0 },
236
+ { band: 6, gain: 0 },
237
+ { band: 7, gain: 0.65 },
238
+ { band: 8, gain: 0.65 },
239
+ { band: 9, gain: 0.65 },
240
+ { band: 10, gain: 0.65 },
241
+ { band: 11, gain: 0.65 },
242
+ { band: 12, gain: 0.65 },
243
+ { band: 13, gain: 0.65 },
244
+ ];
245
+
246
+ export const trebleBassEqualizer: Band[] = [
247
+ { band: 0, gain: 0.6 },
248
+ { band: 1, gain: 0.67 },
249
+ { band: 2, gain: 0.67 },
250
+ { band: 3, gain: 0 },
251
+ { band: 4, gain: -0.5 },
252
+ { band: 5, gain: 0.15 },
253
+ { band: 6, gain: -0.45 },
254
+ { band: 7, gain: 0.23 },
255
+ { band: 8, gain: 0.35 },
256
+ { band: 9, gain: 0.45 },
257
+ { band: 10, gain: 0.55 },
258
+ { band: 11, gain: 0.6 },
259
+ { band: 12, gain: 0.55 },
260
+ { band: 13, gain: 0 },
261
+ ];
262
+
263
+ export const vaporwaveEqualizer: Band[] = [
264
+ { band: 0, gain: 0 },
265
+ { band: 1, gain: 0 },
266
+ { band: 2, gain: 0 },
267
+ { band: 3, gain: 0 },
268
+ { band: 4, gain: 0 },
269
+ { band: 5, gain: 0 },
270
+ { band: 6, gain: 0 },
271
+ { band: 7, gain: 0 },
272
+ { band: 8, gain: 0.15 },
273
+ { band: 9, gain: 0.15 },
274
+ { band: 10, gain: 0.15 },
275
+ { band: 11, gain: 0.15 },
276
+ { band: 12, gain: 0.15 },
277
+ { band: 13, gain: 0.15 },
278
+ ];
279
+
280
+ }
281
+
282
+ src } Utils } Structures } Utils.ts {
283
+ /* eslint-disable @typescript-eslint/no-unused-vars*/
284
+ import { ClientUser, User } from "discord.js";
285
+ import { Manager } from "./Manager";
286
+ import { Node, NodeStats } from "./Node";
287
+ import { Player, Track, UnresolvedTrack } from "./Player";
288
+ import { Queue } from "./Queue";
289
+
290
+ /** @hidden */
291
+ const TRACK_SYMBOL = Symbol("track"),
292
+ /** @hidden */
293
+ UNRESOLVED_TRACK_SYMBOL = Symbol("unresolved"),
294
+ SIZES = ["0", "1", "2", "3", "default", "mqdefault", "hqdefault", "maxresdefault"];
295
+
296
+ /** @hidden */
297
+ const escapeRegExp = (str: string): string => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
298
+
299
+ export abstract class TrackUtils {
300
+ static trackPartial: string[] | null = null;
301
+ private static manager: Manager;
302
+
303
+ /** @hidden */
304
+ public static init(manager: Manager): void {
305
+ this.manager = manager;
306
+ }
307
+
308
+ static setTrackPartial(partial: string[]): void {
309
+ if (!Array.isArray(partial) || !partial.every((str) => typeof str === "string")) throw new Error("Provided partial is not an array or not a string array.");
310
+ if (!partial.includes("track")) partial.unshift("track");
311
+
312
+ this.trackPartial = partial;
313
+ }
314
+
315
+ /**
316
+ * Checks if the provided argument is a valid Track or UnresolvedTrack, if provided an array then every element will be checked.
317
+ * @param trackOrTracks
318
+ */
319
+ static validate(trackOrTracks: unknown): boolean {
320
+ if (typeof trackOrTracks === "undefined") throw new RangeError("Provided argument must be present.");
321
+
322
+ if (Array.isArray(trackOrTracks) && trackOrTracks.length) {
323
+ for (const track of trackOrTracks) {
324
+ if (!(track[TRACK_SYMBOL] || track[UNRESOLVED_TRACK_SYMBOL])) return false;
325
+ }
326
+ return true;
327
+ }
328
+
329
+ return (trackOrTracks[TRACK_SYMBOL] || trackOrTracks[UNRESOLVED_TRACK_SYMBOL]) === true;
330
+ }
331
+
332
+ /**
333
+ * Checks if the provided argument is a valid UnresolvedTrack.
334
+ * @param track
335
+ */
336
+ static isUnresolvedTrack(track: unknown): boolean {
337
+ if (typeof track === "undefined") throw new RangeError("Provided argument must be present.");
338
+ return track[UNRESOLVED_TRACK_SYMBOL] === true;
339
+ }
340
+
341
+ /**
342
+ * Checks if the provided argument is a valid Track.
343
+ * @param track
344
+ */
345
+ static isTrack(track: unknown): boolean {
346
+ if (typeof track === "undefined") throw new RangeError("Provided argument must be present.");
347
+ return track[TRACK_SYMBOL] === true;
348
+ }
349
+
350
+ /**
351
+ * Builds a Track from the raw data from Lavalink and a optional requester.
352
+ * @param data
353
+ * @param requester
354
+ */
355
+ static build(data: TrackData, requester?: User | ClientUser): Track {
356
+ if (typeof data === "undefined") throw new RangeError('Argument "data" must be present.');
357
+
358
+ try {
359
+ const track: Track = {
360
+ track: data.encoded,
361
+ title: data.info.title,
362
+ identifier: data.info.identifier,
363
+ author: data.info.author,
364
+ duration: data.info.length,
365
+ isrc: data.info?.isrc,
366
+ isSeekable: data.info.isSeekable,
367
+ isStream: data.info.isStream,
368
+ uri: data.info.uri,
369
+ artworkUrl: data.info?.artworkUrl,
370
+ sourceName: data.info?.sourceName,
371
+ thumbnail: data.info.uri.includes("youtube") ? `https://img.youtube.com/vi/${data.info.identifier}/default.jpg` : null,
372
+ displayThumbnail(size = "default"): string | null {
373
+ const finalSize = SIZES.find((s) => s === size) ?? "default";
374
+ return this.uri.includes("youtube") ? `https://img.youtube.com/vi/${data.info.identifier}/${finalSize}.jpg` : null;
375
+ },
376
+ requester,
377
+ pluginInfo: data.pluginInfo,
378
+ customData: {},
379
+ };
380
+
381
+ track.displayThumbnail = track.displayThumbnail.bind(track);
382
+
383
+ if (this.trackPartial) {
384
+ for (const key of Object.keys(track)) {
385
+ if (this.trackPartial.includes(key)) continue;
386
+ delete track[key];
387
+ }
388
+ }
389
+
390
+ Object.defineProperty(track, TRACK_SYMBOL, {
391
+ configurable: true,
392
+ value: true,
393
+ });
394
+
395
+ return track;
396
+ } catch (error) {
397
+ throw new RangeError(`Argument "data" is not a valid track: ${error.message}`);
398
+ }
399
+ }
400
+
401
+ /**
402
+ * Builds a UnresolvedTrack to be resolved before being played .
403
+ * @param query
404
+ * @param requester
405
+ */
406
+ static buildUnresolved(query: string | UnresolvedQuery, requester?: User | ClientUser): UnresolvedTrack {
407
+ if (typeof query === "undefined") throw new RangeError('Argument "query" must be present.');
408
+
409
+ let unresolvedTrack: Partial<UnresolvedTrack> = {
410
+ requester,
411
+ async resolve(): Promise<void> {
412
+ const resolved = await TrackUtils.getClosestTrack(this);
413
+ Object.getOwnPropertyNames(this).forEach((prop) => delete this[prop]);
414
+ Object.assign(this, resolved);
415
+ },
416
+ };
417
+
418
+ if (typeof query === "string") unresolvedTrack.title = query;
419
+ else unresolvedTrack = { ...unresolvedTrack, ...query };
420
+
421
+ Object.defineProperty(unresolvedTrack, UNRESOLVED_TRACK_SYMBOL, {
422
+ configurable: true,
423
+ value: true,
424
+ });
425
+
426
+ return unresolvedTrack as UnresolvedTrack;
427
+ }
428
+
429
+ static async getClosestTrack(unresolvedTrack: UnresolvedTrack): Promise<Track> {
430
+ if (!TrackUtils.manager) throw new RangeError("Manager has not been initiated.");
431
+
432
+ if (!TrackUtils.isUnresolvedTrack(unresolvedTrack)) throw new RangeError("Provided track is not a UnresolvedTrack.");
433
+
434
+ const query = unresolvedTrack.uri ? unresolvedTrack.uri : [unresolvedTrack.author, unresolvedTrack.title].filter(Boolean).join(" - ");
435
+ const res = await TrackUtils.manager.search(query, unresolvedTrack.requester);
436
+
437
+ if (unresolvedTrack.author) {
438
+ const channelNames = [unresolvedTrack.author, `${unresolvedTrack.author} - Topic`];
439
+
440
+ const originalAudio = res.tracks.find((track) => {
441
+ return (
442
+ channelNames.some((name) => new RegExp(`^${escapeRegExp(name)}$`, "i").test(track.author)) ||
443
+ new RegExp(`^${escapeRegExp(unresolvedTrack.title)}$`, "i").test(track.title)
444
+ );
445
+ });
446
+
447
+ if (originalAudio) return originalAudio;
448
+ }
449
+
450
+ if (unresolvedTrack.duration) {
451
+ const sameDuration = res.tracks.find((track) => track.duration >= unresolvedTrack.duration - 1500 && track.duration <= unresolvedTrack.duration + 1500);
452
+
453
+ if (sameDuration) return sameDuration;
454
+ }
455
+
456
+ const finalTrack = res.tracks[0];
457
+ finalTrack.customData = unresolvedTrack.customData;
458
+ return finalTrack;
459
+ }
460
+ }
461
+
462
+ /** Gets or extends structures to extend the built in, or already extended, classes to add more functionality. */
463
+ export abstract class Structure {
464
+ /**
465
+ * Extends a class.
466
+ * @param name
467
+ * @param extender
468
+ */
469
+ public static extend<K extends keyof Extendable, T extends Extendable[K]>(name: K, extender: (target: Extendable[K]) => T): T {
470
+ if (!structures[name]) throw new TypeError(`"${name} is not a valid structure`);
471
+ const extended = extender(structures[name]);
472
+ structures[name] = extended;
473
+ return extended;
474
+ }
475
+
476
+ /**
477
+ * Get a structure from available structures by name.
478
+ * @param name
479
+ */
480
+ public static get<K extends keyof Extendable>(name: K): Extendable[K] {
481
+ const structure = structures[name];
482
+ if (!structure) throw new TypeError('"structure" must be provided.');
483
+ return structure;
484
+ }
485
+ }
486
+
487
+ export class Plugin {
488
+ public load(manager: Manager): void {}
489
+ public unload(manager: Manager): void {}
490
+ }
491
+
492
+ const structures = {
493
+ Player,
494
+ Queue,
495
+ Node,
496
+ };
497
+
498
+ export interface UnresolvedQuery {
499
+ /** The title of the unresolved track. */
500
+ title: string;
501
+ /** The author of the unresolved track. If provided it will have a more precise search. */
502
+ author?: string;
503
+ /** The duration of the unresolved track. If provided it will have a more precise search. */
504
+ duration?: number;
505
+ }
506
+
507
+ export type Sizes = "0" | "1" | "2" | "3" | "default" | "mqdefault" | "hqdefault" | "maxresdefault";
508
+
509
+ export type LoadType = "track" | "playlist" | "search" | "empty" | "error";
510
+
511
+ export type State = "CONNECTED" | "CONNECTING" | "DISCONNECTED" | "DISCONNECTING" | "DESTROYING" | "MOVING";
512
+
513
+ export type PlayerEvents = TrackStartEvent | TrackEndEvent | TrackStuckEvent | TrackExceptionEvent | WebSocketClosedEvent;
514
+
515
+ export type PlayerEventType = "TrackStartEvent" | "TrackEndEvent" | "TrackExceptionEvent" | "TrackStuckEvent" | "WebSocketClosedEvent";
516
+
517
+ export type TrackEndReason = "finished" | "loadFailed" | "stopped" | "replaced" | "cleanup";
518
+
519
+ export type Severity = "common" | "suspicious" | "fault";
520
+
521
+ export interface TrackData {
522
+ /** The track information. */
523
+ encoded: string;
524
+ /** The detailed information of the track. */
525
+ info: TrackDataInfo;
526
+ /** Additional track info provided by plugins. */
527
+ pluginInfo: Record<string, string>;
528
+ }
529
+
530
+ export interface TrackDataInfo {
531
+ identifier: string;
532
+ isSeekable: boolean;
533
+ author: string;
534
+ length: number;
535
+ isrc?: string;
536
+ isStream: boolean;
537
+ title: string;
538
+ uri?: string;
539
+ artworkUrl?: string;
540
+ sourceName?: TrackSourceName;
541
+ }
542
+
543
+ export type TrackSourceName = "deezer" | "spotify" | "soundcloud" | "youtube" | "tiktok";
544
+
545
+ export interface Extendable {
546
+ Player: typeof Player;
547
+ Queue: typeof Queue;
548
+ Node: typeof Node;
549
+ }
550
+
551
+ export interface VoiceState {
552
+ op: "voiceUpdate";
553
+ guildId: string;
554
+ event: VoiceServer;
555
+ sessionId?: string;
556
+ }
557
+
558
+ export interface VoiceServer {
559
+ token: string;
560
+ guild_id: string;
561
+ endpoint: string;
562
+ }
563
+
564
+ export interface VoiceState {
565
+ guild_id: string;
566
+ user_id: string;
567
+ session_id: string;
568
+ channel_id: string;
569
+ }
570
+
571
+ export interface VoicePacket {
572
+ t?: "VOICE_SERVER_UPDATE" | "VOICE_STATE_UPDATE";
573
+ d: VoiceState | VoiceServer;
574
+ }
575
+
576
+ export interface NodeMessage extends NodeStats {
577
+ type: PlayerEventType;
578
+ op: "stats" | "playerUpdate" | "event";
579
+ guildId: string;
580
+ }
581
+
582
+ export interface PlayerEvent {
583
+ op: "event";
584
+ type: PlayerEventType;
585
+ guildId: string;
586
+ }
587
+
588
+ export interface Exception {
589
+ message: string;
590
+ severity: Severity;
591
+ cause: string;
592
+ }
593
+
594
+ export interface TrackStartEvent extends PlayerEvent {
595
+ type: "TrackStartEvent";
596
+ track: TrackData;
597
+ }
598
+
599
+ export interface TrackEndEvent extends PlayerEvent {
600
+ type: "TrackEndEvent";
601
+ track: TrackData;
602
+ reason: TrackEndReason;
603
+ }
604
+
605
+ export interface TrackExceptionEvent extends PlayerEvent {
606
+ exception?: Exception;
607
+ guildId: string;
608
+ type: "TrackExceptionEvent";
609
+ }
610
+
611
+ export interface TrackStuckEvent extends PlayerEvent {
612
+ type: "TrackStuckEvent";
613
+ thresholdMs: number;
614
+ }
615
+
616
+ export interface WebSocketClosedEvent extends PlayerEvent {
617
+ type: "WebSocketClosedEvent";
618
+ code: number;
619
+ reason: string;
620
+ byRemote: boolean;
621
+ }
622
+
623
+ export interface PlayerUpdate {
624
+ op: "playerUpdate";
625
+ /** The guild id of the player. */
626
+ guildId: string;
627
+ state: {
628
+ /** Unix timestamp in milliseconds. */
629
+ time: number;
630
+ /** The position of the track in milliseconds. */
631
+ position: number;
632
+ /** Whether Lavalink is connected to the voice gateway. */
633
+ connected: boolean;
634
+ /** The ping of the node to the Discord voice server in milliseconds (-1 if not connected). */
635
+ ping: number;
636
+ };
637
+ }
638
+
639
+ }
640
+ src } Utils } Structures } TikTok.ts {
641
+ import axios from "axios";
642
+ import { TrackUtils, TrackSourceName } from "./Utils";
643
+ import { User, ClientUser } from "discord.js";
644
+
645
+ export class TikTok {
646
+ /**
647
+ * ตรวจสอบว่าลิงก์เป็นของ TikTok หรือไม่
648
+ */
649
+ static isTikTokURL(url: string): boolean {
650
+ return url.includes("tiktok.com");
651
+ }
652
+
653
+ /**
654
+ * ค้นหาและดึงลิงก์เสียงจาก TikTok
655
+ */
656
+ static async searchTikTok(url: string, requester?: User | ClientUser) {
657
+ const tiktokAudioURL = await this.extractTikTokAudio(url);
658
+
659
+ if (!tiktokAudioURL) {
660
+ throw new Error("ไม่สามารถดึงข้อมูลเสียงจาก TikTok ได้");
661
+ }
662
+
663
+ return {
664
+ loadType: "track",
665
+ tracks: [TrackUtils.build({
666
+ encoded: tiktokAudioURL,
667
+ info: {
668
+ title: "TikTok Audio",
669
+ identifier: url,
670
+ author: "TikTok User",
671
+ length: 0,
672
+ isSeekable: true,
673
+ isStream: false,
674
+ uri: url,
675
+ sourceName: "tiktok" as TrackSourceName
676
+ },
677
+ pluginInfo: {}
678
+ }, requester)]
679
+ };
680
+ }
681
+
682
+ /**
683
+ * ดึงลิงก์เสียงจาก TikTok โดยใช้ API หรือ Web Scraper
684
+ */
685
+ static async extractTikTokAudio(url: string): Promise<string | null> {
686
+ try {
687
+ const response = await axios.get(`https://api.example.com/extract?tiktok_url=${encodeURIComponent(url)}`);
688
+ return response.data.audio_url;
689
+ } catch (error) {
690
+ console.error("Error extracting TikTok audio:", error);
691
+ return null;
692
+ }
693
+ }
694
+ }
695
+
696
+ }
697
+
698
+ src } Utils } Structures } Rest.ts {
699
+ import { Node } from "./Node";
700
+ import axios, { AxiosRequestConfig, AxiosResponse } from "axios";
701
+
702
+ /** Handles the requests sent to the Lavalink REST API. */
703
+ export class Rest {
704
+ /** The Node that this Rest instance is connected to. */
705
+ private node: Node;
706
+ /** The ID of the current session. */
707
+ private sessionId: string;
708
+ /** The password for the Node. */
709
+ private readonly password: string;
710
+ /** The URL of the Node. */
711
+ private readonly url: string;
712
+
713
+ constructor(node: Node) {
714
+ this.node = node;
715
+ this.url = `http${node.options.secure ? "s" : ""}://${node.options.host}:${node.options.port}`;
716
+ this.sessionId = node.sessionId;
717
+ this.password = node.options.password;
718
+ }
719
+
720
+ /**
721
+ * Sets the session ID.
722
+ * @returns {string} Returns the session ID.
723
+ */
724
+ public setSessionId(sessionId: string): string {
725
+ this.sessionId = sessionId;
726
+ return this.sessionId;
727
+ }
728
+
729
+ /** Retrieves all the players that are currently running on the node. */
730
+ public async getAllPlayers(): Promise<unknown> {
731
+ return await this.get(`/v4/sessions/${this.sessionId}/players`);
732
+ }
733
+
734
+ /** Sends a PATCH request to update player related data. */
735
+ public async updatePlayer(options: playOptions): Promise<unknown> {
736
+ return await this.patch(`/v4/sessions/${this.sessionId}/players/${options.guildId}?noReplace=false`, options.data);
737
+ }
738
+
739
+ /** Sends a DELETE request to the server to destroy the player. */
740
+ public async destroyPlayer(guildId: string): Promise<unknown> {
741
+ return await this.delete(`/v4/sessions/${this.sessionId}/players/${guildId}`);
742
+ }
743
+
744
+ /* Sends a GET request to the specified endpoint and returns the response data. */
745
+ private async request(method: Method, endpoint: string, body?: unknown): Promise<unknown> {
746
+ const config: AxiosRequestConfig = {
747
+ method,
748
+ url: this.url + endpoint,
749
+ headers: {
750
+ "Content-Type": "application/json",
751
+ Authorization: this.password,
752
+ },
753
+ data: body,
754
+ };
755
+
756
+ try {
757
+ const response = await axios(config) as AxiosResponse
758
+ return response.data;
759
+ } catch (error) {
760
+ if (error?.response?.status === 404) {
761
+ this.node.destroy();
762
+ this.node.manager.createNode(this.node.options).connect();
763
+ }
764
+ return null;
765
+ }
766
+ }
767
+
768
+ /* Sends a GET request to the specified endpoint and returns the response data. */
769
+ public async get(endpoint: string): Promise<unknown> {
770
+ return await this.request("GET", endpoint);
771
+ }
772
+
773
+ /* Sends a PATCH request to the specified endpoint and returns the response data. */
774
+ public async patch(endpoint: string, body: unknown): Promise<unknown> {
775
+ return await this.request("PATCH", endpoint, body);
776
+ }
777
+
778
+ /* Sends a POST request to the specified endpoint and returns the response data. */
779
+ public async post(endpoint: string, body: unknown): Promise<unknown> {
780
+ return await this.request("POST", endpoint, body);
781
+ }
782
+
783
+ /* Sends a DELETE request to the specified endpoint and returns the response data. */
784
+ public async delete(endpoint: string): Promise<unknown> {
785
+ return await this.request("DELETE", endpoint);
786
+ }
787
+ }
788
+
789
+ interface playOptions {
790
+ guildId: string;
791
+ data: {
792
+ /** The base64 encoded track. */
793
+ encodedTrack?: string;
794
+ /** The track ID. */
795
+ identifier?: string;
796
+ /** The track time to start at. */
797
+ startTime?: number;
798
+ /** The track time to end at. */
799
+ endTime?: number;
800
+ /** The player volume level. */
801
+ volume?: number;
802
+ /** The player position in a track. */
803
+ position?: number;
804
+ /** Whether the player is paused. */
805
+ paused?: boolean;
806
+ /** The audio effects. */
807
+ filters?: object;
808
+ /** voice payload. */
809
+ voice?: {
810
+ token: string;
811
+ sessionId: string;
812
+ endpoint: string;
813
+ };
814
+ /** Whether to not replace the track if a play payload is sent. */
815
+ noReplace?: boolean;
816
+ };
817
+ }
818
+
819
+ export type Method = "GET" | "POST" | "PATCH" | "DELETE";
820
+ }
821
+
822
+ src } Utils } Structures } Queue.ts {
823
+ import { Track, UnresolvedTrack } from "./Player";
824
+ import { TrackUtils } from "./Utils";
825
+
826
+ /**
827
+ * The player's queue, the `current` property is the currently playing track, think of the rest as the up-coming tracks.
828
+ */
829
+ export class Queue extends Array<Track | UnresolvedTrack> {
830
+ /** The total duration of the queue. */
831
+ public get duration(): number {
832
+ const current = this.current?.duration ?? 0;
833
+ return this.reduce((acc, cur) => acc + (cur.duration || 0), current);
834
+ }
835
+
836
+ /** The total size of tracks in the queue including the current track. */
837
+ public get totalSize(): number {
838
+ return this.length + (this.current ? 1 : 0);
839
+ }
840
+
841
+ /** The size of tracks in the queue. */
842
+ public get size(): number {
843
+ return this.length;
844
+ }
845
+
846
+ /** The current track */
847
+ public current: Track | UnresolvedTrack | null = null;
848
+
849
+ /** The previous track */
850
+ public previous: Track | UnresolvedTrack | null = null;
851
+
852
+ /**
853
+ * Adds a track to the queue.
854
+ * @param track
855
+ * @param [offset=null]
856
+ */
857
+ public add(track: (Track | UnresolvedTrack) | (Track | UnresolvedTrack)[], offset?: number): void {
858
+ if (!TrackUtils.validate(track)) {
859
+ throw new RangeError('Track must be a "Track" or "Track[]".');
860
+ }
861
+
862
+ if (!this.current) {
863
+ if (Array.isArray(track)) {
864
+ this.current = track.shift() || null;
865
+ this.push(...track);
866
+ } else {
867
+ this.current = track;
868
+ }
869
+ } else {
870
+ if (typeof offset !== "undefined" && typeof offset === "number") {
871
+ if (isNaN(offset)) {
872
+ throw new RangeError("Offset must be a number.");
873
+ }
874
+
875
+ if (offset < 0 || offset > this.length) {
876
+ throw new RangeError(`Offset must be between 0 and ${this.length}.`);
877
+ }
878
+
879
+ if (Array.isArray(track)) {
880
+ this.splice(offset, 0, ...track);
881
+ } else {
882
+ this.splice(offset, 0, track);
883
+ }
884
+ } else {
885
+ if (Array.isArray(track)) {
886
+ this.push(...track);
887
+ } else {
888
+ this.push(track);
889
+ }
890
+ }
891
+ }
892
+ }
893
+
894
+ /**
895
+ * Removes a track from the queue. Defaults to the first track, returning the removed track, EXCLUDING THE `current` TRACK.
896
+ * @param [position=0]
897
+ */
898
+ public remove(position?: number): (Track | UnresolvedTrack)[];
899
+
900
+ /**
901
+ * Removes an amount of tracks using a exclusive start and end exclusive index, returning the removed tracks, EXCLUDING THE `current` TRACK.
902
+ * @param start
903
+ * @param end
904
+ */
905
+ public remove(start: number, end: number): (Track | UnresolvedTrack)[];
906
+
907
+ public remove(startOrPosition = 0, end?: number): (Track | UnresolvedTrack)[] {
908
+ if (typeof end !== "undefined") {
909
+ if (isNaN(Number(startOrPosition)) || isNaN(Number(end))) {
910
+ throw new RangeError(`Missing "start" or "end" parameter.`);
911
+ }
912
+
913
+ if (startOrPosition >= end || startOrPosition >= this.length) {
914
+ throw new RangeError("Invalid start or end values.");
915
+ }
916
+
917
+ return this.splice(startOrPosition, end - startOrPosition);
918
+ }
919
+
920
+ return this.splice(startOrPosition, 1);
921
+ }
922
+
923
+ /** Clears the queue. */
924
+ public clear(): void {
925
+ this.splice(0);
926
+ }
927
+
928
+ /** Shuffles the queue. */
929
+ public shuffle(): void {
930
+ for (let i = this.length - 1; i > 0; i--) {
931
+ const j = Math.floor(Math.random() * (i + 1));
932
+ [this[i], this[j]] = [this[j], this[i]];
933
+ }
934
+ }
935
+
936
+ public equalizedShuffle() {
937
+ const userTracks = new Map<string, Array<Track | UnresolvedTrack>>();
938
+
939
+ this.forEach((track) => {
940
+ const user = track.requester.id;
941
+
942
+ if (!userTracks.has(user)) {
943
+ userTracks.set(user, []);
944
+ }
945
+
946
+ userTracks.get(user).push(track);
947
+ });
948
+
949
+ const shuffledQueue: Array<Track | UnresolvedTrack> = [];
950
+
951
+ while (shuffledQueue.length < this.length) {
952
+ userTracks.forEach((tracks) => {
953
+ const track = tracks.shift();
954
+ if (track) {
955
+ shuffledQueue.push(track);
956
+ }
957
+ });
958
+ }
959
+
960
+ this.clear();
961
+ this.add(shuffledQueue);
962
+ console.log(this);
963
+ }
964
+ }
965
+
966
+ }
967
+
968
+ src } Utils } Structures } Player.ts {
969
+ import { Filters } from "./Filters";
970
+ import { LavalinkResponse, Manager, PlaylistRawData, SearchQuery, SearchResult } from "./Manager";
971
+ import { LavalinkInfo, Node } from "./Node";
972
+ import { Queue } from "./Queue";
973
+ import { Sizes, State, Structure, TrackSourceName, TrackUtils, VoiceState } from "./Utils";
974
+ import * as _ from "lodash";
975
+ import playerCheck from "../Utils/PlayerCheck";
976
+ import { ClientUser, Message, User } from "discord.js";
977
+
978
+ export class Player {
979
+ /** The Queue for the Player. */
980
+ public readonly queue = new (Structure.get("Queue"))() as Queue;
981
+ /** The filters applied to the audio. */
982
+ public filters: Filters;
983
+ /** Whether the queue repeats the track. */
984
+ public trackRepeat = false;
985
+ /** Whether the queue repeats the queue. */
986
+ public queueRepeat = false;
987
+ /**Whether the queue repeats and shuffles after each song. */
988
+ public dynamicRepeat = false;
989
+ /** The time the player is in the track. */
990
+ public position = 0;
991
+ /** Whether the player is playing. */
992
+ public playing = false;
993
+ /** Whether the player is paused. */
994
+ public paused = false;
995
+ /** The volume for the player */
996
+ public volume: number;
997
+ /** The Node for the Player. */
998
+ public node: Node;
999
+ /** The guild for the player. */
1000
+ public guild: string;
1001
+ /** The voice channel for the player. */
1002
+ public voiceChannel: string | null = null;
1003
+ /** The text channel for the player. */
1004
+ public textChannel: string | null = null;
1005
+ /**The now playing message. */
1006
+ public nowPlayingMessage?: Message;
1007
+ /** The current state of the player. */
1008
+ public state: State = "DISCONNECTED";
1009
+ /** The equalizer bands array. */
1010
+ public bands = new Array<number>(15).fill(0.0);
1011
+ /** The voice state object from Discord. */
1012
+ public voiceState: VoiceState;
1013
+ /** The Manager. */
1014
+ public manager: Manager;
1015
+ /** The autoplay state of the player. */
1016
+ public isAutoplay: boolean = false;
1017
+
1018
+ private static _manager: Manager;
1019
+ private readonly data: Record<string, unknown> = {};
1020
+ private dynamicLoopInterval: NodeJS.Timeout;
1021
+
1022
+ /**
1023
+ * Set custom data.
1024
+ * @param key
1025
+ * @param value
1026
+ */
1027
+ public set(key: string, value: unknown): void {
1028
+ this.data[key] = value;
1029
+ }
1030
+
1031
+ /**
1032
+ * Get custom data.
1033
+ * @param key
1034
+ */
1035
+ public get<T>(key: string): T {
1036
+ return this.data[key] as T;
1037
+ }
1038
+
1039
+ /** @hidden */
1040
+ public static init(manager: Manager): void {
1041
+ this._manager = manager;
1042
+ }
1043
+
1044
+ /**
1045
+ * Creates a new player, returns one if it already exists.
1046
+ * @param options
1047
+ */
1048
+ constructor(public options: PlayerOptions) {
1049
+ if (!this.manager) this.manager = Structure.get("Player")._manager;
1050
+ if (!this.manager) throw new RangeError("Manager has not been initiated.");
1051
+
1052
+ if (this.manager.players.has(options.guild)) {
1053
+ return this.manager.players.get(options.guild);
1054
+ }
1055
+
1056
+ playerCheck(options);
1057
+
1058
+ this.guild = options.guild;
1059
+ this.voiceState = Object.assign({
1060
+ op: "voiceUpdate",
1061
+ guild_id: options.guild,
1062
+ });
1063
+
1064
+ if (options.voiceChannel) this.voiceChannel = options.voiceChannel;
1065
+ if (options.textChannel) this.textChannel = options.textChannel;
1066
+
1067
+ const node = this.manager.nodes.get(options.node);
1068
+ this.node = node || this.manager.useableNodes;
1069
+
1070
+ if (!this.node) throw new RangeError("No available nodes.");
1071
+
1072
+ this.manager.players.set(options.guild, this);
1073
+ this.manager.emit("PlayerCreate", this);
1074
+ this.setVolume(options.volume ?? 100);
1075
+ this.filters = new Filters(this);
1076
+ }
1077
+
1078
+ /**
1079
+ * Same as Manager#search() but a shortcut on the player itself.
1080
+ * @param query
1081
+ * @param requester
1082
+ */
1083
+ public search(query: string | SearchQuery, requester?: User | ClientUser): Promise<SearchResult> {
1084
+ return this.manager.search(query, requester);
1085
+ }
1086
+
1087
+ /** Connect to the voice channel. */
1088
+ public connect(): this {
1089
+ if (!this.voiceChannel) throw new RangeError("No voice channel has been set.");
1090
+ this.state = "CONNECTING";
1091
+
1092
+ this.manager.options.send(this.guild, {
1093
+ op: 4,
1094
+ d: {
1095
+ guild_id: this.guild,
1096
+ channel_id: this.voiceChannel,
1097
+ self_mute: this.options.selfMute || false,
1098
+ self_deaf: this.options.selfDeafen || false,
1099
+ },
1100
+ });
1101
+
1102
+ this.state = "CONNECTED";
1103
+ return this;
1104
+ }
1105
+
1106
+ /**
1107
+ * Moves the player to a different node.
1108
+ *
1109
+ * @param {string} [node] - The ID of the node to move to.
1110
+ * @returns {this} - The player instance.
1111
+ */
1112
+ public async moveNode(node?: string): Promise<this> {
1113
+ node = node || this.manager.leastLoadNode.first().options.identifier || this.manager.nodes.filter((n) => n.connected).first().options.identifier;
1114
+ if (!this.manager.nodes.has(node)) throw new RangeError("No nodes available.");
1115
+ if (this.node.options.identifier === node) return this;
1116
+
1117
+ const destroyOldNode = async (node: Node) => {
1118
+ this.state = "MOVING";
1119
+
1120
+ if (this.manager.nodes.get(node.options.identifier) && this.manager.nodes.get(node.options.identifier).connected) await node.rest.destroyPlayer(this.guild);
1121
+
1122
+ setTimeout(() => (this.state = "CONNECTED"), 5000);
1123
+ };
1124
+
1125
+ const currentNode = this.node;
1126
+ const destinationNode = this.manager.nodes.get(node);
1127
+ let position = this.position;
1128
+
1129
+ if (currentNode.connected) {
1130
+ const fetchedPlayer = await currentNode.rest.get(`/v4/sessions/${currentNode.sessionId}/players/${this.guild}`) as { track?: { info: { position: number } } };
1131
+ position = fetchedPlayer.track.info.position;
1132
+ }
1133
+
1134
+ await destinationNode.rest.updatePlayer({
1135
+ guildId: this.guild,
1136
+ data: {
1137
+ encodedTrack: this.queue.current?.track,
1138
+ position: position,
1139
+ volume: this.volume,
1140
+ paused: this.paused,
1141
+ filters: {
1142
+ distortion: this.filters.distortion,
1143
+ equalizer: this.filters.equalizer,
1144
+ karaoke: this.filters.karaoke,
1145
+ rotation: this.filters.rotation,
1146
+ timescale: this.filters.timescale,
1147
+ vibrato: this.filters.vibrato,
1148
+ volume: this.filters.volume,
1149
+ },
1150
+ },
1151
+ });
1152
+
1153
+ await destinationNode.rest.updatePlayer({
1154
+ guildId: this.guild,
1155
+ data: {
1156
+ voice: {
1157
+ token: this.voiceState.event.token,
1158
+ endpoint: this.voiceState.event.endpoint,
1159
+ sessionId: this.voiceState?.sessionId ?? "",
1160
+ },
1161
+ },
1162
+ });
1163
+
1164
+ this.node = destinationNode;
1165
+ destroyOldNode(currentNode);
1166
+ return this;
1167
+ }
1168
+
1169
+ /** Disconnect from the voice channel. */
1170
+ public disconnect(): this {
1171
+ if (this.voiceChannel === null) return this;
1172
+ this.state = "DISCONNECTING";
1173
+
1174
+ this.pause(true);
1175
+ this.manager.options.send(this.guild, {
1176
+ op: 4,
1177
+ d: {
1178
+ guild_id: this.guild,
1179
+ channel_id: null,
1180
+ self_mute: false,
1181
+ self_deaf: false,
1182
+ },
1183
+ });
1184
+
1185
+ this.voiceChannel = null;
1186
+ this.state = "DISCONNECTED";
1187
+ return this;
1188
+ }
1189
+
1190
+ /** Destroys the player. */
1191
+ public destroy(disconnect = true): void {
1192
+ this.state = "DESTROYING";
1193
+
1194
+ if (disconnect) {
1195
+ this.disconnect();
1196
+ }
1197
+
1198
+ this.node.rest.destroyPlayer(this.guild);
1199
+ this.manager.emit("PlayerDestroy", this);
1200
+ this.manager.players.delete(this.guild);
1201
+ }
1202
+
1203
+ /**
1204
+ * Sets the player voice channel.
1205
+ * @param channel
1206
+ */
1207
+ public setVoiceChannel(channel: string): this {
1208
+ if (typeof channel !== "string") throw new TypeError("Channel must be a non-empty string.");
1209
+
1210
+ this.voiceChannel = channel;
1211
+ this.connect();
1212
+ return this;
1213
+ }
1214
+
1215
+ /**
1216
+ * Sets the player text channel.
1217
+ * @param channel
1218
+ */
1219
+ public setTextChannel(channel: string): this {
1220
+ if (typeof channel !== "string") throw new TypeError("Channel must be a non-empty string.");
1221
+
1222
+ this.textChannel = channel;
1223
+ return this;
1224
+ }
1225
+
1226
+ /** Sets the now playing message. */
1227
+ public setNowPlayingMessage(message: Message): Message {
1228
+ if (!message) {
1229
+ throw new TypeError("You must provide the message of the now playing message.");
1230
+ }
1231
+ return (this.nowPlayingMessage = message);
1232
+ }
1233
+
1234
+ /** Plays the next track. */
1235
+ public async play(): Promise<void>;
1236
+
1237
+ /**
1238
+ * Plays the specified track.
1239
+ * @param track
1240
+ */
1241
+ public async play(track: Track | UnresolvedTrack): Promise<void>;
1242
+
1243
+ /**
1244
+ * Plays the next track with some options.
1245
+ * @param options
1246
+ */
1247
+ public async play(options: PlayOptions): Promise<void>;
1248
+
1249
+
1250
+ /**
1251
+ * Plays the specified track with some options.
1252
+ * @param track
1253
+ * @param options
1254
+ */
1255
+ public async play(track: Track | UnresolvedTrack, options: PlayOptions): Promise<void>;
1256
+ public async play(optionsOrTrack?: PlayOptions | Track | UnresolvedTrack, playOptions?: PlayOptions): Promise<void> {
1257
+ if (typeof optionsOrTrack !== "undefined" && TrackUtils.validate(optionsOrTrack)) {
1258
+ if (this.queue.current) this.queue.previous = this.queue.current;
1259
+ this.queue.current = optionsOrTrack as Track;
1260
+ }
1261
+
1262
+ if (!this.queue.current) throw new RangeError("No current track.");
1263
+
1264
+ const finalOptions = playOptions
1265
+ ? playOptions
1266
+ : ["startTime", "endTime", "noReplace"].every((v) => Object.keys(optionsOrTrack || {}).includes(v))
1267
+ ? (optionsOrTrack as PlayOptions)
1268
+ : {};
1269
+
1270
+ if (TrackUtils.isUnresolvedTrack(this.queue.current)) {
1271
+ try {
1272
+ this.queue.current = await TrackUtils.getClosestTrack(this.queue.current as UnresolvedTrack);
1273
+ } catch (error) {
1274
+ this.manager.emit("TrackError", this, this.queue.current, error);
1275
+ if (this.queue[0]) return this.play(this.queue[0]);
1276
+ return;
1277
+ }
1278
+ }
1279
+
1280
+ await this.node.rest.updatePlayer({
1281
+ guildId: this.guild,
1282
+ data: {
1283
+ encodedTrack: this.queue.current?.track,
1284
+ ...finalOptions,
1285
+ },
1286
+ });
1287
+
1288
+ Object.assign(this, { position: 0, playing: true });
1289
+ }
1290
+
1291
+ /**
1292
+ * Sets the autoplay-state of the player.
1293
+ * @param autoplayState
1294
+ * @param botUser
1295
+ */
1296
+ public setAutoplay(autoplayState: boolean, botUser: object) {
1297
+ if (typeof autoplayState !== "boolean") {
1298
+ throw new TypeError("autoplayState must be a boolean.");
1299
+ }
1300
+
1301
+ if (typeof botUser !== "object") {
1302
+ throw new TypeError("botUser must be a user-object.");
1303
+ }
1304
+
1305
+ this.isAutoplay = autoplayState;
1306
+ this.set("Internal_BotUser", botUser);
1307
+
1308
+ return this;
1309
+ }
1310
+
1311
+
1312
+ /**
1313
+ * Gets recommended tracks and returns an array of tracks.
1314
+ * @param track
1315
+ * @param requester
1316
+ */
1317
+ public async getRecommended(track: Track, requester?: User | ClientUser) {
1318
+ const node = this.manager.useableNodes;
1319
+
1320
+ if (!node) {
1321
+ throw new Error("No available nodes.");
1322
+ }
1323
+
1324
+ const hasSpotifyURL = ["spotify.com", "open.spotify.com"].some((url) => track.uri.includes(url));
1325
+ const hasYouTubeURL = ["youtube.com", "youtu.be"].some((url) => track.uri.includes(url));
1326
+
1327
+ if (hasSpotifyURL) {
1328
+ const res = await node.rest.get(`/v4/info`);
1329
+ const info = res as LavalinkInfo;
1330
+
1331
+ const isSpotifyPluginEnabled = info.plugins.some((plugin: { name: string }) => plugin.name === "lavasrc-plugin");
1332
+ const isSpotifySourceManagerEnabled = info.sourceManagers.includes("spotify");
1333
+
1334
+ if (isSpotifyPluginEnabled && isSpotifySourceManagerEnabled) {
1335
+ const trackID = node.extractSpotifyTrackID(track.uri);
1336
+ const artistID = node.extractSpotifyArtistID(track.pluginInfo.artistUrl);
1337
+
1338
+ let identifier = "";
1339
+ if (trackID && artistID) {
1340
+ identifier = `sprec:seed_artists=${artistID}&seed_tracks=${trackID}`;
1341
+ } else if (trackID) {
1342
+ identifier = `sprec:seed_tracks=${trackID}`;
1343
+ } else if (artistID) {
1344
+ identifier = `sprec:seed_artists=${artistID}`;
1345
+ }
1346
+
1347
+ if (identifier) {
1348
+ const recommendedResult = (await node.rest.get(`/v4/loadtracks?identifier=${encodeURIComponent(identifier)}`)) as LavalinkResponse;
1349
+
1350
+ if (recommendedResult.loadType === "playlist") {
1351
+ const playlistData = recommendedResult.data as PlaylistRawData;
1352
+ const recommendedTracks = playlistData.tracks;
1353
+
1354
+ if (recommendedTracks) {
1355
+ const tracks = recommendedTracks.map((track) => TrackUtils.build(track, requester));
1356
+
1357
+ return tracks;
1358
+ }
1359
+ }
1360
+ }
1361
+ }
1362
+ }
1363
+
1364
+ let videoID = track.uri.substring(track.uri.indexOf("=") + 1);
1365
+
1366
+ if (!hasYouTubeURL) {
1367
+ const res = await this.manager.search(`${track.author} - ${track.title}`);
1368
+
1369
+ videoID = res.tracks[0].uri.substring(res.tracks[0].uri.indexOf("=") + 1);
1370
+ }
1371
+
1372
+ const searchURI = `https://www.youtube.com/watch?v=${videoID}&list=RD${videoID}`;
1373
+
1374
+ const res = await this.manager.search(searchURI);
1375
+
1376
+ if (res.loadType === "empty" || res.loadType === "error") return;
1377
+
1378
+ let tracks = res.tracks;
1379
+
1380
+ if (res.loadType === "playlist") {
1381
+ tracks = res.playlist.tracks;
1382
+ }
1383
+
1384
+ const filteredTracks = tracks.filter((track) => track.uri !== `https://www.youtube.com/watch?v=${videoID}`);
1385
+
1386
+ if (this.manager.options.replaceYouTubeCredentials) {
1387
+ for (const track of filteredTracks) {
1388
+ track.author = track.author.replace("- Topic", "");
1389
+ track.title = track.title.replace("Topic -", "");
1390
+
1391
+ if (track.title.includes("-")) {
1392
+ const [author, title] = track.title.split("-").map((str: string) => str.trim());
1393
+ track.author = author;
1394
+ track.title = title;
1395
+ }
1396
+ }
1397
+ }
1398
+
1399
+ return filteredTracks;
1400
+ }
1401
+
1402
+ /**
1403
+ * Sets the player volume.
1404
+ * @param volume
1405
+ */
1406
+ public setVolume(volume: number): this {
1407
+ if (isNaN(volume)) throw new TypeError("Volume must be a number.");
1408
+
1409
+ this.node.rest.updatePlayer({
1410
+ guildId: this.options.guild,
1411
+ data: {
1412
+ volume,
1413
+ },
1414
+ });
1415
+
1416
+ this.volume = volume;
1417
+
1418
+ return this;
1419
+ }
1420
+
1421
+ /**
1422
+ * Sets the track repeat.
1423
+ * @param repeat
1424
+ */
1425
+ public setTrackRepeat(repeat: boolean): this {
1426
+ if (typeof repeat !== "boolean") throw new TypeError('Repeat can only be "true" or "false".');
1427
+
1428
+ const oldPlayer = { ...this };
1429
+
1430
+ if (repeat) {
1431
+ this.trackRepeat = true;
1432
+ this.queueRepeat = false;
1433
+ this.dynamicRepeat = false;
1434
+ } else {
1435
+ this.trackRepeat = false;
1436
+ this.queueRepeat = false;
1437
+ this.dynamicRepeat = false;
1438
+ }
1439
+
1440
+ this.manager.emit("PlayerStateUpdate", oldPlayer, this);
1441
+ return this;
1442
+ }
1443
+
1444
+ /**
1445
+ * Sets the queue repeat.
1446
+ * @param repeat
1447
+ */
1448
+ public setQueueRepeat(repeat: boolean): this {
1449
+ if (typeof repeat !== "boolean") throw new TypeError('Repeat can only be "true" or "false".');
1450
+
1451
+ const oldPlayer = { ...this };
1452
+
1453
+ if (repeat) {
1454
+ this.trackRepeat = false;
1455
+ this.queueRepeat = true;
1456
+ this.dynamicRepeat = false;
1457
+ } else {
1458
+ this.trackRepeat = false;
1459
+ this.queueRepeat = false;
1460
+ this.dynamicRepeat = false;
1461
+ }
1462
+
1463
+ this.manager.emit("PlayerStateUpdate", oldPlayer, this);
1464
+ return this;
1465
+ }
1466
+
1467
+ /**
1468
+ * Sets the queue to repeat and shuffles the queue after each song.
1469
+ * @param repeat "true" or "false".
1470
+ * @param ms After how many milliseconds to trigger dynamic repeat.
1471
+ */
1472
+ public setDynamicRepeat(repeat: boolean, ms: number): this {
1473
+ if (typeof repeat !== "boolean") {
1474
+ throw new TypeError('Repeat can only be "true" or "false".');
1475
+ }
1476
+
1477
+ if (this.queue.size <= 1) {
1478
+ throw new RangeError("The queue size must be greater than 1.");
1479
+ }
1480
+
1481
+ const oldPlayer = { ...this };
1482
+
1483
+ if (repeat) {
1484
+ this.trackRepeat = false;
1485
+ this.queueRepeat = false;
1486
+ this.dynamicRepeat = true;
1487
+
1488
+ this.dynamicLoopInterval = setInterval(() => {
1489
+ if (!this.dynamicRepeat) return;
1490
+ const shuffled = _.shuffle(this.queue);
1491
+ this.queue.clear();
1492
+ shuffled.forEach((track) => {
1493
+ this.queue.add(track);
1494
+ });
1495
+ }, ms) as NodeJS.Timeout;
1496
+ } else {
1497
+ clearInterval(this.dynamicLoopInterval);
1498
+ this.trackRepeat = false;
1499
+ this.queueRepeat = false;
1500
+ this.dynamicRepeat = false;
1501
+ }
1502
+
1503
+ this.manager.emit("PlayerStateUpdate", oldPlayer, this);
1504
+ return this;
1505
+ }
1506
+
1507
+ /** Restarts the current track to the start. */
1508
+ public restart(): void {
1509
+ if (!this.queue.current?.track) {
1510
+ if (this.queue.length) this.play();
1511
+ return;
1512
+ }
1513
+
1514
+ this.node.rest.updatePlayer({
1515
+ guildId: this.guild,
1516
+ data: {
1517
+ position: 0,
1518
+ encodedTrack: this.queue.current?.track,
1519
+ },
1520
+ });
1521
+ }
1522
+
1523
+ /** Stops the current track, optionally give an amount to skip to, e.g 5 would play the 5th song. */
1524
+ public stop(amount?: number): this {
1525
+ if (typeof amount === "number" && amount > 1) {
1526
+ if (amount > this.queue.length) throw new RangeError("Cannot skip more than the queue length.");
1527
+ this.queue.splice(0, amount - 1);
1528
+ }
1529
+
1530
+ this.node.rest.updatePlayer({
1531
+ guildId: this.guild,
1532
+ data: {
1533
+ encodedTrack: null,
1534
+ },
1535
+ });
1536
+
1537
+ return this;
1538
+ }
1539
+
1540
+ /**
1541
+ * Pauses the current track.
1542
+ * @param pause
1543
+ */
1544
+ public pause(pause: boolean): this {
1545
+ if (typeof pause !== "boolean") throw new RangeError('Pause can only be "true" or "false".');
1546
+
1547
+ if (this.paused === pause || !this.queue.totalSize) return this;
1548
+
1549
+ const oldPlayer = { ...this };
1550
+
1551
+ this.playing = !pause;
1552
+ this.paused = pause;
1553
+
1554
+ this.node.rest.updatePlayer({
1555
+ guildId: this.guild,
1556
+ data: {
1557
+ paused: pause,
1558
+ },
1559
+ });
1560
+
1561
+ this.manager.emit("PlayerStateUpdate", oldPlayer, this);
1562
+ return this;
1563
+ }
1564
+
1565
+ /** Go back to the previous song. */
1566
+ public previous(): this {
1567
+ this.queue.unshift(this.queue.previous);
1568
+ this.stop();
1569
+
1570
+ return this;
1571
+ }
1572
+
1573
+ /**
1574
+ * Seeks to the position in the current track.
1575
+ * @param position
1576
+ */
1577
+ public seek(position: number): this {
1578
+ if (!this.queue.current) return undefined;
1579
+ position = Number(position);
1580
+
1581
+ if (isNaN(position)) throw new RangeError("Position must be a number.");
1582
+ if (position < 0 || position > this.queue.current.duration) position = Math.max(Math.min(position, this.queue.current.duration), 0);
1583
+
1584
+ this.position = position;
1585
+
1586
+ this.node.rest.updatePlayer({
1587
+ guildId: this.guild,
1588
+ data: {
1589
+ position: position,
1590
+ },
1591
+ });
1592
+
1593
+ return this;
1594
+ }
1595
+ }
1596
+
1597
+ export interface PlayerOptions {
1598
+ /** The guild the Player belongs to. */
1599
+ guild: string;
1600
+ /** The text channel the Player belongs to. */
1601
+ textChannel: string;
1602
+ /** The voice channel the Player belongs to. */
1603
+ voiceChannel?: string;
1604
+ /** The node the Player uses. */
1605
+ node?: string;
1606
+ /** The initial volume the Player will use. */
1607
+ volume?: number;
1608
+ /** If the player should mute itself. */
1609
+ selfMute?: boolean;
1610
+ /** If the player should deaf itself. */
1611
+ selfDeafen?: boolean;
1612
+ }
1613
+
1614
+ /** If track partials are set some of these will be `undefined` as they were removed. */
1615
+ export interface Track {
1616
+ /** The base64 encoded track. */
1617
+ readonly track: string;
1618
+ /** The artwork url of the track. */
1619
+ readonly artworkUrl: string;
1620
+ /** The track source name. */
1621
+ readonly sourceName: TrackSourceName;
1622
+ /** The title of the track. */
1623
+ title: string;
1624
+ /** The identifier of the track. */
1625
+ readonly identifier: string;
1626
+ /** The author of the track. */
1627
+ author: string;
1628
+ /** The duration of the track. */
1629
+ readonly duration: number;
1630
+ /** The ISRC of the track. */
1631
+ readonly isrc: string;
1632
+ /** If the track is seekable. */
1633
+ readonly isSeekable: boolean;
1634
+ /** If the track is a stream.. */
1635
+ readonly isStream: boolean;
1636
+ /** The uri of the track. */
1637
+ readonly uri: string;
1638
+ /** The thumbnail of the track or null if it's a unsupported source. */
1639
+ readonly thumbnail: string | null;
1640
+ /** The user that requested the track. */
1641
+ readonly requester: User | ClientUser | null;
1642
+ /** Displays the track thumbnail with optional size or null if it's a unsupported source. */
1643
+ displayThumbnail(size?: Sizes): string;
1644
+ /** Additional track info provided by plugins. */
1645
+ pluginInfo: TrackPluginInfo;
1646
+ /** Add your own data to the track. */
1647
+ customData: Record<string, unknown>;
1648
+ }
1649
+
1650
+ export interface TrackPluginInfo {
1651
+ albumName?: string;
1652
+ albumUrl?: string;
1653
+ artistArtworkUrl?: string;
1654
+ artistUrl?: string;
1655
+ isPreview?: string;
1656
+ previewUrl?: string;
1657
+ }
1658
+
1659
+ /** Unresolved tracks can't be played normally, they will resolve before playing into a Track. */
1660
+ export interface UnresolvedTrack extends Partial<Track> {
1661
+ /** The title to search against. */
1662
+ title: string;
1663
+ /** The author to search against. */
1664
+ author?: string;
1665
+ /** The duration to search within 1500 milliseconds of the results from YouTube. */
1666
+ duration?: number;
1667
+ /** Resolves into a Track. */
1668
+ resolve(): Promise<void>;
1669
+ }
1670
+
1671
+ export interface PlayOptions {
1672
+ /** The position to start the track. */
1673
+ readonly startTime?: number;
1674
+ /** The position to end the track. */
1675
+ readonly endTime?: number;
1676
+ /** Whether to not replace the track if a play payload is sent. */
1677
+ readonly noReplace?: boolean;
1678
+ }
1679
+
1680
+ export interface EqualizerBand {
1681
+ /** The band number being 0 to 14. */
1682
+ band: number;
1683
+ /** The gain amount being -0.25 to 1.00, 0.25 being double. */
1684
+ gain: number;
1685
+ }
1686
+
1687
+ }
1688
+
1689
+ src } Utils } Structures } Node.ts {
1690
+ import {
1691
+ PlayerEvent,
1692
+ PlayerEvents,
1693
+ Structure,
1694
+ TrackEndEvent,
1695
+ TrackExceptionEvent,
1696
+ TrackStartEvent,
1697
+ TrackStuckEvent,
1698
+ TrackUtils,
1699
+ WebSocketClosedEvent,
1700
+ } from "./Utils";
1701
+ import { LavalinkResponse, Manager, PlaylistRawData } from "./Manager";
1702
+ import { Player, Track, UnresolvedTrack } from "./Player";
1703
+ import { Rest } from "./Rest";
1704
+ import nodeCheck from "../Utils/NodeCheck";
1705
+ import WebSocket from "ws";
1706
+
1707
+ export class Node {
1708
+ /** The socket for the node. */
1709
+ public socket: WebSocket | null = null;
1710
+ /** The stats for the node. */
1711
+ public stats: NodeStats;
1712
+ public manager: Manager;
1713
+ /** The node's session ID. */
1714
+ public sessionId: string | null;
1715
+ /** The REST instance. */
1716
+ public readonly rest: Rest;
1717
+
1718
+ private static _manager: Manager;
1719
+ private reconnectTimeout?: NodeJS.Timeout;
1720
+ private reconnectAttempts = 1;
1721
+
1722
+ /** Returns if connected to the Node. */
1723
+ public get connected(): boolean {
1724
+ if (!this.socket) return false;
1725
+ return this.socket.readyState === WebSocket.OPEN;
1726
+ }
1727
+
1728
+ /** Returns the address for this node. */
1729
+ public get address(): string {
1730
+ return `${this.options.host}:${this.options.port}`;
1731
+ }
1732
+
1733
+ /** @hidden */
1734
+ public static init(manager: Manager): void {
1735
+ this._manager = manager;
1736
+ }
1737
+
1738
+ /**
1739
+ * Creates an instance of Node.
1740
+ * @param options
1741
+ */
1742
+ constructor(public options: NodeOptions) {
1743
+ if (!this.manager) this.manager = Structure.get("Node")._manager;
1744
+ if (!this.manager) throw new RangeError("Manager has not been initiated.");
1745
+
1746
+ if (this.manager.nodes.has(options.identifier || options.host)) {
1747
+ return this.manager.nodes.get(options.identifier || options.host);
1748
+ }
1749
+
1750
+ nodeCheck(options);
1751
+
1752
+ this.options = {
1753
+ port: 2333,
1754
+ password: "youshallnotpass",
1755
+ secure: false,
1756
+ retryAmount: 30,
1757
+ retryDelay: 60000,
1758
+ priority: 0,
1759
+ ...options,
1760
+ };
1761
+
1762
+ if (this.options.secure) {
1763
+ this.options.port = 443;
1764
+ }
1765
+
1766
+ this.options.identifier = options.identifier || options.host;
1767
+ this.stats = {
1768
+ players: 0,
1769
+ playingPlayers: 0,
1770
+ uptime: 0,
1771
+ memory: {
1772
+ free: 0,
1773
+ used: 0,
1774
+ allocated: 0,
1775
+ reservable: 0,
1776
+ },
1777
+ cpu: {
1778
+ cores: 0,
1779
+ systemLoad: 0,
1780
+ lavalinkLoad: 0,
1781
+ },
1782
+ frameStats: {
1783
+ sent: 0,
1784
+ nulled: 0,
1785
+ deficit: 0,
1786
+ },
1787
+ };
1788
+
1789
+ this.manager.nodes.set(this.options.identifier, this);
1790
+ this.manager.emit("NodeCreate", this);
1791
+ this.rest = new Rest(this);
1792
+ }
1793
+
1794
+ /** Connects to the Node. */
1795
+ public connect(): void {
1796
+ if (this.connected) return;
1797
+
1798
+ const headers = Object.assign({
1799
+ Authorization: this.options.password,
1800
+ "Num-Shards": String(this.manager.options.shards),
1801
+ "User-Id": this.manager.options.clientId,
1802
+ "Client-Name": this.manager.options.clientName,
1803
+ });
1804
+
1805
+ this.socket = new WebSocket(`ws${this.options.secure ? "s" : ""}://${this.address}/v4/websocket`, { headers });
1806
+ this.socket.on("open", this.open.bind(this));
1807
+ this.socket.on("close", this.close.bind(this));
1808
+ this.socket.on("message", this.message.bind(this));
1809
+ this.socket.on("error", this.error.bind(this));
1810
+ }
1811
+
1812
+ /** Destroys the Node and all players connected with it. */
1813
+ public destroy(): void {
1814
+ if (!this.connected) return;
1815
+
1816
+ const players = this.manager.players.filter((p) => p.node == this);
1817
+ if (players.size) players.forEach((p) => p.destroy());
1818
+
1819
+ this.socket.close(1000, "destroy");
1820
+ this.socket.removeAllListeners();
1821
+ this.socket = null;
1822
+
1823
+ this.reconnectAttempts = 1;
1824
+ clearTimeout(this.reconnectTimeout);
1825
+
1826
+ this.manager.emit("NodeDestroy", this);
1827
+ this.manager.destroyNode(this.options.identifier);
1828
+ }
1829
+
1830
+ private reconnect(): void {
1831
+ this.reconnectTimeout = setTimeout(() => {
1832
+ if (this.reconnectAttempts >= this.options.retryAmount) {
1833
+ const error = new Error(`Unable to connect after ${this.options.retryAmount} attempts.`);
1834
+
1835
+ this.manager.emit("NodeError", this, error);
1836
+ return this.destroy();
1837
+ }
1838
+ this.socket?.removeAllListeners();
1839
+ this.socket = null;
1840
+ this.manager.emit("NodeReconnect", this);
1841
+ this.connect();
1842
+ this.reconnectAttempts++;
1843
+ }, this.options.retryDelay) as unknown as NodeJS.Timeout;
1844
+ }
1845
+
1846
+ protected open(): void {
1847
+ if (this.reconnectTimeout) clearTimeout(this.reconnectTimeout);
1848
+ try {
1849
+ this.manager.emit("NodeConnect", this);
1850
+ } catch (err) {
1851
+ console.error("Error emitting NodeConnect event:", err);
1852
+ }
1853
+ }
1854
+
1855
+
1856
+ protected close(code: number, reason: string): void {
1857
+ this.manager.emit("NodeDisconnect", this, { code, reason });
1858
+ if (code !== 1000 || reason !== "destroy") this.reconnect();
1859
+ }
1860
+
1861
+ protected error(error: Error): void {
1862
+ if (!error) return;
1863
+ this.manager.emit("NodeError", this, error);
1864
+ }
1865
+
1866
+ protected message(d: Buffer | string): void {
1867
+ if (Array.isArray(d)) d = Buffer.concat(d);
1868
+ else if (d instanceof ArrayBuffer) d = Buffer.from(d);
1869
+
1870
+ const payload = JSON.parse(d.toString());
1871
+
1872
+ if (!payload.op) return;
1873
+ this.manager.emit("NodeRaw", payload);
1874
+
1875
+ let player: Player;
1876
+
1877
+ switch (payload.op) {
1878
+ case "stats":
1879
+ delete payload.op;
1880
+ this.stats = { ...payload } as unknown as NodeStats;
1881
+ break;
1882
+ case "playerUpdate":
1883
+ player = this.manager.players.get(payload.guildId);
1884
+ if (player) player.position = payload.state.position || 0;
1885
+ break;
1886
+ case "event":
1887
+ this.handleEvent(payload);
1888
+ break;
1889
+ case "ready":
1890
+ this.rest.setSessionId(payload.sessionId);
1891
+ this.sessionId = payload.sessionId;
1892
+
1893
+ if (this.options.resumeStatus) {
1894
+ this.rest.patch(`/v4/sessions/${this.sessionId}`, {
1895
+ resuming: this.options.resumeStatus,
1896
+ timeout: this.options.resumeTimeout,
1897
+ });
1898
+ }
1899
+ break;
1900
+ default:
1901
+ this.manager.emit("NodeError", this, new Error(`Unexpected op "${payload.op}" with data: ${payload.message}`));
1902
+ return;
1903
+ }
1904
+ }
1905
+
1906
+ protected async handleEvent(payload: PlayerEvent & PlayerEvents): Promise<void> {
1907
+ if (!payload.guildId) return;
1908
+
1909
+ const player = this.manager.players.get(payload.guildId);
1910
+ if (!player) return;
1911
+
1912
+ const track = player.queue.current;
1913
+ const type = payload.type;
1914
+
1915
+ let error: Error;
1916
+ switch (type) {
1917
+ case "TrackStartEvent":
1918
+ this.trackStart(player, track as Track, payload);
1919
+ break;
1920
+
1921
+ case "TrackEndEvent":
1922
+ if (player?.nowPlayingMessage && player?.nowPlayingMessage.deletable) {
1923
+ await player?.nowPlayingMessage?.delete().catch(() => {});
1924
+ }
1925
+
1926
+ this.trackEnd(player, track as Track, payload);
1927
+ break;
1928
+
1929
+ case "TrackStuckEvent":
1930
+ this.trackStuck(player, track as Track, payload);
1931
+ break;
1932
+
1933
+ case "TrackExceptionEvent":
1934
+ this.trackError(player, track, payload);
1935
+ break;
1936
+
1937
+ case "WebSocketClosedEvent":
1938
+ this.socketClosed(player, payload);
1939
+ break;
1940
+
1941
+ default:
1942
+ error = new Error(`Node#event unknown event '${type}'.`);
1943
+ this.manager.emit("NodeError", this, error);
1944
+ break;
1945
+ }
1946
+ }
1947
+
1948
+ protected trackStart(player: Player, track: Track, payload: TrackStartEvent): void {
1949
+ player.playing = true;
1950
+ player.paused = false;
1951
+ this.manager.emit("TrackStart", player, track, payload);
1952
+ }
1953
+
1954
+ protected async trackEnd(player: Player, track: Track, payload: TrackEndEvent): Promise<void> {
1955
+ const { reason } = payload;
1956
+
1957
+ // If the track failed to load or was cleaned up
1958
+ if (["loadFailed", "cleanup"].includes(reason)) {
1959
+ this.handleFailedTrack(player, track, payload);
1960
+ }
1961
+ // If the track was forcibly replaced
1962
+ else if (reason === "replaced") {
1963
+ this.manager.emit("TrackEnd", player, track, payload);
1964
+ player.queue.previous = player.queue.current;
1965
+ }
1966
+ // If the track ended and it's set to repeat (track or queue)
1967
+ else if (track && (player.trackRepeat || player.queueRepeat)) {
1968
+ this.handleRepeatedTrack(player, track, payload);
1969
+ }
1970
+ // If there's another track in the queue
1971
+ else if (player.queue.length) {
1972
+ this.playNextTrack(player, track, payload);
1973
+ }
1974
+ // If there are no more tracks in the queue
1975
+ else {
1976
+ await this.queueEnd(player, track, payload);
1977
+ }
1978
+ }
1979
+
1980
+ public extractSpotifyTrackID(url: string): string | null {
1981
+ const regex = /https:\/\/open\.spotify\.com\/track\/([a-zA-Z0-9]+)/;
1982
+ const match = url.match(regex);
1983
+ return match ? match[1] : null;
1984
+ }
1985
+
1986
+ public extractSpotifyArtistID(url: string): string | null {
1987
+ const regex = /https:\/\/open\.spotify\.com\/artist\/([a-zA-Z0-9]+)/;
1988
+ const match = url.match(regex);
1989
+ return match ? match[1] : null;
1990
+ }
1991
+
1992
+ // Handle autoplay
1993
+ private async handleAutoplay(player: Player, track: Track) {
1994
+ const previousTrack = player.queue.previous;
1995
+
1996
+ if (!player.isAutoplay || !previousTrack) return;
1997
+
1998
+ const hasSpotifyURL = ["spotify.com", "open.spotify.com"].some((url) => previousTrack.uri.includes(url));
1999
+
2000
+ if (hasSpotifyURL) {
2001
+ const node = this.manager.useableNodes;
2002
+
2003
+ const res = await node.rest.get(`/v4/info`);
2004
+ const info = res as LavalinkInfo;
2005
+
2006
+ const isSpotifyPluginEnabled = info.plugins.some((plugin: { name: string }) => plugin.name === "lavasrc-plugin");
2007
+ const isSpotifySourceManagerEnabled = info.sourceManagers.includes("spotify");
2008
+
2009
+ if (isSpotifyPluginEnabled && isSpotifySourceManagerEnabled) {
2010
+ const trackID = this.extractSpotifyTrackID(previousTrack.uri);
2011
+ const artistID = this.extractSpotifyArtistID(previousTrack.pluginInfo.artistUrl);
2012
+
2013
+ let identifier = "";
2014
+ if (trackID && artistID) {
2015
+ identifier = `sprec:seed_artists=${artistID}&seed_tracks=${trackID}`;
2016
+ } else if (trackID) {
2017
+ identifier = `sprec:seed_tracks=${trackID}`;
2018
+ } else if (artistID) {
2019
+ identifier = `sprec:seed_artists=${artistID}`;
2020
+ }
2021
+
2022
+ if (identifier) {
2023
+ const recommendedResult = (await node.rest.get(`/v4/loadtracks?identifier=${encodeURIComponent(identifier)}`)) as LavalinkResponse;
2024
+
2025
+ if (recommendedResult.loadType === "playlist") {
2026
+ const playlistData = recommendedResult.data as PlaylistRawData;
2027
+ const recommendedTrack = playlistData.tracks[0];
2028
+
2029
+ if (recommendedTrack) {
2030
+ player.queue.add(TrackUtils.build(recommendedTrack, player.get("Internal_BotUser")));
2031
+ player.play();
2032
+ return;
2033
+ }
2034
+ }
2035
+ }
2036
+ }
2037
+ }
2038
+
2039
+ const hasYouTubeURL = ["youtube.com", "youtu.be"].some((url) => previousTrack.uri.includes(url));
2040
+
2041
+ let videoID = previousTrack.uri.substring(previousTrack.uri.indexOf("=") + 1);
2042
+
2043
+ if (!hasYouTubeURL) {
2044
+ const res = await player.search(`${previousTrack.author} - ${previousTrack.title}`, player.get("Internal_BotUser"));
2045
+
2046
+ videoID = res.tracks[0].uri.substring(res.tracks[0].uri.indexOf("=") + 1);
2047
+ }
2048
+
2049
+ let randomIndex: number;
2050
+ let searchURI: string;
2051
+
2052
+ do {
2053
+ randomIndex = Math.floor(Math.random() * 23) + 2;
2054
+ searchURI = `https://www.youtube.com/watch?v=${videoID}&list=RD${videoID}&index=${randomIndex}`;
2055
+ } while (track.uri.includes(searchURI));
2056
+
2057
+ const res = await player.search(searchURI, player.get("Internal_BotUser"));
2058
+
2059
+ if (res.loadType === "empty" || res.loadType === "error") return;
2060
+
2061
+ let tracks = res.tracks;
2062
+
2063
+ if (res.loadType === "playlist") {
2064
+ tracks = res.playlist.tracks;
2065
+ }
2066
+
2067
+ const foundTrack = tracks.sort(() => Math.random() - 0.5).find((shuffledTrack) => shuffledTrack.uri !== track.uri);
2068
+
2069
+ if (foundTrack) {
2070
+ if (this.manager.options.replaceYouTubeCredentials) {
2071
+ foundTrack.author = foundTrack.author.replace("- Topic", "");
2072
+ foundTrack.title = foundTrack.title.replace("Topic -", "");
2073
+
2074
+ if (foundTrack.title.includes("-")) {
2075
+ const [author, title] = foundTrack.title.split("-").map((str: string) => str.trim());
2076
+ foundTrack.author = author;
2077
+ foundTrack.title = title;
2078
+ }
2079
+ }
2080
+ player.queue.add(foundTrack);
2081
+ player.play();
2082
+ }
2083
+ }
2084
+
2085
+ // Handle the case when a track failed to load or was cleaned up
2086
+ private handleFailedTrack(player: Player, track: Track, payload: TrackEndEvent): void {
2087
+ player.queue.previous = player.queue.current;
2088
+ player.queue.current = player.queue.shift();
2089
+
2090
+ if (!player.queue.current) {
2091
+ this.queueEnd(player, track, payload);
2092
+ return;
2093
+ }
2094
+
2095
+ this.manager.emit("TrackEnd", player, track, payload);
2096
+ if (this.manager.options.autoPlay) player.play();
2097
+ }
2098
+
2099
+ // Handle the case when a track ended and it's set to repeat (track or queue)
2100
+ private handleRepeatedTrack(player: Player, track: Track, payload: TrackEndEvent): void {
2101
+ const { queue, trackRepeat, queueRepeat } = player;
2102
+ const { autoPlay } = this.manager.options;
2103
+
2104
+ if (trackRepeat) {
2105
+ queue.unshift(queue.current);
2106
+ } else if (queueRepeat) {
2107
+ queue.add(queue.current);
2108
+ }
2109
+
2110
+ queue.previous = queue.current;
2111
+ queue.current = queue.shift();
2112
+
2113
+ this.manager.emit("TrackEnd", player, track, payload);
2114
+
2115
+ if (payload.reason === "stopped" && !(queue.current = queue.shift())) {
2116
+ this.queueEnd(player, track, payload);
2117
+ return;
2118
+ }
2119
+
2120
+ if (autoPlay) player.play();
2121
+ }
2122
+
2123
+ // Handle the case when there's another track in the queue
2124
+ private playNextTrack(player: Player, track: Track, payload: TrackEndEvent): void {
2125
+ player.queue.previous = player.queue.current;
2126
+ player.queue.current = player.queue.shift();
2127
+
2128
+ this.manager.emit("TrackEnd", player, track, payload);
2129
+ if (this.manager.options.autoPlay) player.play();
2130
+ }
2131
+
2132
+ protected async queueEnd(player: Player, track: Track, payload: TrackEndEvent): Promise<void> {
2133
+ player.queue.previous = player.queue.current;
2134
+ player.queue.current = null;
2135
+
2136
+ if (!player.isAutoplay) {
2137
+ player.queue.previous = player.queue.current;
2138
+ player.queue.current = null;
2139
+ player.playing = false;
2140
+ this.manager.emit("QueueEnd", player, track, payload);
2141
+ return;
2142
+ }
2143
+
2144
+ await this.handleAutoplay(player, track);
2145
+ }
2146
+
2147
+ protected trackStuck(player: Player, track: Track, payload: TrackStuckEvent): void {
2148
+ player.stop();
2149
+ this.manager.emit("TrackStuck", player, track, payload);
2150
+ }
2151
+
2152
+ protected trackError(player: Player, track: Track | UnresolvedTrack, payload: TrackExceptionEvent): void {
2153
+ player.stop();
2154
+ this.manager.emit("TrackError", player, track, payload);
2155
+ }
2156
+
2157
+ protected socketClosed(player: Player, payload: WebSocketClosedEvent): void {
2158
+ this.manager.emit("SocketClosed", player, payload);
2159
+ }
2160
+ }
2161
+
2162
+ export interface NodeOptions {
2163
+ /** The host for the node. */
2164
+ host: string;
2165
+ /** The port for the node. */
2166
+ port?: number;
2167
+ /** The password for the node. */
2168
+ password?: string;
2169
+ /** Whether the host uses SSL. */
2170
+ secure?: boolean;
2171
+ /** The identifier for the node. */
2172
+ identifier?: string;
2173
+ /** The retryAmount for the node. */
2174
+ retryAmount?: number;
2175
+ /** The retryDelay for the node. */
2176
+ retryDelay?: number;
2177
+ /** Whether to resume the previous session. */
2178
+ resumeStatus?: boolean;
2179
+ /** The time the manager will wait before trying to resume the previous session. */
2180
+ resumeTimeout?: number;
2181
+ /** The timeout used for api calls. */
2182
+ requestTimeout?: number;
2183
+ /** Priority of the node. */
2184
+ priority?: number;
2185
+ }
2186
+
2187
+ export interface NodeStats {
2188
+ /** The amount of players on the node. */
2189
+ players: number;
2190
+ /** The amount of playing players on the node. */
2191
+ playingPlayers: number;
2192
+ /** The uptime for the node. */
2193
+ uptime: number;
2194
+ /** The memory stats for the node. */
2195
+ memory: MemoryStats;
2196
+ /** The cpu stats for the node. */
2197
+ cpu: CPUStats;
2198
+ /** The frame stats for the node. */
2199
+ frameStats: FrameStats;
2200
+ }
2201
+
2202
+ export interface MemoryStats {
2203
+ /** The free memory of the allocated amount. */
2204
+ free: number;
2205
+ /** The used memory of the allocated amount. */
2206
+ used: number;
2207
+ /** The total allocated memory. */
2208
+ allocated: number;
2209
+ /** The reservable memory. */
2210
+ reservable: number;
2211
+ }
2212
+
2213
+ export interface CPUStats {
2214
+ /** The core amount the host machine has. */
2215
+ cores: number;
2216
+ /** The system load. */
2217
+ systemLoad: number;
2218
+ /** The lavalink load. */
2219
+ lavalinkLoad: number;
2220
+ }
2221
+
2222
+ export interface FrameStats {
2223
+ /** The amount of sent frames. */
2224
+ sent?: number;
2225
+ /** The amount of nulled frames. */
2226
+ nulled?: number;
2227
+ /** The amount of deficit frames. */
2228
+ deficit?: number;
2229
+ }
2230
+
2231
+ export interface LavalinkInfo {
2232
+ version: { semver: string; major: number; minor: number; patch: number; preRelease: string };
2233
+ buildTime: number;
2234
+ git: { branch: string; commit: string; commitTime: number };
2235
+ jvm: string;
2236
+ lavaplayer: string;
2237
+ sourceManagers: string[];
2238
+ filters: string[];
2239
+ plugins: { name: string; version: string }[];
2240
+ }
2241
+
2242
+ }
2243
+
2244
+ src } Utils } Structures } Manager.ts {
2245
+
2246
+ /* eslint-disable no-async-promise-executor */
2247
+ import { EventEmitter } from "events";
2248
+ import {
2249
+ LoadType,
2250
+ Plugin,
2251
+ Structure,
2252
+ TrackData,
2253
+ TrackEndEvent,
2254
+ TrackExceptionEvent,
2255
+ TrackStartEvent,
2256
+ TrackStuckEvent,
2257
+ TrackUtils,
2258
+ VoicePacket,
2259
+ VoiceServer,
2260
+ WebSocketClosedEvent,
2261
+ } from "./Utils";
2262
+ import { Collection } from "@discordjs/collection";
2263
+ import { Node, NodeOptions } from "./Node";
2264
+ import { Player, PlayerOptions, Track, UnresolvedTrack } from "./Player";
2265
+ import { VoiceState } from "..";
2266
+ import managerCheck from "../Utils/ManagerCheck";
2267
+ import { ClientUser, User } from "discord.js";
2268
+ import { TikTok } from "./TikTok";
2269
+
2270
+
2271
+
2272
+ /**
2273
+ * The main hub for interacting with Lavalink and using Magmastream,
2274
+ */
2275
+ export class Manager extends EventEmitter {
2276
+ public static readonly DEFAULT_SOURCES: Record<SearchPlatform, string> = {
2277
+ "youtube music": "ytmsearch",
2278
+ youtube: "ytsearch",
2279
+ spotify: "spsearch",
2280
+ jiosaavn: "jssearch",
2281
+ soundcloud: "scsearch",
2282
+ deezer: "dzsearch",
2283
+ tidal: "tdsearch",
2284
+ applemusic: "amsearch",
2285
+ bandcamp: "bcsearch",
2286
+ tiktok: "tiktok",
2287
+ };
2288
+
2289
+ /** The map of players. */
2290
+ public readonly players = new Collection<string, Player>();
2291
+ /** The map of nodes. */
2292
+ public readonly nodes = new Collection<string, Node>();
2293
+ /** The options that were set. */
2294
+ public readonly options: ManagerOptions;
2295
+ private initiated = false;
2296
+ public caches = new Collection<string, SearchResult>();
2297
+
2298
+ /** Returns the nodes that has the least load. */
2299
+ public get leastLoadNode(): Collection<string, Node> {
2300
+ return this.nodes
2301
+ .filter((node) => node.connected)
2302
+ .sort((a, b) => {
2303
+ const aload = a.stats.cpu ? (a.stats.cpu.lavalinkLoad / a.stats.cpu.cores) * 100 : 0;
2304
+ const bload = b.stats.cpu ? (b.stats.cpu.lavalinkLoad / b.stats.cpu.cores) * 100 : 0;
2305
+ return aload - bload;
2306
+ });
2307
+ }
2308
+
2309
+ /** Returns the nodes that has the least amount of players. */
2310
+ private get leastPlayersNode(): Collection<string, Node> {
2311
+ return this.nodes.filter((node) => node.connected).sort((a, b) => a.stats.players - b.stats.players);
2312
+ }
2313
+
2314
+
2315
+
2316
+ /** Returns a node based on priority. */
2317
+ private get priorityNode(): Node {
2318
+ const filteredNodes = this.nodes.filter((node) => node.connected && node.options.priority > 0);
2319
+ const totalWeight = filteredNodes.reduce((total, node) => total + node.options.priority, 0);
2320
+ const weightedNodes = filteredNodes.map((node) => ({
2321
+ node,
2322
+ weight: node.options.priority / totalWeight,
2323
+ }));
2324
+ const randomNumber = Math.random();
2325
+
2326
+ let cumulativeWeight = 0;
2327
+
2328
+ for (const { node, weight } of weightedNodes) {
2329
+ cumulativeWeight += weight;
2330
+ if (randomNumber <= cumulativeWeight) {
2331
+ return node;
2332
+ }
2333
+ }
2334
+
2335
+ return this.options.useNode === "leastLoad" ? this.leastLoadNode.first() : this.leastPlayersNode.first();
2336
+ }
2337
+
2338
+ /** Returns the node to use. */
2339
+ public get useableNodes(): Node {
2340
+ return this.options.usePriority ? this.priorityNode : this.options.useNode === "leastLoad" ? this.leastLoadNode.first() : this.leastPlayersNode.first();
2341
+ }
2342
+
2343
+ /**
2344
+ * Initiates the Manager class.
2345
+ * @param options
2346
+ */
2347
+ constructor(options: ManagerOptions) {
2348
+ super();
2349
+
2350
+ managerCheck(options);
2351
+ Structure.get("Player").init(this);
2352
+ Structure.get("Node").init(this);
2353
+ TrackUtils.init(this);
2354
+
2355
+ if (options.trackPartial) {
2356
+ TrackUtils.setTrackPartial(options.trackPartial);
2357
+ delete options.trackPartial;
2358
+ }
2359
+
2360
+ this.options = {
2361
+ plugins: [],
2362
+ nodes: [
2363
+ {
2364
+ identifier: "default",
2365
+ host: "localhost",
2366
+ resumeStatus: false,
2367
+ resumeTimeout: 1000,
2368
+ },
2369
+ ],
2370
+ shards: 1,
2371
+ autoPlay: true,
2372
+ usePriority: false,
2373
+ clientName: "For v3.0",
2374
+ defaultSearchPlatform: "youtube",
2375
+ useNode: "leastPlayers",
2376
+ ...options,
2377
+ };
2378
+
2379
+
2380
+
2381
+ if (this.options.plugins) {
2382
+ for (const [index, plugin] of this.options.plugins.entries()) {
2383
+ if (!(plugin instanceof Plugin)) throw new RangeError(`Plugin at index ${index} does not extend Plugin.`);
2384
+ plugin.load(this);
2385
+ }
2386
+ }
2387
+
2388
+ if (this.options.nodes) {
2389
+ for (const nodeOptions of this.options.nodes) {
2390
+ const node = new (Structure.get("Node"))(nodeOptions);
2391
+ this.nodes.set(node.options.identifier, node);
2392
+ }
2393
+ }
2394
+ setInterval(() => {
2395
+ this.caches.clear();
2396
+ }, this.options.caches.time);
2397
+ }
2398
+
2399
+
2400
+ /**
2401
+ * Initiates the Manager.
2402
+ * @param clientId
2403
+ */
2404
+ public init(clientId?: string): this {
2405
+ if (this.initiated) return this;
2406
+ if (typeof clientId !== "undefined") this.options.clientId = clientId;
2407
+
2408
+ if (typeof this.options.clientId !== "string") throw new Error('"clientId" set is not type of "string"');
2409
+
2410
+ if (!this.options.clientId) throw new Error('"clientId" is not set. Pass it in Manager#init() or as a option in the constructor.');
2411
+
2412
+ for (const node of this.nodes.values()) {
2413
+ try {
2414
+ node.connect();
2415
+ } catch (err) {
2416
+ this.emit("NodeError", node, err);
2417
+ }
2418
+ }
2419
+
2420
+ this.initiated = true;
2421
+ return this;
2422
+ }
2423
+
2424
+ /**
2425
+ * Searches the enabled sources based off the URL or the `source` property.
2426
+ * @param query
2427
+ * @param requester
2428
+ * @returns The search result.
2429
+ */
2430
+ public async search(query: string | SearchQuery, requester?: User | ClientUser): Promise<SearchResult> {
2431
+ const node = this.useableNodes;
2432
+
2433
+ if (!node) {
2434
+ throw new Error("No available nodes.");
2435
+ }
2436
+ if (this.options.caches.enabled && this.options.caches.time > 0 && typeof query === "string") {
2437
+ const data = this.caches.get(query);
2438
+ if (data) return data;
2439
+ }
2440
+ const _query: SearchQuery = typeof query === "string" ? { query } : query;
2441
+ const _source = Manager.DEFAULT_SOURCES[_query.source ?? this.options.defaultSearchPlatform] ?? _query.source;
2442
+ let search = _query.query;
2443
+
2444
+ if (!/^https?:\/\//.test(search)) {
2445
+ search = `${_source}:${search}`;
2446
+ }
2447
+
2448
+ if (typeof query !== "string") throw new TypeError("Query must be a string.");
2449
+
2450
+ if (TikTok.isTikTokURL(query)) {
2451
+ const tiktokResult = await TikTok.searchTikTok(query, requester);
2452
+ return {
2453
+ loadType: tiktokResult.loadType as LoadType,
2454
+ tracks: tiktokResult.tracks,
2455
+ };
2456
+ }
2457
+
2458
+ try {
2459
+ const res = (await node.rest.get(`/v4/loadtracks?identifier=${encodeURIComponent(search)}`)) as LavalinkResponse;
2460
+ if (!res) throw new Error("Query not found.");
2461
+
2462
+ let searchData = [];
2463
+ let playlistData: PlaylistRawData | undefined;
2464
+
2465
+ switch (res.loadType) {
2466
+ case "search":
2467
+ searchData = res.data as TrackData[];
2468
+ break;
2469
+
2470
+ case "track":
2471
+ searchData = [res.data as TrackData[]];
2472
+ break;
2473
+
2474
+ case "playlist":
2475
+ playlistData = res.data as PlaylistRawData;
2476
+ break;
2477
+ }
2478
+
2479
+ const tracks = searchData.map((track) => TrackUtils.build(track, requester));
2480
+ let playlist = null;
2481
+
2482
+ if (res.loadType === "playlist") {
2483
+ playlist = {
2484
+ name: playlistData!.info.name,
2485
+ tracks: playlistData!.tracks.map((track) => TrackUtils.build(track, requester)),
2486
+ duration: playlistData!.tracks.reduce((acc, cur) => acc + (cur.info.length || 0), 0),
2487
+ };
2488
+ }
2489
+
2490
+ const result: SearchResult = {
2491
+ loadType: res.loadType,
2492
+ tracks,
2493
+ playlist,
2494
+ };
2495
+
2496
+ if (this.options.replaceYouTubeCredentials) {
2497
+ let tracksToReplace: Track[] = [];
2498
+ if (result.loadType === "playlist") {
2499
+ tracksToReplace = result.playlist.tracks;
2500
+ } else {
2501
+ tracksToReplace = result.tracks;
2502
+ }
2503
+
2504
+ for (const track of tracksToReplace) {
2505
+ if (isYouTubeURL(track.uri)) {
2506
+ track.author = track.author.replace("- Topic", "");
2507
+ track.title = track.title.replace("Topic -", "");
2508
+ }
2509
+ if (track.title.includes("-")) {
2510
+ const [author, title] = track.title.split("-").map((str: string) => str.trim());
2511
+ track.author = author;
2512
+ track.title = title;
2513
+ }
2514
+ }
2515
+ }
2516
+ if (this.options.caches.enabled && this.options.caches.time > 0) this.caches.set(search, result);
2517
+ return result;
2518
+ } catch (err) {
2519
+ throw new Error(err);
2520
+ }
2521
+
2522
+ function isYouTubeURL(uri: string): boolean {
2523
+ return uri.includes("youtube.com") || uri.includes("youtu.be");
2524
+ }
2525
+ }
2526
+
2527
+ /**
2528
+ * Decodes the base64 encoded tracks and returns a TrackData array.
2529
+ * @param tracks
2530
+ */
2531
+ public decodeTracks(tracks: string[]): Promise<TrackData[]> {
2532
+ return new Promise(async(resolve, reject) => {
2533
+ const node = this.nodes.first();
2534
+ if (!node) {
2535
+ return reject(new Error("No available nodes."));
2536
+ }
2537
+
2538
+ await node.rest.post("/v4/decodetracks", JSON.stringify(tracks))
2539
+ .then((res) => {
2540
+ if (!res) return reject(new Error("No data returned from query."));
2541
+ resolve(res as TrackData[]);
2542
+ })
2543
+ .catch((err) => reject(err));
2544
+ });
2545
+ }
2546
+
2547
+ /**
2548
+ * Decodes the base64 encoded track and returns a TrackData.
2549
+ * @param track
2550
+ */
2551
+ public async decodeTrack(track: string): Promise<TrackData> {
2552
+ const res = await this.decodeTracks([track]);
2553
+ return res[0];
2554
+ }
2555
+
2556
+ /**
2557
+ * Creates a player or returns one if it already exists.
2558
+ * @param options
2559
+ */
2560
+ public create(options: PlayerOptions): Player {
2561
+ if (this.players.has(options.guild)) {
2562
+ return this.players.get(options.guild);
2563
+ }
2564
+
2565
+ return new (Structure.get("Player"))(options);
2566
+ }
2567
+
2568
+ /**
2569
+ * Returns a player or undefined if it does not exist.
2570
+ * @param guild
2571
+ */
2572
+ public get(guild: string): Player | undefined {
2573
+ return this.players.get(guild);
2574
+ }
2575
+
2576
+ /**
2577
+ * Destroys a player if it exists.
2578
+ * @param guild
2579
+ */
2580
+ public destroy(guild: string): void {
2581
+ this.players.delete(guild);
2582
+ }
2583
+
2584
+ /**
2585
+ * Creates a node or returns one if it already exists.
2586
+ * @param options
2587
+ */
2588
+ public createNode(options: NodeOptions): Node {
2589
+ if (this.nodes.has(options.identifier || options.host)) {
2590
+ return this.nodes.get(options.identifier || options.host);
2591
+ }
2592
+
2593
+ return new (Structure.get("Node"))(options);
2594
+ }
2595
+
2596
+ /**
2597
+ * Destroys a node if it exists.
2598
+ * @param identifier
2599
+ */
2600
+ public destroyNode(identifier: string): void {
2601
+ const node = this.nodes.get(identifier);
2602
+ if (!node) return;
2603
+ node.destroy();
2604
+ this.nodes.delete(identifier);
2605
+ }
2606
+
2607
+ /**
2608
+ * Sends voice data to the Lavalink server.
2609
+ * @param data
2610
+ */
2611
+ public async updateVoiceState(data: VoicePacket | VoiceServer | VoiceState): Promise<void> {
2612
+ if ("t" in data && !["VOICE_STATE_UPDATE", "VOICE_SERVER_UPDATE"].includes(data.t)) return;
2613
+
2614
+ const update = "d" in data ? data.d : data;
2615
+
2616
+ if (!update || (!("token" in update) && !("session_id" in update))) return;
2617
+
2618
+ const player = this.players.get(update.guild_id);
2619
+
2620
+ if (!player) return;
2621
+ if ("token" in update) {
2622
+ player.voiceState.event = update;
2623
+
2624
+ const {
2625
+ sessionId,
2626
+ event: { token, endpoint },
2627
+ } = player.voiceState;
2628
+
2629
+ await player.node.rest.updatePlayer({
2630
+ guildId: player.guild,
2631
+ data: { voice: { token, endpoint, sessionId } },
2632
+ });
2633
+
2634
+ return;
2635
+ }
2636
+
2637
+ if (update.user_id !== this.options.clientId) return;
2638
+ if (update.channel_id) {
2639
+ if (player.voiceChannel !== update.channel_id) {
2640
+ this.emit("PlayerMove", player, player.voiceChannel, update.channel_id);
2641
+ }
2642
+
2643
+ player.voiceState.sessionId = update.session_id;
2644
+ player.voiceChannel = update.channel_id;
2645
+ return;
2646
+ }
2647
+
2648
+ this.emit("PlayerDisconnect", player, player.voiceChannel);
2649
+ player.voiceChannel = null;
2650
+ player.voiceState = Object.assign({});
2651
+ player.destroy();
2652
+ return;
2653
+ }
2654
+ }
2655
+
2656
+ export interface Payload {
2657
+ /** The OP code */
2658
+ op: number;
2659
+ d: {
2660
+ guild_id: string;
2661
+ channel_id: string | null;
2662
+ self_mute: boolean;
2663
+ self_deaf: boolean;
2664
+ };
2665
+ }
2666
+
2667
+ export interface ManagerOptions {
2668
+ /** Use priority mode over least amount of player or load? */
2669
+ usePriority?: boolean;
2670
+ /** Use the least amount of players or least load? */
2671
+ useNode?: "leastLoad" | "leastPlayers";
2672
+ /** The array of nodes to connect to. */
2673
+ nodes?: NodeOptions[];
2674
+ /** The client ID to use. */
2675
+ clientId?: string;
2676
+ /** Value to use for the `Client-Name` header. */
2677
+ clientName?: string;
2678
+ /** The shard count. */
2679
+ shards?: number;
2680
+ /** A array of plugins to use. */
2681
+ plugins?: Plugin[];
2682
+ /** Whether players should automatically play the next song. */
2683
+ autoPlay?: boolean;
2684
+ /** An array of track properties to keep. `track` will always be present. */
2685
+ trackPartial?: string[];
2686
+ /** The default search platform to use, can be "youtube", "youtube music", "soundcloud" or deezer. */
2687
+ defaultSearchPlatform?: SearchPlatform;
2688
+ /** Whether the YouTube video titles should be replaced if the Author does not exactly match. */
2689
+ replaceYouTubeCredentials?: boolean;
2690
+ caches: {
2691
+ /** Whether to cache the search results. */
2692
+ enabled: boolean;
2693
+ /** The time to cache the search results. */
2694
+ time: number;
2695
+ }
2696
+ /**
2697
+ * Function to send data to the websocket.
2698
+ * @param id
2699
+ * @param payload
2700
+ */
2701
+ send(id: string, payload: Payload): void;
2702
+ }
2703
+
2704
+ export type SearchPlatform = "deezer" | "soundcloud" | "youtube music" | "youtube" | "spotify" | "jiosaavn" | "tidal" | "applemusic" | "bandcamp" | "tiktok";
2705
+
2706
+ export interface SearchQuery {
2707
+ /** The source to search from. */
2708
+ source?: SearchPlatform | string;
2709
+ /** The query to search for. */
2710
+ query: string;
2711
+ }
2712
+
2713
+ export interface LavalinkResponse {
2714
+ loadType: LoadType;
2715
+ data: TrackData[] | PlaylistRawData;
2716
+ }
2717
+
2718
+ export interface SearchResult {
2719
+ /** The load type of the result. */
2720
+ loadType: LoadType;
2721
+ /** The array of tracks from the result. */
2722
+ tracks: Track[];
2723
+ /** The playlist info if the load type is 'playlist'. */
2724
+ playlist?: PlaylistData;
2725
+ }
2726
+
2727
+ export interface PlaylistRawData {
2728
+ info: {
2729
+ /** The playlist name. */
2730
+ name: string;
2731
+ };
2732
+ /** Addition info provided by plugins. */
2733
+ pluginInfo: object;
2734
+ /** The tracks of the playlist */
2735
+ tracks: TrackData[];
2736
+ }
2737
+
2738
+ export interface PlaylistData {
2739
+ /** The playlist name. */
2740
+ name: string;
2741
+ /** The length of the playlist. */
2742
+ duration: number;
2743
+ /** The songs of the playlist. */
2744
+ tracks: Track[];
2745
+ }
2746
+
2747
+ export interface ManagerEvents {
2748
+ NodeCreate: (node: Node) => void;
2749
+ NodeDestroy: (node: Node) => void;
2750
+ NodeConnect: (node: Node) => void;
2751
+ NodeReconnect: (node: Node) => void;
2752
+ NodeDisconnect: (node: Node, reason: { code?: number; reason?: string }) => void;
2753
+ NodeError: (node: Node, error: Error) => void;
2754
+ NodeRaw: (payload: unknown) => void;
2755
+ PlayerCreate: (player: Player) => void;
2756
+ PlayerDestroy: (player: Player) => void;
2757
+ PlayerStateUpdate: (oldPlayer: Player, newPlayer: Player) => void;
2758
+ PlayerMove: (player: Player, initChannel: string, newChannel: string) => void;
2759
+ PlayerDisconnect: (player: Player, oldChannel: string) => void;
2760
+ QueueEnd: (player: Player, track: Track | UnresolvedTrack, payload: TrackEndEvent) => void;
2761
+ SocketClosed: (player: Player, payload: WebSocketClosedEvent) => void;
2762
+ TrackStart: (player: Player, track: Track, payload: TrackStartEvent) => void;
2763
+ TrackEnd: (player: Player, track: Track, payload: TrackEndEvent) => void;
2764
+ TrackStuck: (player: Player, track: Track, payload: TrackStuckEvent) => void;
2765
+ TrackError: (player: Player, track: Track | UnresolvedTrack, payload: TrackExceptionEvent) => void;
2766
+ }
2767
+
2768
+ }
2769
+
2770
+ src } Utils } Structures } Filters.ts {
2771
+ import { Band, bassBoostEqualizer, softEqualizer, trebleBassEqualizer, tvEqualizer, vaporwaveEqualizer } from "../Utils/FiltersEqualizers";
2772
+ import { Player } from "./Player";
2773
+
2774
+ export class Filters {
2775
+ public distortion: DistortionOptions | null;
2776
+ public equalizer: Band[];
2777
+ public karaoke: KaraokeOptions | null;
2778
+ public player: Player;
2779
+ public rotation: RotationOptions | null;
2780
+ public timescale: TimescaleOptions | null;
2781
+ public vibrato: VibratoOptions | null;
2782
+ public volume: number;
2783
+
2784
+ private filterStatus: {
2785
+ [key: string]: boolean;
2786
+ };
2787
+
2788
+ constructor(player: Player) {
2789
+ this.distortion = null;
2790
+ this.equalizer = [];
2791
+ this.karaoke = null;
2792
+ this.player = player;
2793
+ this.rotation = null;
2794
+ this.timescale = null;
2795
+ this.vibrato = null;
2796
+ this.volume = 1.0;
2797
+ // Initialize filter status
2798
+ this.filterStatus = {
2799
+ bassboost: false,
2800
+ distort: false,
2801
+ eightD: false,
2802
+ karaoke: false,
2803
+ nightcore: false,
2804
+ slowmo: false,
2805
+ soft: false,
2806
+ trebleBass: false,
2807
+ tv: false,
2808
+ vaporwave: false,
2809
+ };
2810
+ }
2811
+
2812
+ private async updateFilters(): Promise<this> {
2813
+ const { distortion, equalizer, karaoke, rotation, timescale, vibrato, volume } = this;
2814
+
2815
+ await this.player.node.rest.updatePlayer({
2816
+ data: {
2817
+ filters: {
2818
+ distortion,
2819
+ equalizer,
2820
+ karaoke,
2821
+ rotation,
2822
+ timescale,
2823
+ vibrato,
2824
+ volume,
2825
+ },
2826
+ },
2827
+ guildId: this.player.guild,
2828
+ });
2829
+
2830
+ return this;
2831
+ }
2832
+
2833
+ private applyFilter<T extends keyof Filters>(filter: { property: T; value: Filters[T] }, updateFilters: boolean = true): this {
2834
+ this[filter.property] = filter.value as this[T];
2835
+ if (updateFilters) {
2836
+ this.updateFilters();
2837
+ }
2838
+ return this;
2839
+ }
2840
+
2841
+ private setFilterStatus(filter: keyof AvailableFilters, status: boolean): this {
2842
+ this.filterStatus[filter] = status;
2843
+ return this;
2844
+ }
2845
+
2846
+ /**
2847
+ * Sets the equalizer bands and updates the filters.
2848
+ * @param bands - The equalizer bands.
2849
+ */
2850
+ public setEqualizer(bands?: Band[]): this {
2851
+ return this.applyFilter({ property: "equalizer", value: bands });
2852
+ }
2853
+
2854
+ /** Applies the eight dimension audio effect. */
2855
+ public eightD(): this {
2856
+ return this.setRotation({ rotationHz: 0.2 }).setFilterStatus("eightD", true);
2857
+ }
2858
+
2859
+ /** Applies the bass boost effect. */
2860
+ public bassBoost(): this {
2861
+ return this.setEqualizer(bassBoostEqualizer).setFilterStatus("bassboost", true);
2862
+ }
2863
+
2864
+ /** Applies the nightcore effect. */
2865
+ public nightcore(): this {
2866
+ return this.setTimescale({
2867
+ speed: 1.1,
2868
+ pitch: 1.125,
2869
+ rate: 1.05,
2870
+ }).setFilterStatus("nightcore", true);
2871
+ }
2872
+
2873
+ /** Applies the slow motion audio effect. */
2874
+ public slowmo(): this {
2875
+ return this.setTimescale({
2876
+ speed: 0.7,
2877
+ pitch: 1.0,
2878
+ rate: 0.8,
2879
+ }).setFilterStatus("slowmo", true);
2880
+ }
2881
+
2882
+ /** Applies the soft audio effect. */
2883
+ public soft(): this {
2884
+ return this.setEqualizer(softEqualizer).setFilterStatus("soft", true);
2885
+ }
2886
+
2887
+ /** Applies the television audio effect. */
2888
+ public tv(): this {
2889
+ return this.setEqualizer(tvEqualizer).setFilterStatus("tv", true);
2890
+ }
2891
+
2892
+ /** Applies the treble bass effect. */
2893
+ public trebleBass(): this {
2894
+ return this.setEqualizer(trebleBassEqualizer).setFilterStatus("trebleBass", true);
2895
+ }
2896
+
2897
+ /** Applies the vaporwave effect. */
2898
+ public vaporwave(): this {
2899
+ return this.setEqualizer(vaporwaveEqualizer).setTimescale({ pitch: 0.55 }).setFilterStatus("vaporwave", true);
2900
+ }
2901
+
2902
+ /** Applies the distortion audio effect. */
2903
+ public distort(): this {
2904
+ return this.setDistortion({
2905
+ sinOffset: 0,
2906
+ sinScale: 0.2,
2907
+ cosOffset: 0,
2908
+ cosScale: 0.2,
2909
+ tanOffset: 0,
2910
+ tanScale: 0.2,
2911
+ offset: 0,
2912
+ scale: 1.2,
2913
+ }).setFilterStatus("distort", true);
2914
+ }
2915
+
2916
+ /** Applies the karaoke options specified by the filter. */
2917
+ public setKaraoke(karaoke?: KaraokeOptions): this {
2918
+ return this.applyFilter({
2919
+ property: "karaoke",
2920
+ value: karaoke,
2921
+ }).setFilterStatus("karaoke", true);
2922
+ }
2923
+
2924
+ /** Applies the timescale options specified by the filter. */
2925
+ public setTimescale(timescale?: TimescaleOptions): this {
2926
+ return this.applyFilter({ property: "timescale", value: timescale });
2927
+ }
2928
+
2929
+ /** Applies the vibrato options specified by the filter. */
2930
+ public setVibrato(vibrato?: VibratoOptions): this {
2931
+ return this.applyFilter({ property: "vibrato", value: vibrato });
2932
+ }
2933
+
2934
+ /** Applies the rotation options specified by the filter. */
2935
+ public setRotation(rotation?: RotationOptions): this {
2936
+ return this.applyFilter({ property: "rotation", value: rotation });
2937
+ }
2938
+
2939
+ /** Applies the distortion options specified by the filter. */
2940
+ public setDistortion(distortion?: DistortionOptions): this {
2941
+ return this.applyFilter({ property: "distortion", value: distortion });
2942
+ }
2943
+
2944
+ /** Removes the audio effects and resets the filter status. */
2945
+ public async clearFilters(): Promise<this> {
2946
+ this.filterStatus = {
2947
+ bassboost: false,
2948
+ distort: false,
2949
+ eightD: false,
2950
+ karaoke: false,
2951
+ nightcore: false,
2952
+ slowmo: false,
2953
+ soft: false,
2954
+ trebleBass: false,
2955
+ tv: false,
2956
+ vaporwave: false,
2957
+ };
2958
+
2959
+ this.player.filters = new Filters(this.player);
2960
+ this.setEqualizer([]);
2961
+ this.setDistortion(null);
2962
+ this.setKaraoke(null);
2963
+ this.setRotation(null);
2964
+ this.setTimescale(null);
2965
+ this.setVibrato(null);
2966
+
2967
+ await this.updateFilters();
2968
+ return this;
2969
+ }
2970
+
2971
+ /** Returns the status of the specified filter . */
2972
+ public getFilterStatus(filter: keyof AvailableFilters): boolean {
2973
+ return this.filterStatus[filter];
2974
+ }
2975
+ }
2976
+
2977
+ /** Options for adjusting the timescale of audio. */
2978
+ interface TimescaleOptions {
2979
+ /** The speed factor for the timescale. */
2980
+ speed?: number;
2981
+ /** The pitch factor for the timescale. */
2982
+ pitch?: number;
2983
+ /** The rate factor for the timescale. */
2984
+ rate?: number;
2985
+ }
2986
+
2987
+ /** Options for applying vibrato effect to audio. */
2988
+ interface VibratoOptions {
2989
+ /** The frequency of the vibrato effect. */
2990
+ frequency: number;
2991
+ /** * The depth of the vibrato effect.*/
2992
+ depth: number;
2993
+ }
2994
+
2995
+ /** Options for applying rotation effect to audio. */
2996
+ interface RotationOptions {
2997
+ /** The rotation speed in Hertz (Hz). */
2998
+ rotationHz: number;
2999
+ }
3000
+
3001
+ /** Options for applying karaoke effect to audio. */
3002
+ interface KaraokeOptions {
3003
+ /** The level of karaoke effect. */
3004
+ level?: number;
3005
+ /** The mono level of karaoke effect. */
3006
+ monoLevel?: number;
3007
+ /** The filter band of karaoke effect. */
3008
+ filterBand?: number;
3009
+ /** The filter width of karaoke effect. */
3010
+ filterWidth?: number;
3011
+ }
3012
+
3013
+ interface DistortionOptions {
3014
+ sinOffset?: number;
3015
+ sinScale?: number;
3016
+ cosOffset?: number;
3017
+ cosScale?: number;
3018
+ tanOffset?: number;
3019
+ tanScale?: number;
3020
+ offset?: number;
3021
+ scale?: number;
3022
+ }
3023
+
3024
+ interface AvailableFilters {
3025
+ bassboost: boolean;
3026
+ distort: boolean;
3027
+ eightD: boolean;
3028
+ karaoke: boolean;
3029
+ nightcore: boolean;
3030
+ slowmo: boolean;
3031
+ soft: boolean;
3032
+ trebleBass: boolean;
3033
+ tv: boolean;
3034
+ vaporwave: boolean;
3035
+ }
3036
+
3037
+ }