shoukaku-bun 4.2.0-b

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.
@@ -0,0 +1,543 @@
1
+ import { OpCodes, State } from '../Constants';
2
+ import type { Node } from '../node/Node';
3
+ import type { Exception, Track, UpdatePlayerInfo, UpdatePlayerOptions } from '../node/Rest';
4
+ import { TypedEventEmitter } from '../Utils';
5
+ import { Connection } from './Connection';
6
+
7
+ export type TrackEndReason = 'finished' | 'loadFailed' | 'stopped' | 'replaced' | 'cleanup';
8
+ export type PlayOptions = Omit<UpdatePlayerOptions, 'filters' | 'voice'>;
9
+ export type ResumeOptions = Omit<UpdatePlayerOptions, 'track' | 'filters' | 'voice'>;
10
+
11
+ export enum PlayerEventType {
12
+ TRACK_START_EVENT = 'TrackStartEvent',
13
+ TRACK_END_EVENT = 'TrackEndEvent',
14
+ TRACK_EXCEPTION_EVENT = 'TrackExceptionEvent',
15
+ TRACK_STUCK_EVENT = 'TrackStuckEvent',
16
+ WEBSOCKET_CLOSED_EVENT = 'WebSocketClosedEvent'
17
+ }
18
+
19
+ export interface Band {
20
+ band: number;
21
+ gain: number;
22
+ }
23
+
24
+ export interface KaraokeSettings {
25
+ level?: number;
26
+ monoLevel?: number;
27
+ filterBand?: number;
28
+ filterWidth?: number;
29
+ }
30
+
31
+ export interface TimescaleSettings {
32
+ speed?: number;
33
+ pitch?: number;
34
+ rate?: number;
35
+ }
36
+
37
+ export interface FreqSettings {
38
+ frequency?: number;
39
+ depth?: number;
40
+ }
41
+
42
+ export interface RotationSettings {
43
+ rotationHz?: number;
44
+ }
45
+
46
+ export interface DistortionSettings {
47
+ sinOffset?: number;
48
+ sinScale?: number;
49
+ cosOffset?: number;
50
+ cosScale?: number;
51
+ tanOffset?: number;
52
+ tanScale?: number;
53
+ offset?: number;
54
+ scale?: number;
55
+ }
56
+
57
+ export interface ChannelMixSettings {
58
+ leftToLeft?: number;
59
+ leftToRight?: number;
60
+ rightToLeft?: number;
61
+ rightToRight?: number;
62
+ }
63
+
64
+ export interface LowPassSettings {
65
+ smoothing?: number;
66
+ }
67
+
68
+ export interface PlayerEvent {
69
+ op: OpCodes.EVENT;
70
+ guildId: string;
71
+ }
72
+
73
+ export interface TrackStartEvent extends PlayerEvent {
74
+ type: PlayerEventType.TRACK_START_EVENT;
75
+ track: Track;
76
+ }
77
+
78
+ export interface TrackEndEvent extends PlayerEvent {
79
+ type: PlayerEventType.TRACK_END_EVENT;
80
+ track: Track;
81
+ reason: TrackEndReason;
82
+ }
83
+
84
+ export interface TrackStuckEvent extends PlayerEvent {
85
+ type: PlayerEventType.TRACK_STUCK_EVENT;
86
+ track: Track;
87
+ thresholdMs: number;
88
+ }
89
+
90
+ export interface TrackExceptionEvent extends PlayerEvent {
91
+ type: PlayerEventType.TRACK_EXCEPTION_EVENT;
92
+ exception: Exception;
93
+ }
94
+
95
+ export interface WebSocketClosedEvent extends PlayerEvent {
96
+ type: PlayerEventType.WEBSOCKET_CLOSED_EVENT;
97
+ code: number;
98
+ byRemote: boolean;
99
+ reason: string;
100
+ }
101
+
102
+ export interface PlayerUpdate {
103
+ op: OpCodes.PLAYER_UPDATE;
104
+ state: {
105
+ connected: boolean;
106
+ position: number;
107
+ time: number;
108
+ ping: number;
109
+ };
110
+ guildId: string;
111
+ }
112
+
113
+ export interface FilterOptions {
114
+ volume?: number;
115
+ equalizer?: Band[];
116
+ karaoke?: KaraokeSettings | null;
117
+ timescale?: TimescaleSettings | null;
118
+ tremolo?: FreqSettings | null;
119
+ vibrato?: FreqSettings | null;
120
+ rotation?: RotationSettings | null;
121
+ distortion?: DistortionSettings | null;
122
+ channelMix?: ChannelMixSettings | null;
123
+ lowPass?: LowPassSettings | null;
124
+ }
125
+
126
+ // Interfaces are not final, but types are, and therefore has an index signature
127
+ // https://stackoverflow.com/a/64970740
128
+ // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
129
+ export type PlayerEvents = {
130
+ /**
131
+ * Emitted when the current playing track ends
132
+ * @eventProperty
133
+ */
134
+ 'end': [reason: TrackEndEvent];
135
+ /**
136
+ * Emitted when the current playing track gets stuck due to an error
137
+ * @eventProperty
138
+ */
139
+ 'stuck': [data: TrackStuckEvent];
140
+ /**
141
+ * Emitted when the current websocket connection is closed
142
+ * @eventProperty
143
+ */
144
+ 'closed': [reason: WebSocketClosedEvent];
145
+ /**
146
+ * Emitted when a new track starts
147
+ * @eventProperty
148
+ */
149
+ 'start': [data: TrackStartEvent];
150
+ /**
151
+ * Emitted when there is an error caused by the current playing track
152
+ * @eventProperty
153
+ */
154
+ 'exception': [reason: TrackExceptionEvent];
155
+ /**
156
+ * Emitted when the library manages to resume the player
157
+ * @eventProperty
158
+ */
159
+ 'resumed': [player: Player];
160
+ /**
161
+ * Emitted when a playerUpdate even is received from Lavalink
162
+ * @eventProperty
163
+ */
164
+ 'update': [data: PlayerUpdate];
165
+ };
166
+
167
+ /**
168
+ * Wrapper object around Lavalink
169
+ */
170
+ export class Player extends TypedEventEmitter<PlayerEvents> {
171
+ /**
172
+ * GuildId of this player
173
+ */
174
+ public readonly guildId: string;
175
+ /**
176
+ * Lavalink node this player is connected to
177
+ */
178
+ public node: Node;
179
+ /**
180
+ * Base64 encoded data of the current track
181
+ */
182
+ public track: string | null;
183
+ /**
184
+ * Global volume of the player
185
+ */
186
+ public volume: number;
187
+ /**
188
+ * Pause status in current player
189
+ */
190
+ public paused: boolean;
191
+ /**
192
+ * Ping represents the number of milliseconds between heartbeat and ack. Could be `-1` if not connected
193
+ */
194
+ public ping: number;
195
+ /**
196
+ * Position in ms of current track
197
+ */
198
+ public position: number;
199
+ /**
200
+ * Filters on current track
201
+ */
202
+ public filters: FilterOptions;
203
+
204
+ constructor(guildId: string, node: Node) {
205
+ super();
206
+ this.guildId = guildId;
207
+ this.node = node;
208
+ this.track = null;
209
+ this.volume = 100;
210
+ this.paused = false;
211
+ this.position = 0;
212
+ this.ping = 0;
213
+ this.filters = {};
214
+ }
215
+
216
+ public get data(): UpdatePlayerInfo {
217
+ const connection = this.node.manager.connections.get(this.guildId)!;
218
+ return {
219
+ guildId: this.guildId,
220
+ playerOptions: {
221
+ track: {
222
+ encoded: this.track
223
+ },
224
+ position: this.position,
225
+ paused: this.paused,
226
+ filters: this.filters,
227
+ voice: {
228
+ token: connection.serverUpdate!.token,
229
+ endpoint: connection.serverUpdate!.endpoint,
230
+ sessionId: connection.sessionId!
231
+ },
232
+ volume: this.volume
233
+ }
234
+ };
235
+ }
236
+
237
+ /**
238
+ * Move player to another node
239
+ * @param name Name of node to move to, or the default ideal node
240
+ * @returns true if the player was moved, false if not
241
+ */
242
+ public async move(name?: string): Promise<boolean> {
243
+ const connection = this.node.manager.connections.get(this.guildId);
244
+ const node = this.node.manager.nodes.get(name!) ?? this.node.manager.getIdealNode(connection);
245
+
246
+ if (!node && ![ ...this.node.manager.nodes.values() ].some(node => node.state === State.CONNECTED))
247
+ throw new Error('No available nodes to move to');
248
+
249
+ if (!node || node.name === this.node.name || node.state !== State.CONNECTED) return false;
250
+
251
+ let lastNode = this.node.manager.nodes.get(this.node.name);
252
+ if (!lastNode || lastNode.state !== State.CONNECTED)
253
+ lastNode = this.node.manager.getIdealNode(connection);
254
+
255
+ await this.destroy();
256
+
257
+ try {
258
+ this.node = node;
259
+ await this.resume();
260
+ return true;
261
+ } catch {
262
+ this.node = lastNode!;
263
+ await this.resume();
264
+ return false;
265
+ }
266
+ }
267
+
268
+ /**
269
+ * Destroys the player in remote lavalink side
270
+ */
271
+ public async destroy(): Promise<void> {
272
+ await this.node.rest.destroyPlayer(this.guildId);
273
+ }
274
+
275
+ /**
276
+ * Play a new track
277
+ */
278
+ public playTrack(playerOptions: PlayOptions, noReplace = false): Promise<void> {
279
+ return this.update(playerOptions, noReplace);
280
+ }
281
+
282
+ /**
283
+ * Stop the currently playing track
284
+ */
285
+ public stopTrack(): Promise<void> {
286
+ return this.update({ track: { encoded: null }, position: 0 });
287
+ }
288
+
289
+ /**
290
+ * Pause or unpause the currently playing track
291
+ * @param paused Boolean value to specify whether to pause or unpause the current bot user
292
+ */
293
+ public setPaused(paused = true): Promise<void> {
294
+ return this.update({ paused });
295
+ }
296
+
297
+ /**
298
+ * Seek to a specific time in the currently playing track
299
+ * @param position Position to seek to in milliseconds
300
+ */
301
+ public seekTo(position: number): Promise<void> {
302
+ return this.update({ position });
303
+ }
304
+
305
+ /**
306
+ * Sets the global volume of the player
307
+ * @param volume Target volume 0-1000
308
+ */
309
+ public setGlobalVolume(volume: number): Promise<void> {
310
+ return this.update({ volume });
311
+ }
312
+
313
+ /**
314
+ * Sets the filter volume of the player
315
+ * @param volume Target volume 0.0-5.0
316
+ */
317
+ async setFilterVolume(volume: number): Promise<void> {
318
+ return this.setFilters({ volume });
319
+ }
320
+
321
+ /**
322
+ * Change the equalizer settings applied to the currently playing track
323
+ * @param equalizer An array of objects that conforms to the Bands type that define volumes at different frequencies
324
+ */
325
+ public async setEqualizer(equalizer: Band[]): Promise<void> {
326
+ return this.setFilters({ equalizer });
327
+ }
328
+
329
+ /**
330
+ * Change the karaoke settings applied to the currently playing track
331
+ * @param karaoke An object that conforms to the KaraokeSettings type that defines a range of frequencies to mute
332
+ */
333
+ public setKaraoke(karaoke?: KaraokeSettings): Promise<void> {
334
+ return this.setFilters({ karaoke: karaoke ?? null });
335
+ }
336
+
337
+ /**
338
+ * Change the timescale settings applied to the currently playing track
339
+ * @param timescale An object that conforms to the TimescaleSettings type that defines the time signature to play the audio at
340
+ */
341
+ public setTimescale(timescale?: TimescaleSettings): Promise<void> {
342
+ return this.setFilters({ timescale: timescale ?? null });
343
+ }
344
+
345
+ /**
346
+ * Change the tremolo settings applied to the currently playing track
347
+ * @param tremolo An object that conforms to the FreqSettings type that defines an oscillation in volume
348
+ */
349
+ public setTremolo(tremolo?: FreqSettings): Promise<void> {
350
+ return this.setFilters({ tremolo: tremolo ?? null });
351
+ }
352
+
353
+ /**
354
+ * Change the vibrato settings applied to the currently playing track
355
+ * @param vibrato An object that conforms to the FreqSettings type that defines an oscillation in pitch
356
+ */
357
+ public setVibrato(vibrato?: FreqSettings): Promise<void> {
358
+ return this.setFilters({ vibrato: vibrato ?? null });
359
+ }
360
+
361
+ /**
362
+ * Change the rotation settings applied to the currently playing track
363
+ * @param rotation An object that conforms to the RotationSettings type that defines the frequency of audio rotating round the listener
364
+ */
365
+ public setRotation(rotation?: RotationSettings): Promise<void> {
366
+ return this.setFilters({ rotation: rotation ?? null });
367
+ }
368
+
369
+ /**
370
+ * Change the distortion settings applied to the currently playing track
371
+ * @param distortion An object that conforms to DistortionSettings that defines distortions in the audio
372
+ * @returns The current player instance
373
+ */
374
+ public setDistortion(distortion?: DistortionSettings): Promise<void> {
375
+ return this.setFilters({ distortion: distortion ?? null });
376
+ }
377
+
378
+ /**
379
+ * Change the channel mix settings applied to the currently playing track
380
+ * @param channelMix An object that conforms to ChannelMixSettings that defines how much the left and right channels affect each other (setting all factors to 0.5 causes both channels to get the same audio)
381
+ */
382
+ public setChannelMix(channelMix?: ChannelMixSettings): Promise<void> {
383
+ return this.setFilters({ channelMix: channelMix ?? null });
384
+ }
385
+
386
+ /**
387
+ * Change the low pass settings applied to the currently playing track
388
+ * @param lowPass An object that conforms to LowPassSettings that defines the amount of suppression on higher frequencies
389
+ */
390
+ public setLowPass(lowPass?: LowPassSettings): Promise<void> {
391
+ return this.setFilters({ lowPass: lowPass ?? null });
392
+ }
393
+
394
+ /**
395
+ * Change the all filter settings applied to the currently playing track
396
+ * @param filters An object that conforms to FilterOptions that defines all filters to apply/modify
397
+ */
398
+ public setFilters(filters: FilterOptions): Promise<void> {
399
+ return this.update({ filters });
400
+ }
401
+
402
+ /**
403
+ * Clear all filters applied to the currently playing track
404
+ */
405
+ public clearFilters(): Promise<void> {
406
+ return this.setFilters({
407
+ volume: 1,
408
+ equalizer: [],
409
+ karaoke: null,
410
+ timescale: null,
411
+ tremolo: null,
412
+ vibrato: null,
413
+ rotation: null,
414
+ distortion: null,
415
+ channelMix: null,
416
+ lowPass: null
417
+ });
418
+ }
419
+
420
+ /**
421
+ * Resumes the current track
422
+ * @param options An object that conforms to ResumeOptions that specify behavior on resuming
423
+ * @param noReplace Set it to true if you don't want to replace the currently playing track
424
+ */
425
+ public async resume(options: ResumeOptions = {}, noReplace = false): Promise<void> {
426
+ const data = this.data;
427
+
428
+ if (typeof options.position === 'number')
429
+ data.playerOptions.position = options.position;
430
+ if (typeof options.endTime === 'number')
431
+ data.playerOptions.endTime = options.endTime;
432
+ if (typeof options.paused === 'boolean')
433
+ data.playerOptions.paused = options.paused;
434
+ if (typeof options.volume === 'number')
435
+ data.playerOptions.volume = options.volume;
436
+
437
+ await this.update(data.playerOptions, noReplace);
438
+
439
+ this.emit('resumed', this);
440
+ }
441
+
442
+ /**
443
+ * If you want to update the whole player yourself, sends raw update player info to lavalink
444
+ * @param playerOptions Options to update the player data
445
+ * @param noReplace Set it to true if you don't want to replace the currently playing track
446
+ */
447
+ public async update(playerOptions: UpdatePlayerOptions, noReplace = false): Promise<void> {
448
+ const data = {
449
+ guildId: this.guildId,
450
+ noReplace,
451
+ playerOptions
452
+ };
453
+
454
+ await this.node.rest.updatePlayer(data);
455
+
456
+ if (!noReplace) this.paused = false;
457
+
458
+ if (playerOptions.filters) {
459
+ this.filters = { ...this.filters, ...playerOptions.filters };
460
+ }
461
+
462
+ if (typeof playerOptions.track !== 'undefined')
463
+ this.track = playerOptions.track.encoded ?? null;
464
+ if (typeof playerOptions.paused === 'boolean')
465
+ this.paused = playerOptions.paused;
466
+ if (typeof playerOptions.volume === 'number')
467
+ this.volume = playerOptions.volume;
468
+ if (typeof playerOptions.position === 'number')
469
+ this.position = playerOptions.position;
470
+ }
471
+
472
+ /**
473
+ * Cleans this player instance
474
+ * @internal
475
+ */
476
+ public clean(): void {
477
+ this.removeAllListeners();
478
+ this.track = null;
479
+ this.volume = 100;
480
+ this.position = 0;
481
+ this.filters = {};
482
+ }
483
+
484
+ /**
485
+ * Sends server update to lavalink
486
+ * @internal
487
+ */
488
+ public async sendServerUpdate(connection: Connection): Promise<void> {
489
+ const playerUpdate = {
490
+ guildId: this.guildId,
491
+ playerOptions: {
492
+ voice: {
493
+ token: connection.serverUpdate!.token,
494
+ endpoint: connection.serverUpdate!.endpoint,
495
+ sessionId: connection.sessionId!
496
+ }
497
+ }
498
+ };
499
+ await this.node.rest.updatePlayer(playerUpdate);
500
+ }
501
+
502
+ /**
503
+ * Handle player update data
504
+ */
505
+ public onPlayerUpdate(json: PlayerUpdate): void {
506
+ const { position, ping } = json.state;
507
+ this.position = position;
508
+ this.ping = ping;
509
+ this.emit('update', json);
510
+ }
511
+
512
+ /**
513
+ * Handle player events received from Lavalink
514
+ * @param json JSON data from Lavalink
515
+ * @internal
516
+ */
517
+ public onPlayerEvent(json: TrackStartEvent | TrackEndEvent | TrackStuckEvent | TrackExceptionEvent | WebSocketClosedEvent): void {
518
+ switch (json.type) {
519
+ case PlayerEventType.TRACK_START_EVENT:
520
+ if (this.track) this.track = json.track.encoded;
521
+ this.emit('start', json);
522
+ break;
523
+ case PlayerEventType.TRACK_END_EVENT:
524
+ this.emit('end', json);
525
+ break;
526
+ case PlayerEventType.TRACK_STUCK_EVENT:
527
+ this.emit('stuck', json);
528
+ break;
529
+ case PlayerEventType.TRACK_EXCEPTION_EVENT:
530
+ this.emit('exception', json);
531
+ break;
532
+ case PlayerEventType.WEBSOCKET_CLOSED_EVENT:
533
+ this.emit('closed', json);
534
+ break;
535
+ default:
536
+ this.node.manager.emit(
537
+ 'debug',
538
+ this.node.name,
539
+ `[Player] -> [Node] : Unknown Player Event Type, Data => ${JSON.stringify(json)}`
540
+ );
541
+ }
542
+ }
543
+ }