ziplayer 0.2.7 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AI-Guide.md +624 -956
- package/README.md +277 -10
- package/dist/extensions/BaseExtension.d.ts +1 -0
- package/dist/extensions/BaseExtension.d.ts.map +1 -1
- package/dist/extensions/BaseExtension.js.map +1 -1
- package/dist/extensions/index.d.ts +38 -3
- package/dist/extensions/index.d.ts.map +1 -1
- package/dist/extensions/index.js +259 -41
- package/dist/extensions/index.js.map +1 -1
- package/dist/persistence/PersistenceManager.d.ts +95 -0
- package/dist/persistence/PersistenceManager.d.ts.map +1 -0
- package/dist/persistence/PersistenceManager.js +975 -0
- package/dist/persistence/PersistenceManager.js.map +1 -0
- package/dist/plugins/BasePlugin.js +1 -1
- package/dist/plugins/BasePlugin.js.map +1 -1
- package/dist/plugins/index.d.ts +74 -8
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +657 -116
- package/dist/plugins/index.js.map +1 -1
- package/dist/structures/FilterManager.js +3 -3
- package/dist/structures/FilterManager.js.map +1 -1
- package/dist/structures/PersistenceManager.d.ts +96 -0
- package/dist/structures/PersistenceManager.d.ts.map +1 -0
- package/dist/structures/PersistenceManager.js +1008 -0
- package/dist/structures/PersistenceManager.js.map +1 -0
- package/dist/structures/Player.d.ts +158 -14
- package/dist/structures/Player.d.ts.map +1 -1
- package/dist/structures/Player.js +1175 -188
- package/dist/structures/Player.js.map +1 -1
- package/dist/structures/PlayerManager.d.ts +106 -91
- package/dist/structures/PlayerManager.d.ts.map +1 -1
- package/dist/structures/PlayerManager.js +365 -124
- package/dist/structures/PlayerManager.js.map +1 -1
- package/dist/structures/Queue.d.ts +136 -31
- package/dist/structures/Queue.d.ts.map +1 -1
- package/dist/structures/Queue.js +265 -46
- package/dist/structures/Queue.js.map +1 -1
- package/dist/structures/StreamManager.d.ts +137 -0
- package/dist/structures/StreamManager.d.ts.map +1 -0
- package/dist/structures/StreamManager.js +420 -0
- package/dist/structures/StreamManager.js.map +1 -0
- package/dist/types/index.d.ts +181 -8
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/persistence.d.ts +77 -0
- package/dist/types/persistence.d.ts.map +1 -0
- package/dist/types/persistence.js +3 -0
- package/dist/types/persistence.js.map +1 -0
- package/package.json +3 -2
- package/src/extensions/BaseExtension.ts +1 -0
- package/src/extensions/index.ts +320 -37
- package/src/plugins/BasePlugin.ts +1 -1
- package/src/plugins/index.ts +809 -139
- package/src/structures/FilterManager.ts +3 -3
- package/src/structures/Player.ts +2810 -1693
- package/src/structures/PlayerManager.ts +438 -129
- package/src/structures/Queue.ts +300 -55
- package/src/structures/StreamManager.ts +524 -0
- package/src/types/extension.ts +129 -129
- package/src/types/fillter.ts +264 -264
- package/src/types/index.ts +187 -12
- package/src/types/plugin.ts +59 -59
- package/tsconfig.json +0 -1
package/src/structures/Player.ts
CHANGED
|
@@ -1,1693 +1,2810 @@
|
|
|
1
|
-
import { EventEmitter } from "events";
|
|
2
|
-
import {
|
|
3
|
-
createAudioPlayer,
|
|
4
|
-
createAudioResource,
|
|
5
|
-
entersState,
|
|
6
|
-
AudioPlayerStatus,
|
|
7
|
-
VoiceConnection,
|
|
8
|
-
AudioPlayer as DiscordAudioPlayer,
|
|
9
|
-
VoiceConnectionStatus,
|
|
10
|
-
NoSubscriberBehavior,
|
|
11
|
-
joinVoiceChannel,
|
|
12
|
-
AudioResource,
|
|
13
|
-
StreamType,
|
|
14
|
-
} from "@discordjs/voice";
|
|
15
|
-
|
|
16
|
-
import { Readable } from "stream";
|
|
17
|
-
import
|
|
18
|
-
import type {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
import {
|
|
38
|
-
|
|
39
|
-
import {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* await player.
|
|
64
|
-
*
|
|
65
|
-
* //
|
|
66
|
-
* player.
|
|
67
|
-
* player.
|
|
68
|
-
* player.
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
* //
|
|
73
|
-
* player.
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
* });
|
|
80
|
-
*
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
public
|
|
89
|
-
public
|
|
90
|
-
public
|
|
91
|
-
public
|
|
92
|
-
public
|
|
93
|
-
public
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
private
|
|
103
|
-
private
|
|
104
|
-
private
|
|
105
|
-
private
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
private
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
this.
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
*
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
this.
|
|
318
|
-
this.
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
*
|
|
378
|
-
*
|
|
379
|
-
*
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
if (
|
|
498
|
-
this.queue
|
|
499
|
-
this.
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
this.
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
//
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
this.
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
*
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
this.
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
if (
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
return
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
};
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
}
|
|
1325
|
-
|
|
1326
|
-
/**
|
|
1327
|
-
*
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
this.
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
}
|
|
1468
|
-
|
|
1469
|
-
//
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
this.
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
this.
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
this.
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
}
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
this.
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
*
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1
|
+
import { EventEmitter } from "events";
|
|
2
|
+
import {
|
|
3
|
+
createAudioPlayer,
|
|
4
|
+
createAudioResource,
|
|
5
|
+
entersState,
|
|
6
|
+
AudioPlayerStatus,
|
|
7
|
+
VoiceConnection,
|
|
8
|
+
AudioPlayer as DiscordAudioPlayer,
|
|
9
|
+
VoiceConnectionStatus,
|
|
10
|
+
NoSubscriberBehavior,
|
|
11
|
+
joinVoiceChannel,
|
|
12
|
+
AudioResource,
|
|
13
|
+
StreamType,
|
|
14
|
+
} from "@discordjs/voice";
|
|
15
|
+
|
|
16
|
+
import { Readable } from "stream";
|
|
17
|
+
import { LRUCache } from "lru-cache";
|
|
18
|
+
import type { BaseExtension } from "../extensions";
|
|
19
|
+
import type {
|
|
20
|
+
Track,
|
|
21
|
+
PlayerOptions,
|
|
22
|
+
PlayerEvents,
|
|
23
|
+
SourcePlugin,
|
|
24
|
+
SearchResult,
|
|
25
|
+
ProgressBarOptions,
|
|
26
|
+
LoopMode,
|
|
27
|
+
StreamInfo,
|
|
28
|
+
SaveOptions,
|
|
29
|
+
VoiceChannel,
|
|
30
|
+
PlayerSession,
|
|
31
|
+
ExtensionPlayRequest,
|
|
32
|
+
ExtensionPlayResponse,
|
|
33
|
+
ExtensionAfterPlayPayload,
|
|
34
|
+
PreloadState,
|
|
35
|
+
StreamSlot,
|
|
36
|
+
} from "../types";
|
|
37
|
+
import type { PlayerManager } from "./PlayerManager";
|
|
38
|
+
|
|
39
|
+
import { Queue } from "./Queue";
|
|
40
|
+
import { PluginManager } from "../plugins";
|
|
41
|
+
import { ExtensionManager } from "../extensions";
|
|
42
|
+
import { withTimeout } from "../utils/timeout";
|
|
43
|
+
import { FilterManager } from "./FilterManager";
|
|
44
|
+
import { StreamManager } from "./StreamManager";
|
|
45
|
+
|
|
46
|
+
export declare interface Player {
|
|
47
|
+
on<K extends keyof PlayerEvents>(event: K, listener: (...args: PlayerEvents[K]) => void): this;
|
|
48
|
+
emit<K extends keyof PlayerEvents>(event: K, ...args: PlayerEvents[K]): boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Represents a music player for a specific Discord guild.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* // Create and configure player
|
|
56
|
+
* const player = await manager.create(guildId, {
|
|
57
|
+
* tts: { interrupt: true, volume: 1 },
|
|
58
|
+
* leaveOnEnd: true,
|
|
59
|
+
* leaveTimeout: 30000
|
|
60
|
+
* });
|
|
61
|
+
*
|
|
62
|
+
* // Connect to voice channel
|
|
63
|
+
* await player.connect(voiceChannel);
|
|
64
|
+
*
|
|
65
|
+
* // Play different types of content
|
|
66
|
+
* await player.play("Never Gonna Give You Up", userId); // Search query
|
|
67
|
+
* await player.play("https://youtube.com/watch?v=dQw4w9WgXcQ", userId); // Direct URL
|
|
68
|
+
* await player.play("tts: Hello everyone!", userId); // Text-to-Speech
|
|
69
|
+
*
|
|
70
|
+
* // Player controls
|
|
71
|
+
* player.pause(); // Pause current track
|
|
72
|
+
* player.resume(); // Resume paused track
|
|
73
|
+
* player.skip(); // Skip to next track
|
|
74
|
+
* player.stop(); // Stop and clear queue
|
|
75
|
+
* player.setVolume(0.5); // Set volume to 50%
|
|
76
|
+
*
|
|
77
|
+
* // Event handling
|
|
78
|
+
* player.on("trackStart", (player, track) => {
|
|
79
|
+
* console.log(`Now playing: ${track.title}`);
|
|
80
|
+
* });
|
|
81
|
+
*
|
|
82
|
+
* player.on("queueEnd", (player) => {
|
|
83
|
+
* console.log("Queue finished");
|
|
84
|
+
* });
|
|
85
|
+
*
|
|
86
|
+
*/
|
|
87
|
+
export class Player extends EventEmitter {
|
|
88
|
+
public readonly guildId: string;
|
|
89
|
+
public connection: VoiceConnection | null = null;
|
|
90
|
+
public audioPlayer: DiscordAudioPlayer;
|
|
91
|
+
public queue: Queue;
|
|
92
|
+
public volume: number = 100;
|
|
93
|
+
public isPlaying: boolean = false;
|
|
94
|
+
public isPaused: boolean = false;
|
|
95
|
+
public options: PlayerOptions;
|
|
96
|
+
public pluginManager: PluginManager;
|
|
97
|
+
public extensionManager: ExtensionManager;
|
|
98
|
+
public streamManager: StreamManager;
|
|
99
|
+
|
|
100
|
+
public userdata?: Record<string, any>;
|
|
101
|
+
public _lastActivity: number = Date.now();
|
|
102
|
+
private manager: PlayerManager;
|
|
103
|
+
private leaveTimeout: NodeJS.Timeout | null = null;
|
|
104
|
+
private currentResource: AudioResource | null = null;
|
|
105
|
+
private volumeInterval: NodeJS.Timeout | null = null;
|
|
106
|
+
private stuckTimer: NodeJS.Timeout | null = null;
|
|
107
|
+
|
|
108
|
+
private skipLoop = false;
|
|
109
|
+
private filter!: FilterManager;
|
|
110
|
+
private refreshLock = false;
|
|
111
|
+
//preloaded resource
|
|
112
|
+
|
|
113
|
+
private preloadState: PreloadState = {
|
|
114
|
+
resource: null,
|
|
115
|
+
track: null,
|
|
116
|
+
abortController: null,
|
|
117
|
+
timeoutId: null,
|
|
118
|
+
isValid: false,
|
|
119
|
+
isBeingUsed: false,
|
|
120
|
+
};
|
|
121
|
+
private isPreloading = false;
|
|
122
|
+
private currentSlot: StreamSlot = {
|
|
123
|
+
resource: null,
|
|
124
|
+
track: null,
|
|
125
|
+
streamId: null,
|
|
126
|
+
abortController: null,
|
|
127
|
+
isValid: false,
|
|
128
|
+
isLoading: false,
|
|
129
|
+
loadPromise: null,
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
private preloadSlot: StreamSlot = {
|
|
133
|
+
resource: null,
|
|
134
|
+
track: null,
|
|
135
|
+
streamId: null,
|
|
136
|
+
abortController: null,
|
|
137
|
+
isValid: false,
|
|
138
|
+
isLoading: false,
|
|
139
|
+
loadPromise: null,
|
|
140
|
+
};
|
|
141
|
+
private preloadLock = false;
|
|
142
|
+
private preloadEnabled = true;
|
|
143
|
+
private crossfadeEnabled = true;
|
|
144
|
+
private crossfadeDurationMs = 500;
|
|
145
|
+
private lowPerformanceMode = false;
|
|
146
|
+
private crossfadeTransitionLock = false;
|
|
147
|
+
private smartTransitionEnabled = true;
|
|
148
|
+
private smartTransitionGenreAware = true;
|
|
149
|
+
private smartTransitionBeatAlign = true;
|
|
150
|
+
private smartTransitionBaseMs = 800;
|
|
151
|
+
private smartTransitionMinMs = 120;
|
|
152
|
+
private smartTransitionMaxMs = 8000;
|
|
153
|
+
private smartTransitionGenreDurations: Record<string, number> = {
|
|
154
|
+
chill: 700,
|
|
155
|
+
ambient: 750,
|
|
156
|
+
lofi: 650,
|
|
157
|
+
pop: 450,
|
|
158
|
+
rock: 350,
|
|
159
|
+
edm: 220,
|
|
160
|
+
house: 250,
|
|
161
|
+
techno: 200,
|
|
162
|
+
};
|
|
163
|
+
private smartTransitionBeatAlignMaxWaitMs = 180;
|
|
164
|
+
private antiStuckEnabled = true;
|
|
165
|
+
private antiStuckMaxRetries = 2;
|
|
166
|
+
private antiStuckRetryDelayMs = 900;
|
|
167
|
+
private antiStuckReusePreloadFirst = true;
|
|
168
|
+
private antiStuckReduceQualityOnRetry = true;
|
|
169
|
+
private antiStuckControlledSkipThreshold = 3;
|
|
170
|
+
private antiStuckConsecutiveFailures = 0;
|
|
171
|
+
private loudnessNormalizationEnabled = false;
|
|
172
|
+
private loudnessTargetLUFS = -14;
|
|
173
|
+
private loudnessMaxBoostDb = 8;
|
|
174
|
+
private loudnessMaxCutDb = 10;
|
|
175
|
+
private loudnessLimiterCeiling = 0.95;
|
|
176
|
+
|
|
177
|
+
// Cache for search results to avoid duplicate calls
|
|
178
|
+
private searchCache: LRUCache<string, SearchResult>;
|
|
179
|
+
private readonly SEARCH_CACHE_TTL = 2 * 60 * 1000; // 2 minutes
|
|
180
|
+
private ttsPlayer: DiscordAudioPlayer | null = null;
|
|
181
|
+
private lastDuration: number = 0;
|
|
182
|
+
|
|
183
|
+
constructor(guildId: string, options: PlayerOptions = {}, manager: PlayerManager) {
|
|
184
|
+
super();
|
|
185
|
+
this.debug(`[Player] Constructor called for guildId: ${guildId}`);
|
|
186
|
+
this.guildId = guildId;
|
|
187
|
+
this.queue = new Queue();
|
|
188
|
+
this.manager = manager;
|
|
189
|
+
this.audioPlayer = createAudioPlayer({
|
|
190
|
+
behaviors: {
|
|
191
|
+
noSubscriber: NoSubscriberBehavior.Pause,
|
|
192
|
+
maxMissedFrames: 100,
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
this.options = {
|
|
197
|
+
leaveOnEnd: true,
|
|
198
|
+
leaveOnEmpty: true,
|
|
199
|
+
leaveTimeout: 100000,
|
|
200
|
+
volume: 100,
|
|
201
|
+
quality: "high",
|
|
202
|
+
extractorTimeout: 50000,
|
|
203
|
+
selfDeaf: true,
|
|
204
|
+
selfMute: false,
|
|
205
|
+
...options,
|
|
206
|
+
tts: {
|
|
207
|
+
createPlayer: false,
|
|
208
|
+
interrupt: true,
|
|
209
|
+
volume: 100,
|
|
210
|
+
maxTimeTts: 60_000,
|
|
211
|
+
...(options?.tts || {}),
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
this.lowPerformanceMode = this.options.lowPerformance ?? this.options.quality === "low";
|
|
215
|
+
|
|
216
|
+
const preloadOptions = this.options.preload || {};
|
|
217
|
+
const preloadAutoDisable = preloadOptions.autoDisableInLowPerformance ?? true;
|
|
218
|
+
this.preloadEnabled = preloadOptions.enabled ?? true;
|
|
219
|
+
if (this.lowPerformanceMode && preloadAutoDisable) {
|
|
220
|
+
this.preloadEnabled = false;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const crossfadeOptions = this.options.crossfade || {};
|
|
224
|
+
const crossfadeAutoEnable = crossfadeOptions.autoEnable ?? true;
|
|
225
|
+
const crossfadeAutoDisable = crossfadeOptions.autoDisableInLowPerformance ?? true;
|
|
226
|
+
this.crossfadeDurationMs = Math.max(0, crossfadeOptions.durationMs ?? 5000);
|
|
227
|
+
|
|
228
|
+
if (typeof crossfadeOptions.enabled === "boolean") {
|
|
229
|
+
this.crossfadeEnabled = crossfadeOptions.enabled;
|
|
230
|
+
} else {
|
|
231
|
+
this.crossfadeEnabled = crossfadeAutoEnable;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (this.lowPerformanceMode && crossfadeAutoDisable) {
|
|
235
|
+
this.crossfadeEnabled = false;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const smartTransitionOptions = this.options.smartTransition || {};
|
|
239
|
+
this.smartTransitionEnabled = smartTransitionOptions.enabled ?? true;
|
|
240
|
+
this.smartTransitionGenreAware = smartTransitionOptions.genreAware ?? true;
|
|
241
|
+
this.smartTransitionBeatAlign = smartTransitionOptions.beatAlign ?? true;
|
|
242
|
+
this.smartTransitionBaseMs = Math.max(0, smartTransitionOptions.baseDurationMs ?? this.crossfadeDurationMs);
|
|
243
|
+
this.smartTransitionMinMs = Math.max(0, smartTransitionOptions.minDurationMs ?? 1200);
|
|
244
|
+
this.smartTransitionMaxMs = Math.max(this.smartTransitionMinMs, smartTransitionOptions.maxDurationMs ?? 8000);
|
|
245
|
+
this.smartTransitionBeatAlignMaxWaitMs = Math.max(0, smartTransitionOptions.beatAlignMaxWaitMs ?? 1200);
|
|
246
|
+
this.smartTransitionGenreDurations = {
|
|
247
|
+
...this.smartTransitionGenreDurations,
|
|
248
|
+
...(smartTransitionOptions.genreDurations || {}),
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const antiStuckOptions = this.options.antiStuck || {};
|
|
252
|
+
this.antiStuckEnabled = antiStuckOptions.enabled ?? true;
|
|
253
|
+
this.antiStuckMaxRetries = Math.max(0, antiStuckOptions.maxRetries ?? 2);
|
|
254
|
+
this.antiStuckRetryDelayMs = Math.max(0, antiStuckOptions.retryDelayMs ?? 900);
|
|
255
|
+
this.antiStuckReusePreloadFirst = antiStuckOptions.reusePreloadFirst ?? true;
|
|
256
|
+
this.antiStuckReduceQualityOnRetry = antiStuckOptions.reduceQualityOnRetry ?? true;
|
|
257
|
+
this.antiStuckControlledSkipThreshold = Math.max(1, antiStuckOptions.controlledSkipThreshold ?? 3);
|
|
258
|
+
|
|
259
|
+
const loudnessOptions = this.options.loudnessNormalization || {};
|
|
260
|
+
this.loudnessNormalizationEnabled = loudnessOptions.enabled ?? false;
|
|
261
|
+
this.loudnessTargetLUFS = loudnessOptions.targetLUFS ?? -14;
|
|
262
|
+
this.loudnessMaxBoostDb = Math.max(0, loudnessOptions.maxBoostDb ?? 8);
|
|
263
|
+
this.loudnessMaxCutDb = Math.max(0, loudnessOptions.maxCutDb ?? 10);
|
|
264
|
+
this.loudnessLimiterCeiling = Math.min(1, Math.max(0.1, loudnessOptions.limiterCeiling ?? 0.95));
|
|
265
|
+
|
|
266
|
+
this.debug(
|
|
267
|
+
`[Player] Runtime options resolved: lowPerformance=${this.lowPerformanceMode}, preload=${this.preloadEnabled}, crossfade=${this.crossfadeEnabled} (${this.crossfadeDurationMs}ms), smartTransition=${this.smartTransitionEnabled}, antiStuck=${this.antiStuckEnabled}, loudnessNormalization=${this.loudnessNormalizationEnabled}`,
|
|
268
|
+
);
|
|
269
|
+
this.filter = new FilterManager(this, this.manager);
|
|
270
|
+
this.extensionManager = new ExtensionManager(this, this.manager);
|
|
271
|
+
this.pluginManager = new PluginManager(this, this.manager, {
|
|
272
|
+
extractorTimeout: this.options.extractorTimeout,
|
|
273
|
+
});
|
|
274
|
+
this.streamManager = new StreamManager({
|
|
275
|
+
maxConcurrentStreams: 20,
|
|
276
|
+
streamTimeout: 5 * 60 * 1000,
|
|
277
|
+
maxListenersPerStream: 15,
|
|
278
|
+
enableMetrics: true,
|
|
279
|
+
autoDestroy: true,
|
|
280
|
+
});
|
|
281
|
+
this.volume = this.options.volume || 100;
|
|
282
|
+
this.userdata = this.options.userdata;
|
|
283
|
+
this.searchCache = new LRUCache<string, SearchResult>({
|
|
284
|
+
max: 200,
|
|
285
|
+
ttl: this.SEARCH_CACHE_TTL,
|
|
286
|
+
dispose: (value, key, reason) => {
|
|
287
|
+
if (this.listenerCount("debug") > 0) {
|
|
288
|
+
this.debug(`[SearchCache] Disposed cache entry: ${key}, reason: ${reason}`);
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
allowStale: false,
|
|
292
|
+
updateAgeOnGet: true,
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
this.setupEventListeners();
|
|
296
|
+
|
|
297
|
+
// Initialize filters from options
|
|
298
|
+
if (this.options.filters && this.options.filters.length > 0) {
|
|
299
|
+
this.debug(`[Player] Initializing ${this.options.filters.length} filters from options`);
|
|
300
|
+
// Use async version but don't await in constructor
|
|
301
|
+
this.filter.applyFilters(this.options.filters).catch((error: any) => {
|
|
302
|
+
this.debug(`[Player] Error initializing filters:`, error);
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Optionally pre-create the TTS AudioPlayer
|
|
307
|
+
if (this.options?.tts?.createPlayer) {
|
|
308
|
+
this.ensureTTSPlayer();
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Destroy current stream to prevent memory leaks
|
|
314
|
+
* @private
|
|
315
|
+
*/
|
|
316
|
+
private destroyCurrentStream(): void {
|
|
317
|
+
this.audioPlayer.stop(true);
|
|
318
|
+
if (!this.currentResource) return;
|
|
319
|
+
|
|
320
|
+
const stream = (this.currentResource as any)?.metadata?.stream ?? (this.currentResource as any)?.stream;
|
|
321
|
+
|
|
322
|
+
if (stream && typeof stream.destroy === "function") {
|
|
323
|
+
stream.destroy().catch((e: any) => this.debug("Stream destroy error:", e));
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
this.currentResource = null;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
//#region Search
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Search for tracks using the player's extensions and plugins
|
|
333
|
+
*
|
|
334
|
+
* @param {string} query - The query to search for
|
|
335
|
+
* @param {string} requestedBy - The user ID who requested the search
|
|
336
|
+
* @returns {Promise<SearchResult>} The search result
|
|
337
|
+
* @example
|
|
338
|
+
* const result = await player.search("Never Gonna Give You Up", userId);
|
|
339
|
+
* console.log(`Search result: ${result.tracks.length} tracks`);
|
|
340
|
+
*/
|
|
341
|
+
async search(query: string, requestedBy: string): Promise<SearchResult> {
|
|
342
|
+
this.debug(`[Player] Search called with query: ${query}, requestedBy: ${requestedBy}`);
|
|
343
|
+
|
|
344
|
+
// Check player cache first (LRU)
|
|
345
|
+
const cachedResult = this.getCachedSearchResult(query);
|
|
346
|
+
if (cachedResult) {
|
|
347
|
+
return cachedResult;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Try extensions first
|
|
351
|
+
const extensionResult = await this.extensionManager.provideSearch(query, requestedBy);
|
|
352
|
+
if (extensionResult && Array.isArray(extensionResult.tracks) && extensionResult.tracks.length > 0) {
|
|
353
|
+
this.debug(`[Player] Extension handled search for query: ${query}`);
|
|
354
|
+
this.cacheSearchResult(query, extensionResult);
|
|
355
|
+
return extensionResult;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Use PluginManager for search with deduplication and evaluation
|
|
359
|
+
const pluginResult = await this.pluginManager.search(query, requestedBy);
|
|
360
|
+
|
|
361
|
+
if (pluginResult && pluginResult.tracks.length > 0) {
|
|
362
|
+
this.debug(`[Player] Plugin search returned ${pluginResult.tracks.length} tracks (score: ${pluginResult.score?.score}%)`);
|
|
363
|
+
|
|
364
|
+
if (pluginResult.score) {
|
|
365
|
+
this.debug(`[Player] Search evaluation - ${pluginResult.score.reason}`);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
this.cacheSearchResult(query, pluginResult);
|
|
369
|
+
return pluginResult;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
this.debug(`[Player] No search results for query: ${query}`);
|
|
373
|
+
throw new Error(`No results found for: ${query}`);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Get cached search result or null if not found/expired
|
|
378
|
+
* @param query The search query
|
|
379
|
+
* @returns Cached search result or null
|
|
380
|
+
*/
|
|
381
|
+
private getCachedSearchResult(query: string): SearchResult | null {
|
|
382
|
+
const cacheKey = query.toLowerCase().trim();
|
|
383
|
+
const cached = this.searchCache.get(cacheKey);
|
|
384
|
+
if (cached) {
|
|
385
|
+
this.debug(`[SearchCache] Using cached search result for: ${query}`);
|
|
386
|
+
return cached;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Cache search result
|
|
394
|
+
* @param query The search query
|
|
395
|
+
* @param result The search result to cache
|
|
396
|
+
*/
|
|
397
|
+
private cacheSearchResult(query: string, result: SearchResult): void {
|
|
398
|
+
const cacheKey = query.toLowerCase().trim();
|
|
399
|
+
this.searchCache.set(cacheKey, result);
|
|
400
|
+
this.debug(`[SearchCache] Cached search result for: ${query} (${result.tracks.length} tracks)`);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Clear expired search cache entries
|
|
405
|
+
*/
|
|
406
|
+
private clearExpiredSearchCache(): void {
|
|
407
|
+
this.searchCache.purgeStale();
|
|
408
|
+
this.debug(`[SearchCache] Purged stale search cache entries`);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Clear all search cache entries
|
|
413
|
+
* @example
|
|
414
|
+
* player.clearSearchCache();
|
|
415
|
+
*/
|
|
416
|
+
public clearSearchCache(): void {
|
|
417
|
+
const cacheSize = this.searchCache.size;
|
|
418
|
+
this.searchCache.clear();
|
|
419
|
+
this.debug(`[SearchCache] Cleared all ${cacheSize} search cache entries`);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Debug method to check for duplicate search calls
|
|
424
|
+
* @param query The search query to check
|
|
425
|
+
* @returns Debug information about the query
|
|
426
|
+
*/
|
|
427
|
+
public debugSearchQuery(query: string): {
|
|
428
|
+
isCached: boolean;
|
|
429
|
+
cacheAge?: number;
|
|
430
|
+
pluginCount: number;
|
|
431
|
+
ttsFiltered: boolean;
|
|
432
|
+
} {
|
|
433
|
+
const cacheKey = query.toLowerCase().trim();
|
|
434
|
+
const cached = this.searchCache.get(cacheKey);
|
|
435
|
+
const isCached = !!cached;
|
|
436
|
+
|
|
437
|
+
const allPlugins = this.pluginManager.getAll();
|
|
438
|
+
const plugins = allPlugins.filter((p) => {
|
|
439
|
+
if (p.name.toLowerCase() === "tts" && !query.toLowerCase().startsWith("tts:")) {
|
|
440
|
+
return false;
|
|
441
|
+
}
|
|
442
|
+
return true;
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
return {
|
|
446
|
+
isCached,
|
|
447
|
+
cacheAge: undefined,
|
|
448
|
+
pluginCount: plugins.length,
|
|
449
|
+
ttsFiltered: allPlugins.length > plugins.length,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
private async generateWillNext(): Promise<void> {
|
|
454
|
+
const lastTrack = this.queue.previousTracks[this.queue.previousTracks.length - 1] ?? this.queue.currentTrack;
|
|
455
|
+
if (!lastTrack) return;
|
|
456
|
+
const related = await this.pluginManager.getRelatedTracks(lastTrack);
|
|
457
|
+
if (!related || related.length === 0) return;
|
|
458
|
+
const randomchoice = Math.floor(Math.random() * related.length);
|
|
459
|
+
const nextTrack = this.queue.nextTrack ? this.queue.nextTrack : related[randomchoice];
|
|
460
|
+
this.queue.willNextTrack(nextTrack);
|
|
461
|
+
this.queue.relatedTracks(related);
|
|
462
|
+
this.debug(`[Player] Will next track if autoplay: ${nextTrack?.title}]`);
|
|
463
|
+
this.emit("willPlay", nextTrack, related);
|
|
464
|
+
}
|
|
465
|
+
//#endregion
|
|
466
|
+
//#region Play
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Play a track, search query, search result, or play from queue
|
|
470
|
+
*
|
|
471
|
+
* @param {string | Track | SearchResult | null} query - Track URL, search query, Track object, SearchResult, or null for play
|
|
472
|
+
* @param {string} requestedBy - User ID who requested the track
|
|
473
|
+
* @returns {Promise<boolean>} True if playback started successfully
|
|
474
|
+
* @example
|
|
475
|
+
* await player.play("Never Gonna Give You Up", userId); // Search query
|
|
476
|
+
* await player.play("https://youtube.com/watch?v=dQw4w9WgXcQ", userId); // Direct URL
|
|
477
|
+
* await player.play("tts: Hello everyone!", userId); // Text-to-Speech
|
|
478
|
+
* await player.play(trackObject, userId); // Track object
|
|
479
|
+
* await player.play(searchResult, userId); // SearchResult object
|
|
480
|
+
* await player.play(null); // play from queue
|
|
481
|
+
*/
|
|
482
|
+
async play(query: string | Track | SearchResult | null, requestedBy?: string): Promise<boolean> {
|
|
483
|
+
const debugInfo =
|
|
484
|
+
query === null ? "null"
|
|
485
|
+
: typeof query === "string" ? query
|
|
486
|
+
: "tracks" in query ? `${query.tracks.length} tracks`
|
|
487
|
+
: query.title || "unknown";
|
|
488
|
+
this.debug(`[Player] Play called with query: ${debugInfo}`);
|
|
489
|
+
this.clearLeaveTimeout();
|
|
490
|
+
let tracksToAdd: Track[] = [];
|
|
491
|
+
let isPlaylist = false;
|
|
492
|
+
let effectiveRequest: ExtensionPlayRequest = { query: query as string | Track, requestedBy };
|
|
493
|
+
let hookResponse: ExtensionPlayResponse = {};
|
|
494
|
+
|
|
495
|
+
try {
|
|
496
|
+
// Handle null query - play from queue
|
|
497
|
+
if (query === null) {
|
|
498
|
+
this.debug(`[Player] Play from queue requested`);
|
|
499
|
+
if (this.queue.isEmpty) {
|
|
500
|
+
this.debug(`[Player] Queue is empty, nothing to play`);
|
|
501
|
+
return false;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (!this.isPlaying) {
|
|
505
|
+
return await this.playNext();
|
|
506
|
+
}
|
|
507
|
+
return true;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Handle SearchResult
|
|
511
|
+
if (query && typeof query === "object" && "tracks" in query && Array.isArray(query.tracks)) {
|
|
512
|
+
this.debug(`[Player] Playing SearchResult with ${query.tracks.length} tracks`);
|
|
513
|
+
tracksToAdd = query.tracks;
|
|
514
|
+
isPlaylist = !!query.playlist || query.tracks.length > 1;
|
|
515
|
+
|
|
516
|
+
if (query.playlist) {
|
|
517
|
+
this.debug(`[Player] Added playlist: ${query.playlist.name} (${tracksToAdd.length} tracks)`);
|
|
518
|
+
}
|
|
519
|
+
} else {
|
|
520
|
+
// Handle other types (string, Track)
|
|
521
|
+
const hookOutcome = await this.extensionManager.beforePlayHooks(effectiveRequest);
|
|
522
|
+
effectiveRequest = hookOutcome.request;
|
|
523
|
+
hookResponse = hookOutcome.response;
|
|
524
|
+
if (effectiveRequest.requestedBy === undefined) {
|
|
525
|
+
effectiveRequest.requestedBy = requestedBy;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const hookTracks = Array.isArray(hookResponse.tracks) ? hookResponse.tracks : undefined;
|
|
529
|
+
|
|
530
|
+
if (hookResponse.handled && (!hookTracks || hookTracks.length === 0)) {
|
|
531
|
+
const handledPayload: ExtensionAfterPlayPayload = {
|
|
532
|
+
success: hookResponse.success ?? true,
|
|
533
|
+
query: effectiveRequest.query,
|
|
534
|
+
requestedBy: effectiveRequest.requestedBy,
|
|
535
|
+
tracks: [],
|
|
536
|
+
isPlaylist: hookResponse.isPlaylist ?? false,
|
|
537
|
+
error: hookResponse.error,
|
|
538
|
+
};
|
|
539
|
+
await this.extensionManager.afterPlayHooks(handledPayload);
|
|
540
|
+
if (hookResponse.error) {
|
|
541
|
+
this.emit("playerError", hookResponse.error);
|
|
542
|
+
}
|
|
543
|
+
return hookResponse.success ?? true;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (hookTracks && hookTracks.length > 0) {
|
|
547
|
+
tracksToAdd = hookTracks;
|
|
548
|
+
isPlaylist = hookResponse.isPlaylist ?? hookTracks.length > 1;
|
|
549
|
+
} else if (typeof effectiveRequest.query === "string") {
|
|
550
|
+
const searchResult = await this.search(effectiveRequest.query, effectiveRequest.requestedBy || "Unknown");
|
|
551
|
+
tracksToAdd = searchResult.tracks;
|
|
552
|
+
if (searchResult.playlist) {
|
|
553
|
+
isPlaylist = true;
|
|
554
|
+
this.debug(`[Player] Added playlist: ${searchResult.playlist.name} (${tracksToAdd.length} tracks)`);
|
|
555
|
+
}
|
|
556
|
+
} else if (effectiveRequest.query) {
|
|
557
|
+
tracksToAdd = [effectiveRequest.query as Track];
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
if (tracksToAdd.length === 0) {
|
|
562
|
+
this.debug(`[Player] No tracks found for play`);
|
|
563
|
+
throw new Error("No tracks found");
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const isTTS = (t: Track | undefined) => {
|
|
567
|
+
if (!t) return false;
|
|
568
|
+
try {
|
|
569
|
+
return typeof t.source === "string" && t.source.toLowerCase().includes("tts");
|
|
570
|
+
} catch {
|
|
571
|
+
return false;
|
|
572
|
+
}
|
|
573
|
+
};
|
|
574
|
+
|
|
575
|
+
const queryLooksTTS =
|
|
576
|
+
typeof effectiveRequest.query === "string" && effectiveRequest.query.trim().toLowerCase().startsWith("tts");
|
|
577
|
+
|
|
578
|
+
if (
|
|
579
|
+
!isPlaylist &&
|
|
580
|
+
tracksToAdd.length > 0 &&
|
|
581
|
+
this.options?.tts?.interrupt !== false &&
|
|
582
|
+
(isTTS(tracksToAdd[0]) || queryLooksTTS)
|
|
583
|
+
) {
|
|
584
|
+
this.debug(`[Player] Interrupting with TTS: ${tracksToAdd[0].title}`);
|
|
585
|
+
await this.interruptWithTTSTrack(tracksToAdd[0]);
|
|
586
|
+
await this.extensionManager.afterPlayHooks({
|
|
587
|
+
success: true,
|
|
588
|
+
query: effectiveRequest.query,
|
|
589
|
+
requestedBy: effectiveRequest.requestedBy,
|
|
590
|
+
tracks: tracksToAdd,
|
|
591
|
+
isPlaylist,
|
|
592
|
+
});
|
|
593
|
+
return true;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
if (isPlaylist) {
|
|
597
|
+
this.queue.addMultiple(tracksToAdd);
|
|
598
|
+
this.emit("queueAddList", tracksToAdd);
|
|
599
|
+
} else {
|
|
600
|
+
this.queue.add(tracksToAdd[0]);
|
|
601
|
+
this.emit("queueAdd", tracksToAdd[0]);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const started = !this.isPlaying ? await this.playNext() : true;
|
|
605
|
+
|
|
606
|
+
await this.extensionManager.afterPlayHooks({
|
|
607
|
+
success: started,
|
|
608
|
+
query: effectiveRequest.query,
|
|
609
|
+
requestedBy: effectiveRequest.requestedBy,
|
|
610
|
+
tracks: tracksToAdd,
|
|
611
|
+
isPlaylist,
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
return started;
|
|
615
|
+
} catch (error) {
|
|
616
|
+
await this.extensionManager.afterPlayHooks({
|
|
617
|
+
success: false,
|
|
618
|
+
query: effectiveRequest.query,
|
|
619
|
+
requestedBy: effectiveRequest.requestedBy,
|
|
620
|
+
tracks: tracksToAdd,
|
|
621
|
+
isPlaylist,
|
|
622
|
+
error: error as Error,
|
|
623
|
+
});
|
|
624
|
+
this.debug(`[Player] Play error:`, error);
|
|
625
|
+
this.emit("playerError", error as Error);
|
|
626
|
+
return false;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
//#endregion
|
|
630
|
+
//#region Preload
|
|
631
|
+
/**
|
|
632
|
+
* Main preload method - only one at a time
|
|
633
|
+
*/
|
|
634
|
+
private async preloadNextTrack(): Promise<void> {
|
|
635
|
+
if (!this.preloadEnabled) {
|
|
636
|
+
this.debug(`[Preload] Disabled by options/runtime profile`);
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Prevent concurrent preloads
|
|
641
|
+
if (this.preloadLock) {
|
|
642
|
+
this.debug(`[Preload] Already preloading, skipping`);
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const nextTrack = this.queue.nextTrack;
|
|
647
|
+
if (!nextTrack) {
|
|
648
|
+
this.debug(`[Preload] No next track to preload`);
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// Check if already preloaded correctly
|
|
653
|
+
if (this.preloadSlot.isValid && this.preloadSlot.track?.id === nextTrack.id && this.preloadSlot.resource) {
|
|
654
|
+
this.debug(`[Preload] Already have valid preload for: ${nextTrack.title}`);
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// Check if currently loading the same track
|
|
659
|
+
if (this.preloadSlot.isLoading && this.preloadSlot.track?.id === nextTrack.id) {
|
|
660
|
+
this.debug(`[Preload] Currently loading same track, waiting...`);
|
|
661
|
+
if (this.preloadSlot.loadPromise) {
|
|
662
|
+
await this.preloadSlot.loadPromise;
|
|
663
|
+
}
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// Cancel old preload if different track
|
|
668
|
+
if (this.preloadSlot.isValid && this.preloadSlot.track?.id !== nextTrack.id) {
|
|
669
|
+
this.debug(`[Preload] Cancelling old preload for different track: ${this.preloadSlot.track?.title}`);
|
|
670
|
+
await this.safeCancelPreload();
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
this.preloadLock = true;
|
|
674
|
+
|
|
675
|
+
// Create new abort controller
|
|
676
|
+
const abortController = new AbortController();
|
|
677
|
+
|
|
678
|
+
// Setup preload slot
|
|
679
|
+
this.preloadSlot.track = nextTrack;
|
|
680
|
+
this.preloadSlot.abortController = abortController;
|
|
681
|
+
this.preloadSlot.isLoading = true;
|
|
682
|
+
|
|
683
|
+
// Create load promise
|
|
684
|
+
const loadPromise = this.executePreload(nextTrack, abortController);
|
|
685
|
+
this.preloadSlot.loadPromise = loadPromise;
|
|
686
|
+
|
|
687
|
+
try {
|
|
688
|
+
await loadPromise;
|
|
689
|
+
} catch (err) {
|
|
690
|
+
if (err instanceof Error && err.message === "PRELOAD_CANCELLED") {
|
|
691
|
+
this.debug(`[Preload] Cancelled for ${nextTrack.title}`);
|
|
692
|
+
} else {
|
|
693
|
+
this.debug(`[Preload] Failed for ${nextTrack.title}:`, err);
|
|
694
|
+
}
|
|
695
|
+
this.clearSlot(this.preloadSlot);
|
|
696
|
+
} finally {
|
|
697
|
+
this.preloadLock = false;
|
|
698
|
+
this.preloadSlot.isLoading = false;
|
|
699
|
+
this.preloadSlot.loadPromise = null;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Execute actual preload
|
|
705
|
+
*/
|
|
706
|
+
private async executePreload(track: Track, abortController: AbortController): Promise<void> {
|
|
707
|
+
this.debug(`[Preload] Starting preload for: ${track.title}`);
|
|
708
|
+
|
|
709
|
+
// Check for cancellation
|
|
710
|
+
if (abortController.signal.aborted) {
|
|
711
|
+
throw new Error("PRELOAD_CANCELLED");
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// Check if track still relevant
|
|
715
|
+
if (this.queue.nextTrack?.id !== track.id) {
|
|
716
|
+
this.debug(`[Preload] Track changed, cancelling`);
|
|
717
|
+
throw new Error("PRELOAD_CANCELLED");
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
try {
|
|
721
|
+
// Get stream with abort support - NO TIMEOUT
|
|
722
|
+
const streamInfo = await this.getStreamWithCancel(track, abortController.signal);
|
|
723
|
+
|
|
724
|
+
// Check cancellation
|
|
725
|
+
if (abortController.signal.aborted) {
|
|
726
|
+
throw new Error("PRELOAD_CANCELLED");
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// Check track relevance again
|
|
730
|
+
if (this.queue.nextTrack?.id !== track.id) {
|
|
731
|
+
this.debug(`[Preload] Track changed after stream fetch`);
|
|
732
|
+
throw new Error("PRELOAD_CANCELLED");
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
if (!streamInfo?.stream) {
|
|
736
|
+
throw new Error(`No stream available`);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// Register with StreamManager as preload
|
|
740
|
+
const streamId = this.streamManager.registerStream(streamInfo.stream, track, {
|
|
741
|
+
source: track.source || "preload",
|
|
742
|
+
isPreload: true,
|
|
743
|
+
priority: 5,
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
// Create resource
|
|
747
|
+
const resource = createAudioResource(streamInfo.stream, {
|
|
748
|
+
inlineVolume: true,
|
|
749
|
+
metadata: { ...track, preloaded: true },
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
// Verify resource is valid
|
|
753
|
+
if (!resource.playStream || resource.playStream.readable === false) {
|
|
754
|
+
throw new Error("Resource not readable");
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// Update preload slot
|
|
758
|
+
this.preloadSlot.resource = resource;
|
|
759
|
+
this.preloadSlot.streamId = streamId;
|
|
760
|
+
this.preloadSlot.isValid = true;
|
|
761
|
+
this.preloadSlot.track = track;
|
|
762
|
+
|
|
763
|
+
this.debug(`[Preload] Successfully preloaded: ${track.title} (Stream ID: ${streamId})`);
|
|
764
|
+
} catch (err) {
|
|
765
|
+
if (err instanceof Error && err.message === "PRELOAD_CANCELLED") {
|
|
766
|
+
throw err;
|
|
767
|
+
}
|
|
768
|
+
this.debug(`[Preload] Error during preload:`, err);
|
|
769
|
+
throw err;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* Safe cancel preload - doesn't throw
|
|
775
|
+
*/
|
|
776
|
+
private async safeCancelPreload(): Promise<void> {
|
|
777
|
+
if (!this.preloadSlot.abortController && !this.preloadSlot.resource) {
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
this.debug(`[Preload] Safely cancelling preload for: ${this.preloadSlot.track?.title || "unknown"}`);
|
|
782
|
+
|
|
783
|
+
// Abort the operation
|
|
784
|
+
if (this.preloadSlot.abortController) {
|
|
785
|
+
this.preloadSlot.abortController.abort();
|
|
786
|
+
this.preloadSlot.abortController = null;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// Clean up stream
|
|
790
|
+
if (this.preloadSlot.streamId && this.streamManager) {
|
|
791
|
+
this.streamManager.unregisterStream(this.preloadSlot.streamId, true);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// Clean up resource
|
|
795
|
+
if (this.preloadSlot.resource) {
|
|
796
|
+
try {
|
|
797
|
+
const stream = this.preloadSlot.resource.playStream;
|
|
798
|
+
if (stream && typeof stream.destroy === "function" && !stream.destroyed) {
|
|
799
|
+
stream.destroy();
|
|
800
|
+
}
|
|
801
|
+
} catch (err) {
|
|
802
|
+
// Ignore destroy errors
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// Clear slot
|
|
807
|
+
this.clearSlot(this.preloadSlot);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* Get stream with proper cancellation
|
|
812
|
+
*/
|
|
813
|
+
private async getStreamWithCancel(track: Track, signal: AbortSignal): Promise<StreamInfo | null> {
|
|
814
|
+
// Create abort promise
|
|
815
|
+
const abortPromise = new Promise<never>((_, reject) => {
|
|
816
|
+
if (signal.aborted) {
|
|
817
|
+
reject(new Error("PRELOAD_CANCELLED"));
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
const handler = () => {
|
|
821
|
+
signal.removeEventListener("abort", handler);
|
|
822
|
+
reject(new Error("PRELOAD_CANCELLED"));
|
|
823
|
+
};
|
|
824
|
+
signal.addEventListener("abort", handler);
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
try {
|
|
828
|
+
// Check if stream already exists and is valid
|
|
829
|
+
const existingStream = this.streamManager.getStreamByTrack(track.id || track.title);
|
|
830
|
+
if (existingStream && !existingStream.destroyed && existingStream.readable !== false) {
|
|
831
|
+
this.debug(`[Stream] Using existing stream for preload: ${track.title}`);
|
|
832
|
+
return { stream: existingStream, type: "arbitrary" };
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// Race between stream fetch and abort
|
|
836
|
+
const streamPromise = this.getStream(track);
|
|
837
|
+
const result = await Promise.race([streamPromise, abortPromise]);
|
|
838
|
+
return result as StreamInfo | null;
|
|
839
|
+
} catch (err) {
|
|
840
|
+
if (err instanceof Error && err.message === "PRELOAD_CANCELLED") {
|
|
841
|
+
throw err;
|
|
842
|
+
}
|
|
843
|
+
throw err;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* Preload next track with proper error handling and cleanup
|
|
848
|
+
*/
|
|
849
|
+
async preloadNext(): Promise<void> {
|
|
850
|
+
if (!this.preloadEnabled) {
|
|
851
|
+
this.debug(`[Preload] Disabled by options/runtime profile`);
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
this.cancelPreload();
|
|
856
|
+
|
|
857
|
+
const next = this.queue.nextTrack;
|
|
858
|
+
if (!next || this.isPreloading) {
|
|
859
|
+
this.debug(`[Preload] Skipped - ${!next ? "no next track" : "already preloading"}`);
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
this.isPreloading = true;
|
|
864
|
+
|
|
865
|
+
// Create new AbortController
|
|
866
|
+
const abortController = new AbortController();
|
|
867
|
+
const timeoutId = setTimeout(() => {
|
|
868
|
+
// this.debug(`[Preload] Timeout for track: ${next.title}`);
|
|
869
|
+
// abortController.abort();
|
|
870
|
+
}, 30000);
|
|
871
|
+
|
|
872
|
+
this.preloadState.abortController = abortController;
|
|
873
|
+
this.preloadState.timeoutId = timeoutId;
|
|
874
|
+
|
|
875
|
+
try {
|
|
876
|
+
this.debug(`[Preload] Starting preload for: ${next.title}`);
|
|
877
|
+
|
|
878
|
+
// Check if already aborted
|
|
879
|
+
if (abortController.signal.aborted) {
|
|
880
|
+
throw new Error("Preload aborted before start");
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// Check if this track is still the next one
|
|
884
|
+
if (this.queue.nextTrack?.id !== next.id) {
|
|
885
|
+
this.debug(`[Preload] Track changed, cancelling preload`);
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
const streamInfo = await this.getStreamWithCancel(next, abortController.signal);
|
|
890
|
+
|
|
891
|
+
// Double check
|
|
892
|
+
if (abortController.signal.aborted) {
|
|
893
|
+
throw new Error("Preload aborted after stream fetch");
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
if (this.queue.nextTrack?.id !== next.id) {
|
|
897
|
+
this.debug(`[Preload] Track changed after stream fetch`);
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
if (!streamInfo?.stream) {
|
|
902
|
+
throw new Error(`No stream available`);
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// Register with StreamManager
|
|
906
|
+
const streamId = this.streamManager.registerStream(streamInfo.stream, next, {
|
|
907
|
+
source: next.source || "preload",
|
|
908
|
+
isPreload: true,
|
|
909
|
+
priority: 8,
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
// Create resource
|
|
913
|
+
const resource = createAudioResource(streamInfo.stream, {
|
|
914
|
+
inlineVolume: true,
|
|
915
|
+
metadata: { ...next, preloaded: true },
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
// Store preload state
|
|
919
|
+
this.preloadState = {
|
|
920
|
+
resource,
|
|
921
|
+
track: next,
|
|
922
|
+
abortController,
|
|
923
|
+
timeoutId,
|
|
924
|
+
isValid: true,
|
|
925
|
+
isBeingUsed: false,
|
|
926
|
+
streamId,
|
|
927
|
+
};
|
|
928
|
+
|
|
929
|
+
this.debug(`[Preload] Successfully preloaded: ${next.title} (Stream ID: ${streamId})`);
|
|
930
|
+
} catch (err) {
|
|
931
|
+
if (err instanceof Error && err.message.includes("aborted")) {
|
|
932
|
+
this.debug(`[Preload] Cancelled for ${next.title}`);
|
|
933
|
+
} else {
|
|
934
|
+
this.debug(`[Preload] Failed for ${next?.title}:`, err);
|
|
935
|
+
}
|
|
936
|
+
this.cancelPreload();
|
|
937
|
+
} finally {
|
|
938
|
+
this.isPreloading = false;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
private async fadeResourceVolume(resource: AudioResource, from: number, to: number, durationMs: number): Promise<void> {
|
|
943
|
+
if (!resource.volume) return;
|
|
944
|
+
|
|
945
|
+
const safeDuration = Math.max(0, durationMs);
|
|
946
|
+
if (safeDuration === 0) {
|
|
947
|
+
resource.volume.setVolume(to);
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
const steps = Math.max(1, Math.floor(safeDuration / 50));
|
|
952
|
+
const stepDuration = Math.max(20, Math.floor(safeDuration / steps));
|
|
953
|
+
const delta = (to - from) / steps;
|
|
954
|
+
|
|
955
|
+
resource.volume.setVolume(from);
|
|
956
|
+
for (let i = 1; i <= steps; i++) {
|
|
957
|
+
await new Promise((resolve) => setTimeout(resolve, stepDuration));
|
|
958
|
+
resource.volume.setVolume(from + delta * i);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
private async applyCrossfadeIn(resource: AudioResource, track: Track): Promise<void> {
|
|
963
|
+
if (!this.crossfadeEnabled || !resource.volume) return;
|
|
964
|
+
const targetVolume = this.getTrackTargetVolume(track);
|
|
965
|
+
const transitionMs = this.resolveSmartTransitionDuration(track);
|
|
966
|
+
await this.fadeResourceVolume(resource, 0, targetVolume, transitionMs);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
private async applyCrossfadeOutCurrent(): Promise<void> {
|
|
970
|
+
if (!this.crossfadeEnabled) return;
|
|
971
|
+
const current = this.currentSlot.resource || this.currentResource;
|
|
972
|
+
if (!current?.volume) return;
|
|
973
|
+
const currentVolume = current.volume.volume ?? this.volume / 100;
|
|
974
|
+
const currentTrack = this.queue.currentTrack;
|
|
975
|
+
const transitionMs =
|
|
976
|
+
currentTrack ? this.resolveSmartTransitionDuration(currentTrack) : this.resolveSmartTransitionDuration({} as Track);
|
|
977
|
+
await this.fadeResourceVolume(current, currentVolume, 0, transitionMs);
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
private async crossfadeSkipAndStop(): Promise<void> {
|
|
981
|
+
if (!this.crossfadeEnabled) {
|
|
982
|
+
this.audioPlayer.stop();
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
if (this.crossfadeTransitionLock) {
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
this.crossfadeTransitionLock = true;
|
|
989
|
+
try {
|
|
990
|
+
await this.applyCrossfadeOutCurrent();
|
|
991
|
+
this.audioPlayer.stop();
|
|
992
|
+
} finally {
|
|
993
|
+
this.crossfadeTransitionLock = false;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
private getTrackMetadataValue(track: Track, key: string): any {
|
|
998
|
+
const md = track?.metadata as Record<string, any> | undefined;
|
|
999
|
+
if (!md) return undefined;
|
|
1000
|
+
return md[key];
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
private resolveSmartTransitionDuration(track: Track): number {
|
|
1004
|
+
if (!this.smartTransitionEnabled) {
|
|
1005
|
+
return this.crossfadeDurationMs;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
let duration = this.smartTransitionBaseMs;
|
|
1009
|
+
if (this.smartTransitionGenreAware) {
|
|
1010
|
+
const rawGenre = this.getTrackMetadataValue(track, "genre");
|
|
1011
|
+
const genre = typeof rawGenre === "string" ? rawGenre.toLowerCase().trim() : "";
|
|
1012
|
+
if (genre && this.smartTransitionGenreDurations[genre] !== undefined) {
|
|
1013
|
+
duration = this.smartTransitionGenreDurations[genre];
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
return Math.min(this.smartTransitionMaxMs, Math.max(this.smartTransitionMinMs, duration));
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
private async maybeAlignToBeatBoundary(): Promise<void> {
|
|
1021
|
+
if (!this.smartTransitionEnabled || !this.smartTransitionBeatAlign) return;
|
|
1022
|
+
const currentTrack = this.queue.currentTrack;
|
|
1023
|
+
if (!currentTrack || !this.currentResource) return;
|
|
1024
|
+
|
|
1025
|
+
const bpmRaw = this.getTrackMetadataValue(currentTrack, "bpm");
|
|
1026
|
+
const bpm = typeof bpmRaw === "number" ? bpmRaw : Number(bpmRaw);
|
|
1027
|
+
if (!Number.isFinite(bpm) || bpm <= 0) return;
|
|
1028
|
+
|
|
1029
|
+
const beatMs = 60000 / bpm;
|
|
1030
|
+
const positionMs = this.currentResource.playbackDuration;
|
|
1031
|
+
const remainder = positionMs % beatMs;
|
|
1032
|
+
const waitMs = beatMs - remainder;
|
|
1033
|
+
if (waitMs > 0 && waitMs <= this.smartTransitionBeatAlignMaxWaitMs) {
|
|
1034
|
+
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
private getTrackTargetVolume(track: Track): number {
|
|
1039
|
+
const base = this.volume / 100;
|
|
1040
|
+
if (!this.loudnessNormalizationEnabled) {
|
|
1041
|
+
return base;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
const lufsRaw = this.getTrackMetadataValue(track, "lufs");
|
|
1045
|
+
const trackLufs = typeof lufsRaw === "number" ? lufsRaw : Number(lufsRaw);
|
|
1046
|
+
if (!Number.isFinite(trackLufs)) {
|
|
1047
|
+
return Math.min(base, this.loudnessLimiterCeiling);
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
const deltaDbRaw = this.loudnessTargetLUFS - trackLufs;
|
|
1051
|
+
const maxBoost = this.loudnessMaxBoostDb;
|
|
1052
|
+
const maxCut = this.loudnessMaxCutDb;
|
|
1053
|
+
const deltaDb = Math.max(-maxCut, Math.min(maxBoost, deltaDbRaw));
|
|
1054
|
+
const multiplier = Math.pow(10, deltaDb / 20);
|
|
1055
|
+
const adjusted = base * multiplier;
|
|
1056
|
+
return Math.min(this.loudnessLimiterCeiling, Math.max(0, adjusted));
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
private async attemptTrackRecovery(track: Track, reason: unknown): Promise<boolean> {
|
|
1060
|
+
if (!this.antiStuckEnabled) return false;
|
|
1061
|
+
this.debug(`[AntiStuck] Recovery started for: ${track.title}`, reason);
|
|
1062
|
+
|
|
1063
|
+
const originalQuality = this.options.quality;
|
|
1064
|
+
let attempted = 0;
|
|
1065
|
+
|
|
1066
|
+
while (attempted < this.antiStuckMaxRetries) {
|
|
1067
|
+
attempted++;
|
|
1068
|
+
if (this.antiStuckReduceQualityOnRetry) {
|
|
1069
|
+
this.options.quality = "low";
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
if (this.antiStuckRetryDelayMs > 0) {
|
|
1073
|
+
await new Promise((resolve) => setTimeout(resolve, this.antiStuckRetryDelayMs));
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
try {
|
|
1077
|
+
if (this.antiStuckReusePreloadFirst && this.preloadSlot.isValid && this.preloadSlot.track?.id === track.id) {
|
|
1078
|
+
const startedFromPreload = await this.startTrack(track);
|
|
1079
|
+
if (startedFromPreload) {
|
|
1080
|
+
this.antiStuckConsecutiveFailures = 0;
|
|
1081
|
+
this.options.quality = originalQuality;
|
|
1082
|
+
return true;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
const started = await this.loadFreshStream(track);
|
|
1087
|
+
if (started) {
|
|
1088
|
+
this.antiStuckConsecutiveFailures = 0;
|
|
1089
|
+
this.options.quality = originalQuality;
|
|
1090
|
+
return true;
|
|
1091
|
+
}
|
|
1092
|
+
} catch (error) {
|
|
1093
|
+
this.debug(`[AntiStuck] Retry ${attempted} failed for ${track.title}:`, error);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
this.options.quality = originalQuality;
|
|
1098
|
+
this.antiStuckConsecutiveFailures++;
|
|
1099
|
+
if (this.antiStuckConsecutiveFailures >= this.antiStuckControlledSkipThreshold) {
|
|
1100
|
+
this.debug(`[AntiStuck] Controlled skip threshold reached for ${track.title}`);
|
|
1101
|
+
return false;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
// Avoid hard skip storm by leaving track for next natural retry window.
|
|
1105
|
+
this.debug(`[AntiStuck] Keeping track for controlled retry window: ${track.title}`);
|
|
1106
|
+
return false;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
/**
|
|
1110
|
+
* Clear preloaded resource with proper cleanup
|
|
1111
|
+
*/
|
|
1112
|
+
private clearPreload(): void {
|
|
1113
|
+
// Abort ongoing preload
|
|
1114
|
+
if (this.preloadState.abortController) {
|
|
1115
|
+
this.preloadState.abortController.abort();
|
|
1116
|
+
this.preloadState.abortController = null;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// Clean up stream
|
|
1120
|
+
const stream = (this.preloadState as any).stream;
|
|
1121
|
+
if (stream && typeof stream.destroy === "function") {
|
|
1122
|
+
try {
|
|
1123
|
+
stream.destroy();
|
|
1124
|
+
} catch (err) {
|
|
1125
|
+
this.debug(`[Preload] Error destroying stream:`, err);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// Clean up resource
|
|
1130
|
+
if (this.preloadState.resource) {
|
|
1131
|
+
try {
|
|
1132
|
+
const playStream = this.preloadState.resource.playStream;
|
|
1133
|
+
if (playStream && typeof playStream.destroy === "function") {
|
|
1134
|
+
playStream.destroy();
|
|
1135
|
+
}
|
|
1136
|
+
} catch (err) {
|
|
1137
|
+
this.debug(`[Preload] Error destroying resource:`, err);
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
this.preloadState = {
|
|
1142
|
+
resource: null,
|
|
1143
|
+
track: null,
|
|
1144
|
+
abortController: null,
|
|
1145
|
+
timeoutId: null,
|
|
1146
|
+
isValid: false,
|
|
1147
|
+
isBeingUsed: false,
|
|
1148
|
+
streamId: undefined,
|
|
1149
|
+
};
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
/**
|
|
1153
|
+
* Cancel preload (when skipping or stopping)
|
|
1154
|
+
*/
|
|
1155
|
+
private cancelPreload(): void {
|
|
1156
|
+
if (this.preloadSlot.abortController) {
|
|
1157
|
+
this.debug(`[Preload] Cancelling preload for: ${this.preloadSlot.track?.title}`);
|
|
1158
|
+
this.preloadSlot.abortController.abort();
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
if (this.preloadSlot.streamId && this.streamManager) {
|
|
1162
|
+
this.streamManager.unregisterStream(this.preloadSlot.streamId, true);
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
this.clearSlot(this.preloadSlot);
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
/**
|
|
1169
|
+
* Clear a stream slot
|
|
1170
|
+
*/
|
|
1171
|
+
private clearSlot(slot: StreamSlot): void {
|
|
1172
|
+
if (slot.resource) {
|
|
1173
|
+
try {
|
|
1174
|
+
const stream = slot.resource.playStream;
|
|
1175
|
+
if (stream && typeof stream.destroy === "function" && !stream.destroyed) {
|
|
1176
|
+
stream.destroy();
|
|
1177
|
+
}
|
|
1178
|
+
} catch (err) {
|
|
1179
|
+
// Ignore
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
if (slot.streamId && this.streamManager) {
|
|
1184
|
+
// Don't wait for unregister
|
|
1185
|
+
this.streamManager.unregisterStream(slot.streamId, true);
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
slot.resource = null;
|
|
1189
|
+
slot.track = null;
|
|
1190
|
+
slot.streamId = null;
|
|
1191
|
+
slot.abortController = null;
|
|
1192
|
+
slot.isValid = false;
|
|
1193
|
+
slot.isLoading = false;
|
|
1194
|
+
slot.loadPromise = null;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
/**
|
|
1198
|
+
* Promote preload slot to current slot without destroying promoted stream.
|
|
1199
|
+
*/
|
|
1200
|
+
private promotePreloadToCurrent(track: Track): void {
|
|
1201
|
+
const promotedResource = this.preloadSlot.resource;
|
|
1202
|
+
const promotedStreamId = this.preloadSlot.streamId;
|
|
1203
|
+
|
|
1204
|
+
// Move ownership to current slot.
|
|
1205
|
+
this.currentSlot.resource = promotedResource;
|
|
1206
|
+
this.currentSlot.track = track;
|
|
1207
|
+
this.currentSlot.streamId = promotedStreamId;
|
|
1208
|
+
this.currentSlot.abortController = null;
|
|
1209
|
+
this.currentSlot.isValid = !!promotedResource;
|
|
1210
|
+
this.currentSlot.isLoading = false;
|
|
1211
|
+
this.currentSlot.loadPromise = null;
|
|
1212
|
+
this.currentResource = promotedResource;
|
|
1213
|
+
|
|
1214
|
+
// Reset preload slot only (do not destroy promoted resource/stream).
|
|
1215
|
+
this.preloadSlot.resource = null;
|
|
1216
|
+
this.preloadSlot.track = null;
|
|
1217
|
+
this.preloadSlot.streamId = null;
|
|
1218
|
+
this.preloadSlot.abortController = null;
|
|
1219
|
+
this.preloadSlot.isValid = false;
|
|
1220
|
+
this.preloadSlot.isLoading = false;
|
|
1221
|
+
this.preloadSlot.loadPromise = null;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
/**
|
|
1225
|
+
* Create AudioResource with filters and seek applied
|
|
1226
|
+
*
|
|
1227
|
+
* @param {StreamInfo} streamInfo - The stream information
|
|
1228
|
+
* @param {Track} track - The track being processed
|
|
1229
|
+
* @param {number} position - Position in milliseconds to seek to (0 = no seek)
|
|
1230
|
+
* @returns {Promise<AudioResource>} The AudioResource with filters and seek applied
|
|
1231
|
+
*/
|
|
1232
|
+
private async createResource(streamInfo: StreamInfo, track: Track, position: number = 0): Promise<AudioResource> {
|
|
1233
|
+
const filterString = this.filter.getFilterString();
|
|
1234
|
+
|
|
1235
|
+
this.debug(`[Player] Creating AudioResource with filters: ${filterString || "none"}, seek: ${position}ms`);
|
|
1236
|
+
|
|
1237
|
+
try {
|
|
1238
|
+
let stream: Readable = streamInfo.stream;
|
|
1239
|
+
// Apply filters and seek if needed
|
|
1240
|
+
if (filterString || position > 0) {
|
|
1241
|
+
stream = await this.filter.applyFiltersAndSeek(streamInfo.stream, position);
|
|
1242
|
+
streamInfo.type = StreamType.Arbitrary;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
// Create AudioResource with better error handling
|
|
1246
|
+
const resource = createAudioResource(stream, {
|
|
1247
|
+
metadata: track,
|
|
1248
|
+
inputType:
|
|
1249
|
+
streamInfo.type === "webm/opus" ? StreamType.WebmOpus
|
|
1250
|
+
: streamInfo.type === "ogg/opus" ? StreamType.OggOpus
|
|
1251
|
+
: StreamType.Arbitrary,
|
|
1252
|
+
inlineVolume: true,
|
|
1253
|
+
});
|
|
1254
|
+
|
|
1255
|
+
return resource;
|
|
1256
|
+
} catch (error) {
|
|
1257
|
+
this.debug(`[Player] Error creating AudioResource with filters+seek:`, error);
|
|
1258
|
+
// Fallback to basic AudioResource
|
|
1259
|
+
try {
|
|
1260
|
+
const resource = createAudioResource(streamInfo.stream, {
|
|
1261
|
+
metadata: track,
|
|
1262
|
+
inputType:
|
|
1263
|
+
streamInfo.type === "webm/opus" ? StreamType.WebmOpus
|
|
1264
|
+
: streamInfo.type === "ogg/opus" ? StreamType.OggOpus
|
|
1265
|
+
: StreamType.Arbitrary,
|
|
1266
|
+
inlineVolume: true,
|
|
1267
|
+
});
|
|
1268
|
+
return resource;
|
|
1269
|
+
} catch (fallbackError) {
|
|
1270
|
+
this.debug(`[Player] Fallback AudioResource creation failed:`, fallbackError);
|
|
1271
|
+
throw fallbackError;
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
private async getStream(track: Track): Promise<StreamInfo | null> {
|
|
1277
|
+
const trackId = track.id || track.url || track.title;
|
|
1278
|
+
const existingStream = this.streamManager.getStreamByTrack(trackId);
|
|
1279
|
+
|
|
1280
|
+
if (existingStream && !existingStream.destroyed) {
|
|
1281
|
+
this.debug(`[Stream] Using existing stream from manager for: ${track.title}`);
|
|
1282
|
+
return { stream: existingStream, type: "arbitrary" };
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
let stream = await this.extensionManager.provideStream(track);
|
|
1286
|
+
if (stream?.stream) {
|
|
1287
|
+
// Register with StreamManager
|
|
1288
|
+
const streamId = this.streamManager.registerStream(stream.stream, track, {
|
|
1289
|
+
source: "extension",
|
|
1290
|
+
isPreload: false,
|
|
1291
|
+
priority: 10,
|
|
1292
|
+
});
|
|
1293
|
+
this.debug(`[Stream] Extension stream registered with ID: ${streamId}`);
|
|
1294
|
+
return stream;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
stream = await this.pluginManager.getStream(track);
|
|
1298
|
+
if (stream?.stream) {
|
|
1299
|
+
const existingAgain = this.streamManager.getStreamByTrack(trackId);
|
|
1300
|
+
if (existingAgain && !existingAgain.destroyed) {
|
|
1301
|
+
if (stream.stream.destroy) stream.stream.destroy();
|
|
1302
|
+
return { stream: existingAgain, type: "arbitrary" };
|
|
1303
|
+
}
|
|
1304
|
+
// Register with StreamManager
|
|
1305
|
+
const streamId = this.streamManager.registerStream(stream.stream, track, {
|
|
1306
|
+
source: track.source || "plugin",
|
|
1307
|
+
isPreload: false,
|
|
1308
|
+
priority: 5,
|
|
1309
|
+
});
|
|
1310
|
+
this.debug(`[Stream] Plugin stream registered with ID: ${streamId}`);
|
|
1311
|
+
return stream;
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
if (!this.pluginManager.hasStreamCandidate(track)) {
|
|
1315
|
+
throw new Error(`UNRECOVERABLE_NO_PLUGIN:${track.title}`);
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
throw new Error(`No stream available for track: ${track.title}`);
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
private isUnrecoverableStreamError(error: unknown): boolean {
|
|
1322
|
+
if (!(error instanceof Error)) return false;
|
|
1323
|
+
return error.message.startsWith("UNRECOVERABLE_NO_PLUGIN:");
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
/**
|
|
1327
|
+
* Start playing a specific track immediately, replacing the current resource.
|
|
1328
|
+
*/
|
|
1329
|
+
private async startTrack(track: Track): Promise<boolean> {
|
|
1330
|
+
try {
|
|
1331
|
+
// Try to use preloaded resource
|
|
1332
|
+
if (
|
|
1333
|
+
this.preloadSlot.isValid &&
|
|
1334
|
+
this.preloadSlot.track?.id === track.id &&
|
|
1335
|
+
this.preloadSlot.resource &&
|
|
1336
|
+
this.preloadSlot.resource.playStream?.readable !== false
|
|
1337
|
+
) {
|
|
1338
|
+
this.debug(`[Player] Using preloaded stream for: ${track.title}`);
|
|
1339
|
+
|
|
1340
|
+
// Stop current playback
|
|
1341
|
+
this.audioPlayer.stop(true);
|
|
1342
|
+
|
|
1343
|
+
// Clean up old current stream (but delay to be safe)
|
|
1344
|
+
const oldStreamId = this.currentSlot.streamId;
|
|
1345
|
+
if (oldStreamId && this.streamManager) {
|
|
1346
|
+
setTimeout(() => {
|
|
1347
|
+
if (this.currentSlot.streamId === oldStreamId) {
|
|
1348
|
+
this.streamManager.unregisterStream(oldStreamId, true);
|
|
1349
|
+
}
|
|
1350
|
+
}, 3000);
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
// Set current slot from preload
|
|
1354
|
+
this.promotePreloadToCurrent(track);
|
|
1355
|
+
const currentResource = this.currentSlot.resource;
|
|
1356
|
+
if (!currentResource) {
|
|
1357
|
+
return false;
|
|
1358
|
+
}
|
|
1359
|
+
const targetVolume = this.getTrackTargetVolume(track);
|
|
1360
|
+
|
|
1361
|
+
// Apply volume
|
|
1362
|
+
if (currentResource.volume) {
|
|
1363
|
+
currentResource.volume.setVolume(this.crossfadeEnabled ? 0 : targetVolume);
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
// Play
|
|
1367
|
+
await this.maybeAlignToBeatBoundary();
|
|
1368
|
+
this.audioPlayer.play(currentResource);
|
|
1369
|
+
await entersState(this.audioPlayer, AudioPlayerStatus.Playing, 10_000);
|
|
1370
|
+
await this.applyCrossfadeIn(currentResource, track);
|
|
1371
|
+
|
|
1372
|
+
// Start preloading next track (async, don't await)
|
|
1373
|
+
this.preloadNextTrack().catch((err) => {
|
|
1374
|
+
this.debug(`[Player] Preload error:`, err);
|
|
1375
|
+
});
|
|
1376
|
+
|
|
1377
|
+
return true;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
// No valid preload, load fresh
|
|
1381
|
+
this.debug(`[Player] No preload available, loading fresh: ${track.title}`);
|
|
1382
|
+
return await this.loadFreshStream(track);
|
|
1383
|
+
} catch (error) {
|
|
1384
|
+
this.debug(`[Player] startTrack error:`, error);
|
|
1385
|
+
this.emit("playerError", error as Error, track);
|
|
1386
|
+
return false;
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
/**
|
|
1391
|
+
* Swap preload slot to current slot
|
|
1392
|
+
*/
|
|
1393
|
+
private async swapToCurrent(track: Track): Promise<boolean> {
|
|
1394
|
+
// Store preload resource
|
|
1395
|
+
const newResource = this.preloadSlot.resource;
|
|
1396
|
+
const oldStreamId = this.currentSlot.streamId;
|
|
1397
|
+
|
|
1398
|
+
if (!newResource) {
|
|
1399
|
+
return false;
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
// Stop current playback
|
|
1403
|
+
this.audioPlayer.stop(true);
|
|
1404
|
+
|
|
1405
|
+
// Clean up old current stream (but keep it for a moment)
|
|
1406
|
+
if (oldStreamId && this.streamManager) {
|
|
1407
|
+
// Delay cleanup to avoid destroying if still needed
|
|
1408
|
+
setTimeout(() => {
|
|
1409
|
+
if (this.currentSlot.streamId === oldStreamId) {
|
|
1410
|
+
this.streamManager.unregisterStream(oldStreamId, true);
|
|
1411
|
+
}
|
|
1412
|
+
}, 5000);
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
// Set new current
|
|
1416
|
+
this.promotePreloadToCurrent(track);
|
|
1417
|
+
const currentResource = this.currentSlot.resource;
|
|
1418
|
+
if (!currentResource) {
|
|
1419
|
+
return false;
|
|
1420
|
+
}
|
|
1421
|
+
const targetVolume = this.getTrackTargetVolume(track);
|
|
1422
|
+
|
|
1423
|
+
// Apply volume
|
|
1424
|
+
if (currentResource.volume) {
|
|
1425
|
+
currentResource.volume.setVolume(this.crossfadeEnabled ? 0 : targetVolume);
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
// Play
|
|
1429
|
+
await this.maybeAlignToBeatBoundary();
|
|
1430
|
+
this.audioPlayer.play(currentResource);
|
|
1431
|
+
|
|
1432
|
+
try {
|
|
1433
|
+
await entersState(this.audioPlayer, AudioPlayerStatus.Playing, 10_000);
|
|
1434
|
+
await this.applyCrossfadeIn(currentResource, track);
|
|
1435
|
+
|
|
1436
|
+
// Start preloading next track
|
|
1437
|
+
this.preloadNextTrack().catch((err) => {
|
|
1438
|
+
this.debug(`[Player] Preload error:`, err);
|
|
1439
|
+
});
|
|
1440
|
+
|
|
1441
|
+
return true;
|
|
1442
|
+
} catch (err) {
|
|
1443
|
+
this.debug(`[Player] Failed to play swapped track:`, err);
|
|
1444
|
+
return false;
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
/**
|
|
1449
|
+
* Load fresh stream when no preload available
|
|
1450
|
+
*/
|
|
1451
|
+
private async loadFreshStream(track: Track): Promise<boolean> {
|
|
1452
|
+
// Cancel preload to free resources
|
|
1453
|
+
await this.safeCancelPreload();
|
|
1454
|
+
|
|
1455
|
+
try {
|
|
1456
|
+
const streamInfo = await this.getStream(track);
|
|
1457
|
+
|
|
1458
|
+
if (!streamInfo?.stream) {
|
|
1459
|
+
throw new Error(`No stream available`);
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
// Register with StreamManager
|
|
1463
|
+
const streamId = this.streamManager.registerStream(streamInfo.stream, track, {
|
|
1464
|
+
source: track.source || "stream",
|
|
1465
|
+
isPreload: false,
|
|
1466
|
+
priority: 10,
|
|
1467
|
+
});
|
|
1468
|
+
|
|
1469
|
+
// Create resource
|
|
1470
|
+
const resource = await this.createResource(streamInfo, track, 0);
|
|
1471
|
+
|
|
1472
|
+
// Clean up old current
|
|
1473
|
+
if (this.currentSlot.streamId && this.currentSlot.streamId !== streamId) {
|
|
1474
|
+
this.streamManager.unregisterStream(this.currentSlot.streamId, true);
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
// Set current slot
|
|
1478
|
+
this.currentSlot.resource = resource;
|
|
1479
|
+
this.currentSlot.track = track;
|
|
1480
|
+
this.currentSlot.streamId = streamId;
|
|
1481
|
+
this.currentSlot.isValid = true;
|
|
1482
|
+
this.currentResource = resource;
|
|
1483
|
+
|
|
1484
|
+
// Apply volume
|
|
1485
|
+
const targetVolume = this.getTrackTargetVolume(track);
|
|
1486
|
+
if (resource.volume) {
|
|
1487
|
+
resource.volume.setVolume(this.crossfadeEnabled ? 0 : targetVolume);
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
// Play
|
|
1491
|
+
await this.maybeAlignToBeatBoundary();
|
|
1492
|
+
this.audioPlayer.stop(true);
|
|
1493
|
+
this.audioPlayer.play(resource);
|
|
1494
|
+
await entersState(this.audioPlayer, AudioPlayerStatus.Playing, 10_000);
|
|
1495
|
+
await this.applyCrossfadeIn(resource, track);
|
|
1496
|
+
|
|
1497
|
+
// Preload next (async)
|
|
1498
|
+
this.preloadNextTrack().catch((err) => {
|
|
1499
|
+
this.debug(`[Player] Preload error:`, err);
|
|
1500
|
+
});
|
|
1501
|
+
|
|
1502
|
+
return true;
|
|
1503
|
+
} catch (error) {
|
|
1504
|
+
this.debug(`[Player] loadFreshStream error:`, error);
|
|
1505
|
+
throw error;
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
/**
|
|
1510
|
+
* Play the next track in the queue, handling errors and edge cases gracefully
|
|
1511
|
+
*/
|
|
1512
|
+
private async playNext(): Promise<boolean> {
|
|
1513
|
+
this.debug("[Player] playNext called");
|
|
1514
|
+
|
|
1515
|
+
// Don't cancel preload here unless absolutely necessary
|
|
1516
|
+
// Let startTrack handle it
|
|
1517
|
+
|
|
1518
|
+
while (true) {
|
|
1519
|
+
const track = this.queue.next(this.skipLoop);
|
|
1520
|
+
this.skipLoop = false;
|
|
1521
|
+
|
|
1522
|
+
if (!track) {
|
|
1523
|
+
if (this.queue.autoPlay()) {
|
|
1524
|
+
const willnext = this.queue.willNextTrack();
|
|
1525
|
+
if (willnext) {
|
|
1526
|
+
this.queue.addMultiple([willnext]);
|
|
1527
|
+
continue;
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
this.debug(`[Player] No next track in queue`);
|
|
1532
|
+
this.isPlaying = false;
|
|
1533
|
+
this.emit("queueEnd");
|
|
1534
|
+
|
|
1535
|
+
// Clean up both slots when queue is empty
|
|
1536
|
+
this.clearSlot(this.currentSlot);
|
|
1537
|
+
await this.safeCancelPreload();
|
|
1538
|
+
|
|
1539
|
+
if (this.options.leaveOnEnd) {
|
|
1540
|
+
this.scheduleLeave();
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
return false;
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
this.generateWillNext().catch((err) => this.debug("[Player] generateWillNext error:", err));
|
|
1547
|
+
this.clearLeaveTimeout();
|
|
1548
|
+
this.debug(`[Player] playNext called for track: ${track.title}`);
|
|
1549
|
+
|
|
1550
|
+
try {
|
|
1551
|
+
const started = await this.startTrack(track);
|
|
1552
|
+
if (started) {
|
|
1553
|
+
this.antiStuckConsecutiveFailures = 0;
|
|
1554
|
+
return true;
|
|
1555
|
+
}
|
|
1556
|
+
const recovered = await this.attemptTrackRecovery(track, new Error("TRACK_START_RETURNED_FALSE"));
|
|
1557
|
+
if (recovered) {
|
|
1558
|
+
return true;
|
|
1559
|
+
}
|
|
1560
|
+
if (this.antiStuckEnabled && this.antiStuckConsecutiveFailures < this.antiStuckControlledSkipThreshold) {
|
|
1561
|
+
this.queue.insert(track, 0);
|
|
1562
|
+
if (this.antiStuckRetryDelayMs > 0) {
|
|
1563
|
+
await new Promise((resolve) => setTimeout(resolve, this.antiStuckRetryDelayMs));
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
} catch (err) {
|
|
1567
|
+
this.debug(`[Player] playNext error:`, err);
|
|
1568
|
+
this.emit("playerError", err as Error, track);
|
|
1569
|
+
if (this.isUnrecoverableStreamError(err)) {
|
|
1570
|
+
this.debug(`[Player] Skipping unrecoverable track (no plugin): ${track.title}`);
|
|
1571
|
+
continue;
|
|
1572
|
+
}
|
|
1573
|
+
const recovered = await this.attemptTrackRecovery(track, err);
|
|
1574
|
+
if (recovered) {
|
|
1575
|
+
return true;
|
|
1576
|
+
}
|
|
1577
|
+
if (this.antiStuckEnabled && this.antiStuckConsecutiveFailures < this.antiStuckControlledSkipThreshold) {
|
|
1578
|
+
this.queue.insert(track, 0);
|
|
1579
|
+
if (this.antiStuckRetryDelayMs > 0) {
|
|
1580
|
+
await new Promise((resolve) => setTimeout(resolve, this.antiStuckRetryDelayMs));
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
continue;
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
//#endregion
|
|
1589
|
+
//#region TTS
|
|
1590
|
+
|
|
1591
|
+
private ensureTTSPlayer(): DiscordAudioPlayer {
|
|
1592
|
+
if (this.ttsPlayer) return this.ttsPlayer;
|
|
1593
|
+
this.ttsPlayer = createAudioPlayer({
|
|
1594
|
+
behaviors: {
|
|
1595
|
+
noSubscriber: NoSubscriberBehavior.Pause,
|
|
1596
|
+
maxMissedFrames: 100,
|
|
1597
|
+
},
|
|
1598
|
+
});
|
|
1599
|
+
this.ttsPlayer.on("error", (e) => this.debug("[TTS] error:", e));
|
|
1600
|
+
return this.ttsPlayer;
|
|
1601
|
+
}
|
|
1602
|
+
/**
|
|
1603
|
+
* Interrupt current music with a TTS track. Pauses music, swaps the
|
|
1604
|
+
* subscription to a dedicated TTS player, plays TTS, then resumes.
|
|
1605
|
+
*
|
|
1606
|
+
* @param {Track} track - The track to interrupt with
|
|
1607
|
+
* @returns {Promise<void>}
|
|
1608
|
+
* @example
|
|
1609
|
+
* await player.interruptWithTTSTrack(track);
|
|
1610
|
+
*/
|
|
1611
|
+
public async interruptWithTTSTrack(track: Track): Promise<void> {
|
|
1612
|
+
const wasPlaying =
|
|
1613
|
+
this.audioPlayer.state.status === AudioPlayerStatus.Playing ||
|
|
1614
|
+
this.audioPlayer.state.status === AudioPlayerStatus.Buffering;
|
|
1615
|
+
|
|
1616
|
+
let ttsResource: AudioResource | null = null;
|
|
1617
|
+
let ttsStream: any = null;
|
|
1618
|
+
|
|
1619
|
+
try {
|
|
1620
|
+
if (!this.connection) throw new Error("No voice connection for TTS");
|
|
1621
|
+
const ttsPlayer = this.ensureTTSPlayer();
|
|
1622
|
+
|
|
1623
|
+
// Build resource from plugin stream
|
|
1624
|
+
const streamInfo = await this.pluginManager.getStream(track);
|
|
1625
|
+
if (!streamInfo) {
|
|
1626
|
+
throw new Error(`No stream available for track: ${track.title}`);
|
|
1627
|
+
}
|
|
1628
|
+
ttsStream = streamInfo.stream;
|
|
1629
|
+
const resource = await this.createResource(streamInfo as StreamInfo, track);
|
|
1630
|
+
if (!resource) {
|
|
1631
|
+
throw new Error(`No resource available for track: ${track.title}`);
|
|
1632
|
+
}
|
|
1633
|
+
ttsResource = resource;
|
|
1634
|
+
if (resource.volume) {
|
|
1635
|
+
resource.volume.setVolume((this.options?.tts?.volume ?? this?.volume ?? 100) / 100);
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
// Pause current music if any
|
|
1639
|
+
try {
|
|
1640
|
+
this.pause();
|
|
1641
|
+
} catch {}
|
|
1642
|
+
|
|
1643
|
+
// Swap subscription and play TTS
|
|
1644
|
+
this.connection.subscribe(ttsPlayer);
|
|
1645
|
+
this.emit("ttsStart", { track });
|
|
1646
|
+
ttsPlayer.play(resource);
|
|
1647
|
+
|
|
1648
|
+
// Wait until TTS starts then finishes
|
|
1649
|
+
await entersState(ttsPlayer, AudioPlayerStatus.Playing, 5_000).catch(() => null);
|
|
1650
|
+
// Derive timeoutMs from resource/track duration when available, with a sensible cap
|
|
1651
|
+
const md: any = (resource as any)?.metadata ?? {};
|
|
1652
|
+
const declared =
|
|
1653
|
+
typeof md.duration === "number" ? md.duration
|
|
1654
|
+
: typeof track?.duration === "number" ? track.duration
|
|
1655
|
+
: undefined;
|
|
1656
|
+
const declaredMs =
|
|
1657
|
+
declared ?
|
|
1658
|
+
declared > 1000 ?
|
|
1659
|
+
declared
|
|
1660
|
+
: declared * 1000
|
|
1661
|
+
: undefined;
|
|
1662
|
+
const cap = this.options?.tts?.maxTimeTts ?? 60_000;
|
|
1663
|
+
const idleTimeout = declaredMs ? Math.min(cap, Math.max(1_000, declaredMs + 1_500)) : cap;
|
|
1664
|
+
await entersState(ttsPlayer, AudioPlayerStatus.Idle, idleTimeout).catch(() => null);
|
|
1665
|
+
|
|
1666
|
+
// Swap back and resume if needed
|
|
1667
|
+
this.connection.subscribe(this.audioPlayer);
|
|
1668
|
+
} catch (err) {
|
|
1669
|
+
this.debug("[TTS] error while playing:", err);
|
|
1670
|
+
this.emit("playerError", err as Error);
|
|
1671
|
+
} finally {
|
|
1672
|
+
// Clean up TTS stream and resource
|
|
1673
|
+
try {
|
|
1674
|
+
if (ttsStream && typeof ttsStream.destroy === "function") {
|
|
1675
|
+
ttsStream.destroy();
|
|
1676
|
+
}
|
|
1677
|
+
} catch (error) {
|
|
1678
|
+
this.debug("[TTS] Error destroying stream:", error);
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
if (wasPlaying) {
|
|
1682
|
+
try {
|
|
1683
|
+
this.resume();
|
|
1684
|
+
} catch {}
|
|
1685
|
+
}
|
|
1686
|
+
this.emit("ttsEnd");
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
//#endregion
|
|
1691
|
+
//#region Player Function
|
|
1692
|
+
|
|
1693
|
+
/**
|
|
1694
|
+
* Connect to a voice channel
|
|
1695
|
+
*
|
|
1696
|
+
* @param {VoiceChannel} channel - Discord voice channel
|
|
1697
|
+
* @returns {Promise<VoiceConnection>} The voice connection
|
|
1698
|
+
* @example
|
|
1699
|
+
* await player.connect(voiceChannel);
|
|
1700
|
+
*/
|
|
1701
|
+
async connect(channel: VoiceChannel): Promise<VoiceConnection> {
|
|
1702
|
+
try {
|
|
1703
|
+
this.debug(`[Player] Connecting to voice channel: ${channel.id}`);
|
|
1704
|
+
const connection = joinVoiceChannel({
|
|
1705
|
+
channelId: channel.id,
|
|
1706
|
+
guildId: channel.guildId,
|
|
1707
|
+
adapterCreator: channel.guild.voiceAdapterCreator as any,
|
|
1708
|
+
selfDeaf: this.options.selfDeaf ?? true,
|
|
1709
|
+
selfMute: this.options.selfMute ?? false,
|
|
1710
|
+
});
|
|
1711
|
+
|
|
1712
|
+
await entersState(connection, VoiceConnectionStatus.Ready, 50_000);
|
|
1713
|
+
this.connection = connection;
|
|
1714
|
+
|
|
1715
|
+
connection.on(VoiceConnectionStatus.Disconnected, async () => {
|
|
1716
|
+
try {
|
|
1717
|
+
// move channel
|
|
1718
|
+
await Promise.race([
|
|
1719
|
+
entersState(connection, VoiceConnectionStatus.Signalling, 5_000),
|
|
1720
|
+
entersState(connection, VoiceConnectionStatus.Connecting, 5_000),
|
|
1721
|
+
]);
|
|
1722
|
+
// Signalling/Connecting → reconnect
|
|
1723
|
+
this.debug(`[Player] Reconnecting after channel move...`);
|
|
1724
|
+
} catch {
|
|
1725
|
+
// no reconnect in 5 giây → disconnect
|
|
1726
|
+
this.debug(`[Player] Truly disconnected, destroying player`);
|
|
1727
|
+
this.destroy();
|
|
1728
|
+
}
|
|
1729
|
+
});
|
|
1730
|
+
|
|
1731
|
+
connection.on("error", (error) => {
|
|
1732
|
+
this.debug(`[Player] Voice connection error:`, error);
|
|
1733
|
+
this.emit("connectionError", error);
|
|
1734
|
+
});
|
|
1735
|
+
connection.subscribe(this.audioPlayer);
|
|
1736
|
+
|
|
1737
|
+
this.clearLeaveTimeout();
|
|
1738
|
+
return this.connection;
|
|
1739
|
+
} catch (error) {
|
|
1740
|
+
this.debug(`[Player] Connection error:`, error);
|
|
1741
|
+
this.emit("connectionError", error as Error);
|
|
1742
|
+
this.connection?.destroy();
|
|
1743
|
+
throw error;
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
/**
|
|
1748
|
+
* Pause the current track
|
|
1749
|
+
*
|
|
1750
|
+
* @returns {boolean} True if paused successfully
|
|
1751
|
+
* @example
|
|
1752
|
+
* const paused = player.pause();
|
|
1753
|
+
* console.log(`Paused: ${paused}`);
|
|
1754
|
+
*/
|
|
1755
|
+
pause(): boolean {
|
|
1756
|
+
this.debug(`[Player] pause called`);
|
|
1757
|
+
if (this.isPlaying && !this.isPaused) {
|
|
1758
|
+
return this.audioPlayer.pause();
|
|
1759
|
+
}
|
|
1760
|
+
return false;
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
/**
|
|
1764
|
+
* Resume the current track
|
|
1765
|
+
*
|
|
1766
|
+
* @returns {boolean} True if resumed successfully
|
|
1767
|
+
* @example
|
|
1768
|
+
* const resumed = player.resume();
|
|
1769
|
+
* console.log(`Resumed: ${resumed}`);
|
|
1770
|
+
*/
|
|
1771
|
+
resume(): boolean {
|
|
1772
|
+
this.debug(`[Player] resume called`);
|
|
1773
|
+
if (this.isPaused) {
|
|
1774
|
+
const result = this.audioPlayer.unpause();
|
|
1775
|
+
if (result) {
|
|
1776
|
+
const track = this.queue.currentTrack;
|
|
1777
|
+
if (track) {
|
|
1778
|
+
this.debug(`[Player] Player resumed on track: ${track.title}`);
|
|
1779
|
+
// this.emit("playerResume", track); //đã có trong stateChange
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
return result;
|
|
1783
|
+
}
|
|
1784
|
+
return false;
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
/**
|
|
1788
|
+
* Stop the current track
|
|
1789
|
+
*
|
|
1790
|
+
* @returns {boolean} True if stopped successfully
|
|
1791
|
+
* @example
|
|
1792
|
+
* const stopped = player.stop();
|
|
1793
|
+
* console.log(`Stopped: ${stopped}`);
|
|
1794
|
+
*/
|
|
1795
|
+
stop(): boolean {
|
|
1796
|
+
this.debug(`[Player] stop called`);
|
|
1797
|
+
|
|
1798
|
+
// Cancel preload when stopping
|
|
1799
|
+
this.cancelPreload();
|
|
1800
|
+
|
|
1801
|
+
this.queue.clear();
|
|
1802
|
+
const result = this.audioPlayer.stop();
|
|
1803
|
+
this.destroyCurrentStream();
|
|
1804
|
+
this.currentResource = null;
|
|
1805
|
+
|
|
1806
|
+
this.isPlaying = false;
|
|
1807
|
+
this.isPaused = false;
|
|
1808
|
+
this.emit("playerStop");
|
|
1809
|
+
return result;
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
/**
|
|
1813
|
+
* Seek to a specific position in the current track
|
|
1814
|
+
*
|
|
1815
|
+
* @param {number} position - Position in milliseconds to seek to
|
|
1816
|
+
* @returns {Promise<boolean>} True if seek was successful
|
|
1817
|
+
* @example
|
|
1818
|
+
* // Seek to 30 seconds (30000ms)
|
|
1819
|
+
* const success = await player.seek(30000);
|
|
1820
|
+
* console.log(`Seek successful: ${success}`);
|
|
1821
|
+
*
|
|
1822
|
+
* // Seek to 1 minute 30 seconds (90000ms)
|
|
1823
|
+
* await player.seek(90000);
|
|
1824
|
+
*/
|
|
1825
|
+
async seek(position: number): Promise<boolean> {
|
|
1826
|
+
this.debug(`[Player] seek called with position: ${position}ms`);
|
|
1827
|
+
|
|
1828
|
+
const track = this.queue.currentTrack;
|
|
1829
|
+
if (!track) {
|
|
1830
|
+
this.debug(`[Player] No current track to seek`);
|
|
1831
|
+
return false;
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
const totalDuration = track.duration > 1000 ? track.duration : track.duration * 1000;
|
|
1835
|
+
if (position < 0 || position > totalDuration) {
|
|
1836
|
+
this.debug(`[Player] Invalid seek position: ${position}ms (track duration: ${totalDuration}ms)`);
|
|
1837
|
+
return false;
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
await this.refreshPlayerResource(true, position);
|
|
1841
|
+
|
|
1842
|
+
return true;
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
/**
|
|
1846
|
+
* Skip to the next track or skip to a specific index
|
|
1847
|
+
*
|
|
1848
|
+
* @param {number} index - Optional index to skip to (0 = next track)
|
|
1849
|
+
* @returns {boolean} True if skipped successfully
|
|
1850
|
+
* @example
|
|
1851
|
+
* const skipped = player.skip(); // Skip to next track
|
|
1852
|
+
* const skippedToIndex = player.skip(2); // Skip to track at index 2
|
|
1853
|
+
* console.log(`Skipped: ${skipped}`);
|
|
1854
|
+
*/
|
|
1855
|
+
skip(index?: number): boolean {
|
|
1856
|
+
this.debug(`[Player] skip called with index: ${index}`);
|
|
1857
|
+
|
|
1858
|
+
try {
|
|
1859
|
+
if (typeof index === "number" && index >= 0) {
|
|
1860
|
+
const targetTrack = this.queue.getTrack(index);
|
|
1861
|
+
if (!targetTrack) {
|
|
1862
|
+
this.debug(`[Player] No track found at index ${index}`);
|
|
1863
|
+
return false;
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
for (let i = 0; i < index; i++) {
|
|
1867
|
+
this.queue.remove(0);
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
this.debug(`[Player] Skipped to track at index ${index}: ${targetTrack.title}`);
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
if (this.isPlaying || this.isPaused) {
|
|
1874
|
+
this.skipLoop = true;
|
|
1875
|
+
void this.crossfadeSkipAndStop().catch((error) => {
|
|
1876
|
+
this.debug(`[Player] crossfade skip error:`, error);
|
|
1877
|
+
this.audioPlayer.stop();
|
|
1878
|
+
});
|
|
1879
|
+
return true;
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
return true;
|
|
1883
|
+
} catch (error) {
|
|
1884
|
+
this.debug(`[Player] skip error:`, error);
|
|
1885
|
+
return false;
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
/**
|
|
1890
|
+
* Go back to the previous track in history and play it.
|
|
1891
|
+
*
|
|
1892
|
+
* @returns {Promise<boolean>} True if previous track was played successfully
|
|
1893
|
+
* @example
|
|
1894
|
+
* const previous = await player.previous();
|
|
1895
|
+
* console.log(`Previous: ${previous}`);
|
|
1896
|
+
*/
|
|
1897
|
+
async previous(): Promise<boolean> {
|
|
1898
|
+
this.debug(`[Player] previous called`);
|
|
1899
|
+
const track = this.queue.previous();
|
|
1900
|
+
if (!track) return false;
|
|
1901
|
+
if (this.queue.currentTrack) this.insert(this.queue.currentTrack, 0);
|
|
1902
|
+
this.clearLeaveTimeout();
|
|
1903
|
+
return this.startTrack(track);
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
/**
|
|
1907
|
+
* Save a track's stream to a file and return a Readable stream
|
|
1908
|
+
*
|
|
1909
|
+
* @param {Track} track - The track to save
|
|
1910
|
+
* @param {SaveOptions | string} options - Save options or filename string (for backward compatibility)
|
|
1911
|
+
* @returns {Promise<Readable>} A Readable stream containing the audio data
|
|
1912
|
+
* @example
|
|
1913
|
+
* // Save current track to file
|
|
1914
|
+
* const track = player.currentTrack;
|
|
1915
|
+
* if (track) {
|
|
1916
|
+
* const stream = await player.save(track);
|
|
1917
|
+
*
|
|
1918
|
+
* // Use fs to write the stream to file
|
|
1919
|
+
* const fs = require('fs');
|
|
1920
|
+
* const writeStream = fs.createWriteStream('saved-song.mp3');
|
|
1921
|
+
* stream.pipe(writeStream);
|
|
1922
|
+
*
|
|
1923
|
+
* writeStream.on('finish', () => {
|
|
1924
|
+
* console.log('File saved successfully!');
|
|
1925
|
+
* });
|
|
1926
|
+
* }
|
|
1927
|
+
*
|
|
1928
|
+
* // Save any track by URL
|
|
1929
|
+
* const searchResult = await player.search("Never Gonna Give You Up", userId);
|
|
1930
|
+
* if (searchResult.tracks.length > 0) {
|
|
1931
|
+
* const stream = await player.save(searchResult.tracks[0]);
|
|
1932
|
+
* // Handle the stream...
|
|
1933
|
+
* }
|
|
1934
|
+
*
|
|
1935
|
+
* // Backward compatibility - filename as string
|
|
1936
|
+
* const stream = await player.save(track, "my-song.mp3");
|
|
1937
|
+
*/
|
|
1938
|
+
async save(track: Track, options?: SaveOptions | string): Promise<Readable> {
|
|
1939
|
+
this.debug(`[Player] save called for track: ${track.title}`);
|
|
1940
|
+
|
|
1941
|
+
// Parse options - support both SaveOptions object and filename string (backward compatibility)
|
|
1942
|
+
let saveOptions: SaveOptions = {};
|
|
1943
|
+
if (typeof options === "string") {
|
|
1944
|
+
saveOptions = { filename: options };
|
|
1945
|
+
} else if (options) {
|
|
1946
|
+
saveOptions = options;
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
try {
|
|
1950
|
+
// Skip extension manager for saving - we want the raw stream without filters/seek applied, and extensions may not support this
|
|
1951
|
+
let streamInfo: StreamInfo | null = await this.pluginManager.getStream(track);
|
|
1952
|
+
|
|
1953
|
+
if (!streamInfo || !streamInfo.stream) {
|
|
1954
|
+
throw new Error(`No save stream available for track: ${track.title}`);
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
this.debug(`[Player] Save stream obtained for track: ${track.title}`);
|
|
1958
|
+
if (saveOptions.filename) {
|
|
1959
|
+
this.debug(`[Player] Save options - filename: ${saveOptions.filename}, quality: ${saveOptions.quality || "default"}`);
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
// Apply filters if any are active
|
|
1963
|
+
let finalStream = streamInfo.stream;
|
|
1964
|
+
|
|
1965
|
+
if (saveOptions.filter || saveOptions.seek) {
|
|
1966
|
+
try {
|
|
1967
|
+
this.filter.clearAll();
|
|
1968
|
+
this.filter.applyFilters(saveOptions.filter || []);
|
|
1969
|
+
} catch (err) {
|
|
1970
|
+
this.debug(`[Player] Error applying save filters:`, err);
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
this.debug(`[Player] Applying filters to save stream: ${this.filter.getFilterString() || "none"}`);
|
|
1974
|
+
finalStream = await this.filter.applyFiltersAndSeek(streamInfo.stream, saveOptions.seek || 0).catch((err) => {
|
|
1975
|
+
this.debug(`[Player] Error applying filters to save stream:`, err);
|
|
1976
|
+
return streamInfo!.stream; // Fallback to original stream
|
|
1977
|
+
});
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
// Return the stream directly - caller can pipe it to fs.createWriteStream()
|
|
1981
|
+
return finalStream;
|
|
1982
|
+
} catch (error) {
|
|
1983
|
+
this.debug(`[Player] save error:`, error);
|
|
1984
|
+
this.emit("playerError", error as Error, track);
|
|
1985
|
+
throw error;
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
/**
|
|
1990
|
+
* Loop the current track or queue
|
|
1991
|
+
*
|
|
1992
|
+
* @param {LoopMode | number} mode - The loop mode to set ("off", "track", "queue") or number (0=off, 1=track, 2=queue)
|
|
1993
|
+
* @returns {LoopMode} The loop mode
|
|
1994
|
+
* @example
|
|
1995
|
+
* const loopMode = player.loop("track"); // Loop current track
|
|
1996
|
+
* const loopQueue = player.loop("queue"); // Loop entire queue
|
|
1997
|
+
* const loopTrack = player.loop(1); // Loop current track (same as "track")
|
|
1998
|
+
* const loopQueueNum = player.loop(2); // Loop entire queue (same as "queue")
|
|
1999
|
+
* const noLoop = player.loop("off"); // No loop
|
|
2000
|
+
* const noLoopNum = player.loop(0); // No loop (same as "off")
|
|
2001
|
+
* console.log(`Loop mode: ${loopMode}`);
|
|
2002
|
+
*/
|
|
2003
|
+
loop(mode?: LoopMode | number): LoopMode {
|
|
2004
|
+
this.debug(`[Player] loop called with mode: ${mode}`);
|
|
2005
|
+
|
|
2006
|
+
if (typeof mode === "number") {
|
|
2007
|
+
// Number mode: convert to text mode
|
|
2008
|
+
switch (mode) {
|
|
2009
|
+
case 0:
|
|
2010
|
+
return this.queue.loop("off");
|
|
2011
|
+
case 1:
|
|
2012
|
+
return this.queue.loop("track");
|
|
2013
|
+
case 2:
|
|
2014
|
+
return this.queue.loop("queue");
|
|
2015
|
+
default:
|
|
2016
|
+
this.debug(`[Player] Invalid loop number: ${mode}, using "off"`);
|
|
2017
|
+
return this.queue.loop("off");
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
return this.queue.loop(mode as LoopMode);
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
/**
|
|
2025
|
+
* Set the auto-play mode
|
|
2026
|
+
*
|
|
2027
|
+
* @param {boolean} mode - The auto-play mode to set
|
|
2028
|
+
* @returns {boolean} The auto-play mode
|
|
2029
|
+
* @example
|
|
2030
|
+
* const autoPlayMode = player.autoPlay(true);
|
|
2031
|
+
* console.log(`Auto-play mode: ${autoPlayMode}`);
|
|
2032
|
+
*/
|
|
2033
|
+
autoPlay(mode?: boolean): boolean {
|
|
2034
|
+
return this.queue.autoPlay(mode);
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
/**
|
|
2038
|
+
* Set the volume of the current track
|
|
2039
|
+
*
|
|
2040
|
+
* @param {number} volume - The volume to set
|
|
2041
|
+
* @returns {boolean} True if volume was set successfully
|
|
2042
|
+
* @example
|
|
2043
|
+
* const volumeSet = player.setVolume(50);
|
|
2044
|
+
* console.log(`Volume set: ${volumeSet}`);
|
|
2045
|
+
*/
|
|
2046
|
+
setVolume(volume: number): boolean {
|
|
2047
|
+
this.debug(`[Player] setVolume called: ${volume}`);
|
|
2048
|
+
if (volume < 0 || volume > 200) return false;
|
|
2049
|
+
|
|
2050
|
+
const oldVolume = this.volume;
|
|
2051
|
+
this.volume = volume;
|
|
2052
|
+
const resourceVolume = this.currentResource?.volume;
|
|
2053
|
+
|
|
2054
|
+
if (resourceVolume) {
|
|
2055
|
+
if (this.volumeInterval) clearInterval(this.volumeInterval);
|
|
2056
|
+
|
|
2057
|
+
const start = resourceVolume.volume;
|
|
2058
|
+
const target = this.volume / 100;
|
|
2059
|
+
const steps = 10;
|
|
2060
|
+
let currentStep = 0;
|
|
2061
|
+
|
|
2062
|
+
this.volumeInterval = setInterval(() => {
|
|
2063
|
+
currentStep++;
|
|
2064
|
+
const value = start + ((target - start) * currentStep) / steps;
|
|
2065
|
+
resourceVolume.setVolume(value);
|
|
2066
|
+
if (currentStep >= steps) {
|
|
2067
|
+
clearInterval(this.volumeInterval!);
|
|
2068
|
+
this.volumeInterval = null;
|
|
2069
|
+
}
|
|
2070
|
+
}, 300);
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
this.emit("volumeChange", oldVolume, volume);
|
|
2074
|
+
return true;
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
/**
|
|
2078
|
+
* Shuffle the queue
|
|
2079
|
+
*
|
|
2080
|
+
* @returns {void}
|
|
2081
|
+
* @example
|
|
2082
|
+
* player.shuffle();
|
|
2083
|
+
*/
|
|
2084
|
+
shuffle(): void {
|
|
2085
|
+
this.debug(`[Player] shuffle called`);
|
|
2086
|
+
this.queue.shuffle();
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
/**
|
|
2090
|
+
* Clear the queue
|
|
2091
|
+
*
|
|
2092
|
+
* @returns {void}
|
|
2093
|
+
* @example
|
|
2094
|
+
* player.clearQueue();
|
|
2095
|
+
*/
|
|
2096
|
+
clearQueue(): void {
|
|
2097
|
+
this.debug(`[Player] clearQueue called`);
|
|
2098
|
+
this.queue.clear();
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
/**
|
|
2102
|
+
* Insert a track or list of tracks into the upcoming queue at a specific position (0 = play after current).
|
|
2103
|
+
* - If `query` is a string, performs a search and inserts resulting tracks (playlist supported).
|
|
2104
|
+
* - If a Track or Track[] is provided, inserts directly.
|
|
2105
|
+
* Does not auto-start playback; it only modifies the queue.
|
|
2106
|
+
*
|
|
2107
|
+
* @param {string | Track | Track[]} query - The track or tracks to insert
|
|
2108
|
+
* @param {number} index - The index to insert the tracks at
|
|
2109
|
+
* @param {string} requestedBy - The user ID who requested the insert
|
|
2110
|
+
* @returns {Promise<boolean>} True if the tracks were inserted successfully
|
|
2111
|
+
* @example
|
|
2112
|
+
* const inserted = await player.insert("Song Name", 0, userId);
|
|
2113
|
+
* console.log(`Inserted: ${inserted}`);
|
|
2114
|
+
*/
|
|
2115
|
+
async insert(query: string | Track | Track[], index: number, requestedBy?: string): Promise<boolean> {
|
|
2116
|
+
try {
|
|
2117
|
+
this.debug(`[Player] insert called at index ${index} with type: ${typeof query}`);
|
|
2118
|
+
let tracksToAdd: Track[] = [];
|
|
2119
|
+
let isPlaylist = false;
|
|
2120
|
+
|
|
2121
|
+
if (typeof query === "string") {
|
|
2122
|
+
const searchResult = await this.search(query, requestedBy || "Unknown");
|
|
2123
|
+
tracksToAdd = searchResult.tracks || [];
|
|
2124
|
+
isPlaylist = !!searchResult.playlist;
|
|
2125
|
+
} else if (Array.isArray(query)) {
|
|
2126
|
+
tracksToAdd = query;
|
|
2127
|
+
isPlaylist = query.length > 1;
|
|
2128
|
+
} else if (query) {
|
|
2129
|
+
tracksToAdd = [query];
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
if (!tracksToAdd || tracksToAdd.length === 0) {
|
|
2133
|
+
this.debug(`[Player] insert: no tracks resolved`);
|
|
2134
|
+
throw new Error("No tracks to insert");
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
if (tracksToAdd.length === 1) {
|
|
2138
|
+
this.queue.insert(tracksToAdd[0], index);
|
|
2139
|
+
this.emit("queueAdd", tracksToAdd[0]);
|
|
2140
|
+
this.debug(`[Player] Inserted track at index ${index}: ${tracksToAdd[0].title}`);
|
|
2141
|
+
} else {
|
|
2142
|
+
this.queue.insertMultiple(tracksToAdd, index);
|
|
2143
|
+
this.emit("queueAddList", tracksToAdd);
|
|
2144
|
+
this.debug(`[Player] Inserted ${tracksToAdd.length} ${isPlaylist ? "playlist " : ""}tracks at index ${index}`);
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
return true;
|
|
2148
|
+
} catch (error) {
|
|
2149
|
+
this.debug(`[Player] insert error:`, error);
|
|
2150
|
+
this.emit("playerError", error as Error);
|
|
2151
|
+
return false;
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
/**
|
|
2156
|
+
* Remove a track from the queue
|
|
2157
|
+
*
|
|
2158
|
+
* @param {number} index - The index of the track to remove
|
|
2159
|
+
* @returns {Track | null} The removed track or null
|
|
2160
|
+
* @example
|
|
2161
|
+
* const removed = player.remove(0);
|
|
2162
|
+
* console.log(`Removed: ${removed?.title}`);
|
|
2163
|
+
*/
|
|
2164
|
+
remove(index: number): Track | null {
|
|
2165
|
+
this.debug(`[Player] remove called for index: ${index}`);
|
|
2166
|
+
const track = this.queue.remove(index);
|
|
2167
|
+
if (track) {
|
|
2168
|
+
this.emit("queueRemove", track, index);
|
|
2169
|
+
}
|
|
2170
|
+
return track;
|
|
2171
|
+
}
|
|
2172
|
+
/**
|
|
2173
|
+
* Get the progress bar of the current track
|
|
2174
|
+
*
|
|
2175
|
+
* @param {ProgressBarOptions} options - The options for the progress bar
|
|
2176
|
+
* @returns {string} The progress bar
|
|
2177
|
+
* @example
|
|
2178
|
+
* const progressBar = player.getProgressBar();
|
|
2179
|
+
* console.log(`Progress bar: ${progressBar}`);
|
|
2180
|
+
*
|
|
2181
|
+
* // Custom options
|
|
2182
|
+
* const customBar = player.getProgressBar({
|
|
2183
|
+
* size: 30,
|
|
2184
|
+
* barChar: "─",
|
|
2185
|
+
* progressChar: "●",
|
|
2186
|
+
* timeFormat: "compact" // "compact" = 1:22:12, "full" = 01:22:12
|
|
2187
|
+
* });
|
|
2188
|
+
*/
|
|
2189
|
+
getProgressBar(options: ProgressBarOptions = {}): string {
|
|
2190
|
+
const {
|
|
2191
|
+
size = 20,
|
|
2192
|
+
barChar = "▬",
|
|
2193
|
+
progressChar = "🔘",
|
|
2194
|
+
timeFormat = "compact", // "compact" or "full"
|
|
2195
|
+
showPercentage = false,
|
|
2196
|
+
showTime = true,
|
|
2197
|
+
} = options;
|
|
2198
|
+
|
|
2199
|
+
const track = this.queue.currentTrack;
|
|
2200
|
+
const resource = this.currentResource;
|
|
2201
|
+
|
|
2202
|
+
// Handle live stream
|
|
2203
|
+
if (this.isLive || !track || !resource) {
|
|
2204
|
+
if (this.isLive) return "🔴 LIVE";
|
|
2205
|
+
return "";
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
const total = track.duration > 1000 ? track.duration : track.duration * 1000;
|
|
2209
|
+
if (!total) return this.formatTimeCompact(resource.playbackDuration);
|
|
2210
|
+
|
|
2211
|
+
const current = resource.playbackDuration;
|
|
2212
|
+
const ratio = Math.min(Math.max(current / total, 0), 1);
|
|
2213
|
+
const progress = Math.round(ratio * size);
|
|
2214
|
+
|
|
2215
|
+
// Build progress bar
|
|
2216
|
+
let bar = "";
|
|
2217
|
+
if (progressChar === "none" || options.hideProgressChar) {
|
|
2218
|
+
// Continuous bar without separator
|
|
2219
|
+
const filled = barChar.repeat(progress);
|
|
2220
|
+
const empty = barChar.repeat(size - progress);
|
|
2221
|
+
bar = filled + empty;
|
|
2222
|
+
} else {
|
|
2223
|
+
// Bar with progress character
|
|
2224
|
+
const filled = barChar.repeat(progress);
|
|
2225
|
+
const empty = barChar.repeat(Math.max(0, size - progress));
|
|
2226
|
+
bar = filled + progressChar + empty;
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
// Format time based on option
|
|
2230
|
+
const formatTimeFn = timeFormat === "compact" ? this.formatTimeCompact.bind(this) : this.formatTime.bind(this);
|
|
2231
|
+
const currentTimeStr = formatTimeFn(current);
|
|
2232
|
+
const totalTimeStr = formatTimeFn(total);
|
|
2233
|
+
|
|
2234
|
+
// Build result
|
|
2235
|
+
let result = "";
|
|
2236
|
+
if (showTime) {
|
|
2237
|
+
result = `${currentTimeStr} ${bar} ${totalTimeStr}`;
|
|
2238
|
+
} else {
|
|
2239
|
+
result = bar;
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
// Add percentage if requested
|
|
2243
|
+
if (showPercentage) {
|
|
2244
|
+
const percent = Math.round(ratio * 100);
|
|
2245
|
+
result += ` (${percent}%)`;
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
return result;
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2251
|
+
/**
|
|
2252
|
+
* Format time with leading zeros (00:00 or 00:00:00)
|
|
2253
|
+
* @param ms - Time in milliseconds
|
|
2254
|
+
* @returns Formatted time string with leading zeros
|
|
2255
|
+
*/
|
|
2256
|
+
formatTime(ms: number): string {
|
|
2257
|
+
const totalSeconds = Math.floor(ms / 1000);
|
|
2258
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
2259
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
2260
|
+
const seconds = totalSeconds % 60;
|
|
2261
|
+
const parts: string[] = [];
|
|
2262
|
+
if (hours > 0) parts.push(String(hours).padStart(2, "0"));
|
|
2263
|
+
parts.push(String(minutes).padStart(2, "0"));
|
|
2264
|
+
parts.push(String(seconds).padStart(2, "0"));
|
|
2265
|
+
return parts.join(":");
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
/**
|
|
2269
|
+
* Format time without leading zeros for hours (1:22:12 or 3:45)
|
|
2270
|
+
* @param ms - Time in milliseconds
|
|
2271
|
+
* @returns Compact formatted time string
|
|
2272
|
+
*/
|
|
2273
|
+
formatTimeCompact(ms: number): string {
|
|
2274
|
+
const totalSeconds = Math.floor(ms / 1000);
|
|
2275
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
2276
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
2277
|
+
const seconds = totalSeconds % 60;
|
|
2278
|
+
|
|
2279
|
+
if (hours > 0) {
|
|
2280
|
+
return `${hours}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
|
|
2281
|
+
}
|
|
2282
|
+
return `${minutes}:${String(seconds).padStart(2, "0")}`;
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
/**
|
|
2286
|
+
* Get the time of the current track
|
|
2287
|
+
*
|
|
2288
|
+
* @returns {Object} The time of the current track
|
|
2289
|
+
* @example
|
|
2290
|
+
* const time = player.getTime();
|
|
2291
|
+
* console.log(`Time: ${time.current}`);
|
|
2292
|
+
* console.log(`Formatted: ${time.formatted.current}`); // "1:22:12" or "3:45"
|
|
2293
|
+
*/
|
|
2294
|
+
getTime() {
|
|
2295
|
+
if (this.isLive)
|
|
2296
|
+
return {
|
|
2297
|
+
current: 0,
|
|
2298
|
+
total: 0,
|
|
2299
|
+
format: "LIVE",
|
|
2300
|
+
formatted: {
|
|
2301
|
+
current: "LIVE",
|
|
2302
|
+
total: "LIVE",
|
|
2303
|
+
},
|
|
2304
|
+
};
|
|
2305
|
+
|
|
2306
|
+
const resource = this.currentResource;
|
|
2307
|
+
const track = this.queue.currentTrack;
|
|
2308
|
+
if (!track || !resource) {
|
|
2309
|
+
return {
|
|
2310
|
+
current: 0,
|
|
2311
|
+
total: 0,
|
|
2312
|
+
format: "00:00",
|
|
2313
|
+
formatted: {
|
|
2314
|
+
current: "00:00",
|
|
2315
|
+
total: "00:00",
|
|
2316
|
+
},
|
|
2317
|
+
};
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
const total = track.duration > 1000 ? track.duration : track.duration * 1000;
|
|
2321
|
+
const current = resource.playbackDuration;
|
|
2322
|
+
|
|
2323
|
+
return {
|
|
2324
|
+
current: current,
|
|
2325
|
+
total: total,
|
|
2326
|
+
format: this.formatTime(current),
|
|
2327
|
+
formatted: {
|
|
2328
|
+
current: this.formatTimeCompact(current),
|
|
2329
|
+
total: this.formatTimeCompact(total),
|
|
2330
|
+
},
|
|
2331
|
+
};
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
/**
|
|
2335
|
+
* Destroy the player
|
|
2336
|
+
*
|
|
2337
|
+
* @returns {void}
|
|
2338
|
+
* @example
|
|
2339
|
+
* player.destroy();
|
|
2340
|
+
*/
|
|
2341
|
+
destroy(): void {
|
|
2342
|
+
this.debug(`[Player] destroy called`);
|
|
2343
|
+
|
|
2344
|
+
if (this.leaveTimeout) {
|
|
2345
|
+
clearTimeout(this.leaveTimeout);
|
|
2346
|
+
this.leaveTimeout = null;
|
|
2347
|
+
}
|
|
2348
|
+
this.streamManager.destroyAll(true);
|
|
2349
|
+
// Destroy current stream before stopping audio
|
|
2350
|
+
this.destroyCurrentStream();
|
|
2351
|
+
|
|
2352
|
+
this.clearSlot(this.currentSlot);
|
|
2353
|
+
this.clearSlot(this.preloadSlot);
|
|
2354
|
+
|
|
2355
|
+
this.audioPlayer.removeAllListeners();
|
|
2356
|
+
this.audioPlayer.stop(true);
|
|
2357
|
+
this.clearPreload();
|
|
2358
|
+
|
|
2359
|
+
if (this.ttsPlayer) {
|
|
2360
|
+
try {
|
|
2361
|
+
this.ttsPlayer.stop(true);
|
|
2362
|
+
} catch {}
|
|
2363
|
+
this.ttsPlayer = null;
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
if (this.connection) {
|
|
2367
|
+
this.connection.destroy();
|
|
2368
|
+
this.connection = null;
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2371
|
+
this.queue.clear();
|
|
2372
|
+
this.pluginManager.clear();
|
|
2373
|
+
this.filter.destroy();
|
|
2374
|
+
this.extensionManager.destroy();
|
|
2375
|
+
this.isPlaying = false;
|
|
2376
|
+
this.isPaused = false;
|
|
2377
|
+
|
|
2378
|
+
// Clear any remaining intervals
|
|
2379
|
+
if (this.volumeInterval) {
|
|
2380
|
+
clearInterval(this.volumeInterval);
|
|
2381
|
+
this.volumeInterval = null;
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
this.emit("playerDestroy");
|
|
2385
|
+
this.removeAllListeners();
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2388
|
+
//#endregion
|
|
2389
|
+
//#region utils
|
|
2390
|
+
private scheduleLeave(): void {
|
|
2391
|
+
this.debug(`[Player] scheduleLeave called`);
|
|
2392
|
+
if (this.leaveTimeout) {
|
|
2393
|
+
clearTimeout(this.leaveTimeout);
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2396
|
+
if (this.options.leaveOnEnd && this.options.leaveTimeout) {
|
|
2397
|
+
this.leaveTimeout = setTimeout(() => {
|
|
2398
|
+
this.debug(`[Player] Leaving voice channel after timeoutMs`);
|
|
2399
|
+
this.destroy();
|
|
2400
|
+
}, this.options.leaveTimeout);
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2404
|
+
/**
|
|
2405
|
+
* Refesh player resource (apply filter)
|
|
2406
|
+
*
|
|
2407
|
+
* @param {boolean} applyToCurrent - Apply filter for curent track
|
|
2408
|
+
* @param {number} position - Position to seek to in milliseconds
|
|
2409
|
+
* @returns {Promise<boolean>}
|
|
2410
|
+
* @example
|
|
2411
|
+
* const refreshed = await player.refreshPlayerResource(true, 1000);
|
|
2412
|
+
* console.log(`Refreshed: ${refreshed}`);
|
|
2413
|
+
*/
|
|
2414
|
+
public async refreshPlayerResource(applyToCurrent: boolean = true, position: number = -1): Promise<boolean> {
|
|
2415
|
+
if (!applyToCurrent || !this.queue.currentTrack || !(this.isPlaying || this.isPaused)) {
|
|
2416
|
+
return false;
|
|
2417
|
+
}
|
|
2418
|
+
if (this.refreshLock) return false;
|
|
2419
|
+
this.refreshLock = true;
|
|
2420
|
+
try {
|
|
2421
|
+
const track = this.queue.currentTrack;
|
|
2422
|
+
this.debug(`[Player] Refreshing player resource for track: ${track.title}`);
|
|
2423
|
+
|
|
2424
|
+
// Get current position for seeking
|
|
2425
|
+
const currentPosition = position > 0 ? position : this.currentResource?.playbackDuration || 0;
|
|
2426
|
+
|
|
2427
|
+
const streaminfo = await this.getStream(track);
|
|
2428
|
+
if (!streaminfo?.stream) {
|
|
2429
|
+
this.debug(`[Player] No stream to refresh`);
|
|
2430
|
+
return false;
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2433
|
+
// Create AudioResource with filters and seek to current position
|
|
2434
|
+
const resource = await this.createResource(streaminfo, track, currentPosition);
|
|
2435
|
+
|
|
2436
|
+
// Stop current playback and destroy old resource/stream
|
|
2437
|
+
const wasPlaying = this.isPlaying;
|
|
2438
|
+
const wasPaused = this.isPaused;
|
|
2439
|
+
|
|
2440
|
+
this.audioPlayer.stop();
|
|
2441
|
+
|
|
2442
|
+
// Properly destroy the old resource and stream
|
|
2443
|
+
try {
|
|
2444
|
+
if (this.currentResource) {
|
|
2445
|
+
const oldStream = (this.currentResource as any)._readableState?.stream || (this.currentResource as any).stream;
|
|
2446
|
+
if (oldStream && typeof oldStream.destroy === "function") {
|
|
2447
|
+
oldStream.destroy();
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
} catch (error) {
|
|
2451
|
+
this.debug(`[Player] Error destroying old stream in refreshPlayerResource:`, error);
|
|
2452
|
+
} finally {
|
|
2453
|
+
this.refreshLock = false;
|
|
2454
|
+
}
|
|
2455
|
+
|
|
2456
|
+
this.currentResource = resource;
|
|
2457
|
+
|
|
2458
|
+
// Subscribe to new resource
|
|
2459
|
+
if (this.connection) {
|
|
2460
|
+
this.connection.subscribe(this.audioPlayer);
|
|
2461
|
+
this.audioPlayer.play(resource);
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
// Restore playing state
|
|
2465
|
+
if (wasPlaying && !wasPaused) {
|
|
2466
|
+
this.isPlaying = true;
|
|
2467
|
+
this.isPaused = false;
|
|
2468
|
+
} else if (wasPaused) {
|
|
2469
|
+
this.isPlaying = false;
|
|
2470
|
+
this.isPaused = true;
|
|
2471
|
+
this.audioPlayer.pause();
|
|
2472
|
+
}
|
|
2473
|
+
|
|
2474
|
+
this.debug(`[Player] Successfully applied filter to current track at position ${currentPosition}ms`);
|
|
2475
|
+
return true;
|
|
2476
|
+
} catch (error) {
|
|
2477
|
+
this.debug(`[Player] Error applying filter to current track:`, error);
|
|
2478
|
+
// Filter was still added to active filters, so return true
|
|
2479
|
+
return true;
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
|
|
2483
|
+
/**
|
|
2484
|
+
* Attach an extension to the player
|
|
2485
|
+
*
|
|
2486
|
+
* @param {BaseExtension} extension - The extension to attach
|
|
2487
|
+
* @example
|
|
2488
|
+
* player.attachExtension(new MyExtension());
|
|
2489
|
+
*/
|
|
2490
|
+
public attachExtension(extension: BaseExtension): void {
|
|
2491
|
+
this.extensionManager.register(extension);
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2494
|
+
/**
|
|
2495
|
+
* Detach an extension from the player
|
|
2496
|
+
*
|
|
2497
|
+
* @param {BaseExtension} extension - The extension to detach
|
|
2498
|
+
* @example
|
|
2499
|
+
* player.detachExtension(new MyExtension());
|
|
2500
|
+
*/
|
|
2501
|
+
public detachExtension(extension: BaseExtension): void {
|
|
2502
|
+
this.extensionManager.unregister(extension);
|
|
2503
|
+
}
|
|
2504
|
+
|
|
2505
|
+
/**
|
|
2506
|
+
* Get all extensions attached to the player
|
|
2507
|
+
*
|
|
2508
|
+
* @returns {readonly BaseExtension[]} All attached extensions
|
|
2509
|
+
* @example
|
|
2510
|
+
* const extensions = player.getExtensions();
|
|
2511
|
+
* console.log(`Extensions: ${extensions.length}`);
|
|
2512
|
+
*/
|
|
2513
|
+
public getExtensions(): readonly BaseExtension[] {
|
|
2514
|
+
return this.extensionManager.getAll();
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
private clearLeaveTimeout(): void {
|
|
2518
|
+
if (this.leaveTimeout) {
|
|
2519
|
+
clearTimeout(this.leaveTimeout);
|
|
2520
|
+
this.leaveTimeout = null;
|
|
2521
|
+
this.debug(`[Player] Cleared leave timeoutMs`);
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
private debug(message?: any, ...optionalParams: any[]): void {
|
|
2526
|
+
if (this.listenerCount("debug") > 0) {
|
|
2527
|
+
this.emit("debug", message, ...optionalParams);
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2531
|
+
private setupEventListeners(): void {
|
|
2532
|
+
this.audioPlayer.on("stateChange", (oldState, newState) => {
|
|
2533
|
+
this.debug(`[Player] AudioPlayer stateChange from ${oldState.status} to ${newState.status}`);
|
|
2534
|
+
if (newState.status === AudioPlayerStatus.Idle && oldState.status !== AudioPlayerStatus.Idle) {
|
|
2535
|
+
// Track ended
|
|
2536
|
+
const track = this.queue.currentTrack;
|
|
2537
|
+
if (track) {
|
|
2538
|
+
this.debug(`[Player] Track ended: ${track.title}`);
|
|
2539
|
+
this.emit("trackEnd", track);
|
|
2540
|
+
}
|
|
2541
|
+
this.playNext();
|
|
2542
|
+
} else if (
|
|
2543
|
+
newState.status === AudioPlayerStatus.Playing &&
|
|
2544
|
+
(oldState.status === AudioPlayerStatus.Idle || oldState.status === AudioPlayerStatus.Buffering)
|
|
2545
|
+
) {
|
|
2546
|
+
// Track started
|
|
2547
|
+
this.clearLeaveTimeout();
|
|
2548
|
+
this.isPlaying = true;
|
|
2549
|
+
this.isPaused = false;
|
|
2550
|
+
const track = this.queue.currentTrack;
|
|
2551
|
+
if (track) {
|
|
2552
|
+
this.debug(`[Player] Track started: ${track.title}`);
|
|
2553
|
+
this.emit("trackStart", track);
|
|
2554
|
+
}
|
|
2555
|
+
} else if (newState.status === AudioPlayerStatus.Paused && oldState.status !== AudioPlayerStatus.Paused) {
|
|
2556
|
+
// Track paused
|
|
2557
|
+
this.isPaused = true;
|
|
2558
|
+
const track = this.queue.currentTrack;
|
|
2559
|
+
if (track) {
|
|
2560
|
+
this.debug(`[Player] Player paused on track: ${track.title}`);
|
|
2561
|
+
this.emit("playerPause", track);
|
|
2562
|
+
}
|
|
2563
|
+
} else if (newState.status !== AudioPlayerStatus.Paused && oldState.status === AudioPlayerStatus.Paused) {
|
|
2564
|
+
// Track resumed
|
|
2565
|
+
this.isPaused = false;
|
|
2566
|
+
const track = this.queue.currentTrack;
|
|
2567
|
+
if (track) {
|
|
2568
|
+
this.debug(`[Player] Player resumed on track: ${track.title}`);
|
|
2569
|
+
this.emit("playerResume", track);
|
|
2570
|
+
}
|
|
2571
|
+
} else if (newState.status === AudioPlayerStatus.AutoPaused) {
|
|
2572
|
+
this.debug(`[Player] AudioPlayerStatus.AutoPaused`);
|
|
2573
|
+
} else if (newState.status === AudioPlayerStatus.Buffering) {
|
|
2574
|
+
this.debug(`[Player] AudioPlayerStatus.Buffering`);
|
|
2575
|
+
this.lastDuration = this.currentResource?.playbackDuration || 0;
|
|
2576
|
+
this.stuckTimer = setTimeout(() => {
|
|
2577
|
+
if (this.currentResource?.playbackDuration === this.lastDuration) {
|
|
2578
|
+
this.emit("trackStuck", this.currentTrack);
|
|
2579
|
+
const stuckTrack = this.currentTrack;
|
|
2580
|
+
if (stuckTrack && this.antiStuckEnabled) {
|
|
2581
|
+
void this.attemptTrackRecovery(stuckTrack, new Error("TRACK_STUCK")).then((recovered) => {
|
|
2582
|
+
if (!recovered) {
|
|
2583
|
+
this.skip();
|
|
2584
|
+
}
|
|
2585
|
+
});
|
|
2586
|
+
return;
|
|
2587
|
+
}
|
|
2588
|
+
this.skip();
|
|
2589
|
+
}
|
|
2590
|
+
}, 10000);
|
|
2591
|
+
} else {
|
|
2592
|
+
if (this.stuckTimer) {
|
|
2593
|
+
clearTimeout(this.stuckTimer);
|
|
2594
|
+
this.stuckTimer = null;
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
});
|
|
2598
|
+
this.audioPlayer.on("error", (error) => {
|
|
2599
|
+
this.debug(`[Player] AudioPlayer error:`, error);
|
|
2600
|
+
this.emit("playerError", error, this.queue.currentTrack || undefined);
|
|
2601
|
+
const track = this.queue.currentTrack;
|
|
2602
|
+
if (track && this.antiStuckEnabled) {
|
|
2603
|
+
void this.attemptTrackRecovery(track, error).then((recovered) => {
|
|
2604
|
+
if (!recovered) {
|
|
2605
|
+
this.playNext();
|
|
2606
|
+
}
|
|
2607
|
+
});
|
|
2608
|
+
return;
|
|
2609
|
+
}
|
|
2610
|
+
this.playNext();
|
|
2611
|
+
});
|
|
2612
|
+
|
|
2613
|
+
this.audioPlayer.on("debug", (...args) => {
|
|
2614
|
+
if (this.manager.debugEnabled) {
|
|
2615
|
+
this.emit("debug", ...args);
|
|
2616
|
+
}
|
|
2617
|
+
});
|
|
2618
|
+
//stream Manager events
|
|
2619
|
+
|
|
2620
|
+
this.streamManager.on("streamError", ({ streamId, error }) => {
|
|
2621
|
+
this.debug(`[StreamManager] Error for stream ${streamId}:`, error);
|
|
2622
|
+
this.emit("streamError", error, this.queue.currentTrack || null);
|
|
2623
|
+
});
|
|
2624
|
+
|
|
2625
|
+
this.streamManager.on("streamRegistered", ({ streamId, track, metadata }) => {
|
|
2626
|
+
this.debug(`[StreamManager] Stream registered: ${track.title} (preload: ${metadata.isPreload})`);
|
|
2627
|
+
});
|
|
2628
|
+
|
|
2629
|
+
this.streamManager.on("streamUnregistered", ({ streamId, track, reason }) => {
|
|
2630
|
+
this.debug(`[StreamManager] Stream unregistered: ${track.title} (reason: ${reason})`);
|
|
2631
|
+
});
|
|
2632
|
+
|
|
2633
|
+
this.streamManager.on("debug", (...args) => {
|
|
2634
|
+
this.debug(...args);
|
|
2635
|
+
});
|
|
2636
|
+
}
|
|
2637
|
+
|
|
2638
|
+
addPlugin(plugin: SourcePlugin): void {
|
|
2639
|
+
this.debug(`[Player] Adding plugin: ${plugin.name}`);
|
|
2640
|
+
this.pluginManager.register(plugin);
|
|
2641
|
+
}
|
|
2642
|
+
|
|
2643
|
+
removePlugin(name: string): boolean {
|
|
2644
|
+
this.debug(`[Player] Removing plugin: ${name}`);
|
|
2645
|
+
return this.pluginManager.unregister(name);
|
|
2646
|
+
}
|
|
2647
|
+
/**
|
|
2648
|
+
* Save the current session of the player, including queue, current track, position, volume, loop mode, auto-play mode, and active extensions/plugins.
|
|
2649
|
+
*
|
|
2650
|
+
* @returns {PlayerSession} The saved session data
|
|
2651
|
+
*/
|
|
2652
|
+
saveSession(): PlayerSession {
|
|
2653
|
+
return {
|
|
2654
|
+
guildId: this.guildId,
|
|
2655
|
+
currentTrack: this.currentTrack,
|
|
2656
|
+
position: this.currentResource?.playbackDuration || null,
|
|
2657
|
+
volume: this.volume,
|
|
2658
|
+
queue: this.queue.getTracks(),
|
|
2659
|
+
loopMode: this.queue.loop(),
|
|
2660
|
+
autoPlay: this.queue.autoPlay(),
|
|
2661
|
+
extensions: this.extensionManager.getAll().map((ext) => ext.name),
|
|
2662
|
+
plugins: this.pluginManager.getAll().map((plugin) => plugin.name),
|
|
2663
|
+
};
|
|
2664
|
+
}
|
|
2665
|
+
|
|
2666
|
+
/**
|
|
2667
|
+
* Get serializable state (for manual persistence)
|
|
2668
|
+
*/
|
|
2669
|
+
getSerializableState(): object {
|
|
2670
|
+
return {
|
|
2671
|
+
guildId: this.guildId,
|
|
2672
|
+
queue: this.queue.getTracks(),
|
|
2673
|
+
currentTrack: this.currentTrack,
|
|
2674
|
+
volume: this.volume,
|
|
2675
|
+
isPlaying: this.isPlaying,
|
|
2676
|
+
isPaused: this.isPaused,
|
|
2677
|
+
loopMode: this.queue.loop(),
|
|
2678
|
+
autoPlay: this.queue.autoPlay(),
|
|
2679
|
+
filters: this.filter.getFilterString(),
|
|
2680
|
+
timestamp: Date.now(),
|
|
2681
|
+
};
|
|
2682
|
+
}
|
|
2683
|
+
|
|
2684
|
+
/**
|
|
2685
|
+
* Restore from saved state
|
|
2686
|
+
*/
|
|
2687
|
+
async restoreState(state: any): Promise<boolean> {
|
|
2688
|
+
try {
|
|
2689
|
+
if (state.volume) this.setVolume(state.volume);
|
|
2690
|
+
if (state.loopMode) this.queue.loop(state.loopMode);
|
|
2691
|
+
if (typeof state.autoPlay === "boolean") this.queue.autoPlay(state.autoPlay);
|
|
2692
|
+
if (state.filters) await this.filter.applyFilters(state.filters.split(","));
|
|
2693
|
+
|
|
2694
|
+
// Restore queue
|
|
2695
|
+
if (state.queue && Array.isArray(state.queue)) {
|
|
2696
|
+
this.queue.clear();
|
|
2697
|
+
this.queue.addMultiple(state.queue);
|
|
2698
|
+
}
|
|
2699
|
+
|
|
2700
|
+
this.debug("[Player] State restored");
|
|
2701
|
+
return true;
|
|
2702
|
+
} catch (error) {
|
|
2703
|
+
this.debug("[Player] Failed to restore state:", error);
|
|
2704
|
+
return false;
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
|
|
2708
|
+
/**
|
|
2709
|
+
* Get stream manager stats
|
|
2710
|
+
*/
|
|
2711
|
+
getStreamManagerStats() {
|
|
2712
|
+
return {
|
|
2713
|
+
metrics: this.streamManager.getMetrics(),
|
|
2714
|
+
stats: this.streamManager.getStats(),
|
|
2715
|
+
totalStreams: this.streamManager.getStreamCount(),
|
|
2716
|
+
};
|
|
2717
|
+
}
|
|
2718
|
+
//#endregion
|
|
2719
|
+
//#region Getters
|
|
2720
|
+
|
|
2721
|
+
/**
|
|
2722
|
+
* Get the size of the queue
|
|
2723
|
+
*
|
|
2724
|
+
* @returns {number} The size of the queue
|
|
2725
|
+
* @example
|
|
2726
|
+
* const queueSize = player.queueSize;
|
|
2727
|
+
* console.log(`Queue size: ${queueSize}`);
|
|
2728
|
+
*/
|
|
2729
|
+
get queueSize(): number {
|
|
2730
|
+
return this.queue.size;
|
|
2731
|
+
}
|
|
2732
|
+
|
|
2733
|
+
/**
|
|
2734
|
+
* Get the current track
|
|
2735
|
+
*
|
|
2736
|
+
* @returns {Track | null} The current track or null
|
|
2737
|
+
* @example
|
|
2738
|
+
* const currentTrack = player.currentTrack;
|
|
2739
|
+
* console.log(`Current track: ${currentTrack?.title}`);
|
|
2740
|
+
*/
|
|
2741
|
+
get currentTrack(): Track | null {
|
|
2742
|
+
return this.queue.currentTrack;
|
|
2743
|
+
}
|
|
2744
|
+
|
|
2745
|
+
/**
|
|
2746
|
+
* Get the previous track
|
|
2747
|
+
*
|
|
2748
|
+
* @returns {Track | null} The previous track or null
|
|
2749
|
+
* @example
|
|
2750
|
+
* const previousTrack = player.previousTrack;
|
|
2751
|
+
* console.log(`Previous track: ${previousTrack?.title}`);
|
|
2752
|
+
*/
|
|
2753
|
+
get previousTrack(): Track | null {
|
|
2754
|
+
return this.queue.previousTracks?.at(-1) ?? null;
|
|
2755
|
+
}
|
|
2756
|
+
|
|
2757
|
+
/**
|
|
2758
|
+
* Get the upcoming tracks
|
|
2759
|
+
*
|
|
2760
|
+
* @returns {Track[]} The upcoming tracks
|
|
2761
|
+
* @example
|
|
2762
|
+
* const upcomingTracks = player.upcomingTracks;
|
|
2763
|
+
* console.log(`Upcoming tracks: ${upcomingTracks.length}`);
|
|
2764
|
+
*/
|
|
2765
|
+
get upcomingTracks(): Track[] {
|
|
2766
|
+
return this.queue.getTracks();
|
|
2767
|
+
}
|
|
2768
|
+
|
|
2769
|
+
/**
|
|
2770
|
+
* Get the previous tracks
|
|
2771
|
+
*
|
|
2772
|
+
* @returns {Track[]} The previous tracks
|
|
2773
|
+
* @example
|
|
2774
|
+
* const previousTracks = player.previousTracks;
|
|
2775
|
+
* console.log(`Previous tracks: ${previousTracks.length}`);
|
|
2776
|
+
*/
|
|
2777
|
+
get previousTracks(): Track[] {
|
|
2778
|
+
return this.queue.previousTracks;
|
|
2779
|
+
}
|
|
2780
|
+
|
|
2781
|
+
/**
|
|
2782
|
+
* Get the available plugins
|
|
2783
|
+
*
|
|
2784
|
+
* @returns {string[]} The available plugins
|
|
2785
|
+
* @example
|
|
2786
|
+
* const availablePlugins = player.availablePlugins;
|
|
2787
|
+
* console.log(`Available plugins: ${availablePlugins.length}`);
|
|
2788
|
+
*/
|
|
2789
|
+
get availablePlugins(): string[] {
|
|
2790
|
+
return this.pluginManager.getAll().map((p) => p.name);
|
|
2791
|
+
}
|
|
2792
|
+
|
|
2793
|
+
/**
|
|
2794
|
+
* Get the related tracks
|
|
2795
|
+
*
|
|
2796
|
+
* @returns {Track[] | null} The related tracks or null
|
|
2797
|
+
* @example
|
|
2798
|
+
* const relatedTracks = player.relatedTracks;
|
|
2799
|
+
* console.log(`Related tracks: ${relatedTracks?.length}`);
|
|
2800
|
+
*/
|
|
2801
|
+
get relatedTracks(): Track[] | null {
|
|
2802
|
+
return this.queue.relatedTracks();
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2805
|
+
get isLive(): boolean {
|
|
2806
|
+
return this.currentTrack?.isLive === true;
|
|
2807
|
+
}
|
|
2808
|
+
|
|
2809
|
+
//#endregion
|
|
2810
|
+
}
|