ziplayer 0.2.5 → 0.2.7-dev.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AI-Guide.md +956 -0
- package/README.md +259 -228
- package/dist/extensions/index.d.ts +1 -1
- package/dist/extensions/index.d.ts.map +1 -1
- package/dist/extensions/index.js +5 -5
- package/dist/extensions/index.js.map +1 -1
- package/dist/plugins/BasePlugin.d.ts +4 -3
- package/dist/plugins/BasePlugin.d.ts.map +1 -1
- package/dist/plugins/BasePlugin.js +4 -1
- package/dist/plugins/BasePlugin.js.map +1 -1
- package/dist/plugins/index.d.ts +10 -1
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +195 -40
- package/dist/plugins/index.js.map +1 -1
- package/dist/structures/Player.d.ts +0 -9
- package/dist/structures/Player.d.ts.map +1 -1
- package/dist/structures/Player.js +60 -94
- package/dist/structures/Player.js.map +1 -1
- package/dist/types/plugin.d.ts +3 -1
- package/dist/types/plugin.d.ts.map +1 -1
- package/dist/types/plugin.js.map +1 -1
- package/package.json +46 -46
- package/src/extensions/BaseExtension.ts +35 -35
- package/src/extensions/index.ts +190 -194
- package/src/index.ts +16 -16
- package/src/plugins/BasePlugin.ts +27 -26
- package/src/plugins/index.ts +288 -109
- package/src/structures/FilterManager.ts +303 -303
- package/src/structures/Player.ts +1704 -1743
- package/src/structures/PlayerManager.ts +416 -416
- package/src/structures/Queue.ts +354 -354
- package/src/types/index.ts +373 -373
- package/src/types/plugin.ts +3 -1
- package/src/utils/timeout.ts +10 -10
- package/tsconfig.json +22 -23
package/src/structures/Player.ts
CHANGED
|
@@ -1,1743 +1,1704 @@
|
|
|
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 type { BaseExtension } from "../extensions";
|
|
18
|
-
import type {
|
|
19
|
-
Track,
|
|
20
|
-
PlayerOptions,
|
|
21
|
-
PlayerEvents,
|
|
22
|
-
SourcePlugin,
|
|
23
|
-
SearchResult,
|
|
24
|
-
ProgressBarOptions,
|
|
25
|
-
LoopMode,
|
|
26
|
-
StreamInfo,
|
|
27
|
-
SaveOptions,
|
|
28
|
-
VoiceChannel,
|
|
29
|
-
ExtensionPlayRequest,
|
|
30
|
-
ExtensionPlayResponse,
|
|
31
|
-
ExtensionAfterPlayPayload,
|
|
32
|
-
} from "../types";
|
|
33
|
-
import type { PlayerManager } from "./PlayerManager";
|
|
34
|
-
|
|
35
|
-
import { Queue } from "./Queue";
|
|
36
|
-
import { PluginManager } from "../plugins";
|
|
37
|
-
import { ExtensionManager } from "../extensions";
|
|
38
|
-
import { withTimeout } from "../utils/timeout";
|
|
39
|
-
import { FilterManager } from "./FilterManager";
|
|
40
|
-
|
|
41
|
-
export declare interface Player {
|
|
42
|
-
on<K extends keyof PlayerEvents>(event: K, listener: (...args: PlayerEvents[K]) => void): this;
|
|
43
|
-
emit<K extends keyof PlayerEvents>(event: K, ...args: PlayerEvents[K]): boolean;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Represents a music player for a specific Discord guild.
|
|
48
|
-
*
|
|
49
|
-
* @example
|
|
50
|
-
* // Create and configure player
|
|
51
|
-
* const player = await manager.create(guildId, {
|
|
52
|
-
* tts: { interrupt: true, volume: 1 },
|
|
53
|
-
* leaveOnEnd: true,
|
|
54
|
-
* leaveTimeout: 30000
|
|
55
|
-
* });
|
|
56
|
-
*
|
|
57
|
-
* // Connect to voice channel
|
|
58
|
-
* await player.connect(voiceChannel);
|
|
59
|
-
*
|
|
60
|
-
* // Play different types of content
|
|
61
|
-
* await player.play("Never Gonna Give You Up", userId); // Search query
|
|
62
|
-
* await player.play("https://youtube.com/watch?v=dQw4w9WgXcQ", userId); // Direct URL
|
|
63
|
-
* await player.play("tts: Hello everyone!", userId); // Text-to-Speech
|
|
64
|
-
*
|
|
65
|
-
* // Player controls
|
|
66
|
-
* player.pause(); // Pause current track
|
|
67
|
-
* player.resume(); // Resume paused track
|
|
68
|
-
* player.skip(); // Skip to next track
|
|
69
|
-
* player.stop(); // Stop and clear queue
|
|
70
|
-
* player.setVolume(0.5); // Set volume to 50%
|
|
71
|
-
*
|
|
72
|
-
* // Event handling
|
|
73
|
-
* player.on("trackStart", (player, track) => {
|
|
74
|
-
* console.log(`Now playing: ${track.title}`);
|
|
75
|
-
* });
|
|
76
|
-
*
|
|
77
|
-
* player.on("queueEnd", (player) => {
|
|
78
|
-
* console.log("Queue finished");
|
|
79
|
-
* });
|
|
80
|
-
*
|
|
81
|
-
*/
|
|
82
|
-
export class Player extends EventEmitter {
|
|
83
|
-
public readonly guildId: string;
|
|
84
|
-
public connection: VoiceConnection | null = null;
|
|
85
|
-
public audioPlayer: DiscordAudioPlayer;
|
|
86
|
-
public queue: Queue;
|
|
87
|
-
public volume: number = 100;
|
|
88
|
-
public isPlaying: boolean = false;
|
|
89
|
-
public isPaused: boolean = false;
|
|
90
|
-
public options: PlayerOptions;
|
|
91
|
-
public pluginManager: PluginManager;
|
|
92
|
-
public extensionManager: ExtensionManager;
|
|
93
|
-
public userdata?: Record<string, any>;
|
|
94
|
-
private manager: PlayerManager;
|
|
95
|
-
private leaveTimeout: NodeJS.Timeout | null = null;
|
|
96
|
-
private currentResource: AudioResource | null = null;
|
|
97
|
-
private volumeInterval: NodeJS.Timeout | null = null;
|
|
98
|
-
private skipLoop = false;
|
|
99
|
-
private filter!: FilterManager;
|
|
100
|
-
|
|
101
|
-
// Cache for search results to avoid duplicate calls
|
|
102
|
-
private searchCache = new Map<string, SearchResult>();
|
|
103
|
-
private readonly SEARCH_CACHE_TTL = 2 * 60 * 1000; // 2 minutes
|
|
104
|
-
private searchCacheTimestamps = new Map<string, number>();
|
|
105
|
-
private ttsPlayer: DiscordAudioPlayer | null = null;
|
|
106
|
-
|
|
107
|
-
constructor(guildId: string, options: PlayerOptions = {}, manager: PlayerManager) {
|
|
108
|
-
super();
|
|
109
|
-
this.debug(`[Player] Constructor called for guildId: ${guildId}`);
|
|
110
|
-
this.guildId = guildId;
|
|
111
|
-
this.queue = new Queue();
|
|
112
|
-
this.manager = manager;
|
|
113
|
-
this.audioPlayer = createAudioPlayer({
|
|
114
|
-
behaviors: {
|
|
115
|
-
noSubscriber: NoSubscriberBehavior.Pause,
|
|
116
|
-
maxMissedFrames: 100,
|
|
117
|
-
},
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
this.options = {
|
|
121
|
-
leaveOnEnd: true,
|
|
122
|
-
leaveOnEmpty: true,
|
|
123
|
-
leaveTimeout: 100000,
|
|
124
|
-
volume: 100,
|
|
125
|
-
quality: "high",
|
|
126
|
-
extractorTimeout: 50000,
|
|
127
|
-
selfDeaf: true,
|
|
128
|
-
selfMute: false,
|
|
129
|
-
...options,
|
|
130
|
-
tts: {
|
|
131
|
-
createPlayer: false,
|
|
132
|
-
interrupt: true,
|
|
133
|
-
volume: 100,
|
|
134
|
-
Max_Time_TTS: 60_000,
|
|
135
|
-
...(options?.tts || {}),
|
|
136
|
-
},
|
|
137
|
-
};
|
|
138
|
-
this.filter = new FilterManager(this, this.manager);
|
|
139
|
-
this.extensionManager = new ExtensionManager(this, this.manager);
|
|
140
|
-
this.pluginManager = new PluginManager(this, this.manager, {
|
|
141
|
-
extractorTimeout: this.options.extractorTimeout,
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
this.volume = this.options.volume || 100;
|
|
145
|
-
this.userdata = this.options.userdata;
|
|
146
|
-
this.setupEventListeners();
|
|
147
|
-
|
|
148
|
-
// Initialize filters from options
|
|
149
|
-
if (this.options.filters && this.options.filters.length > 0) {
|
|
150
|
-
this.debug(`[Player] Initializing ${this.options.filters.length} filters from options`);
|
|
151
|
-
// Use async version but don't await in constructor
|
|
152
|
-
this.filter.applyFilters(this.options.filters).catch((error: any) => {
|
|
153
|
-
this.debug(`[Player] Error initializing filters:`, error);
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Optionally pre-create the TTS AudioPlayer
|
|
158
|
-
if (this.options?.tts?.createPlayer) {
|
|
159
|
-
this.ensureTTSPlayer();
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Destroy current stream to prevent memory leaks
|
|
165
|
-
* @private
|
|
166
|
-
*/
|
|
167
|
-
private destroyCurrentStream(): void {
|
|
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
|
-
|
|
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
|
-
this.debug(`[Player]
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
const
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
const
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
-
if (
|
|
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
|
-
|
|
498
|
-
|
|
499
|
-
|
|
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
|
-
this.
|
|
549
|
-
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
});
|
|
561
|
-
|
|
562
|
-
return
|
|
563
|
-
} catch (error) {
|
|
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
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
this.
|
|
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
|
-
this.
|
|
728
|
-
this.
|
|
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
|
-
this.
|
|
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
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
this.debug(
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
this.
|
|
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
|
-
this.
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
*
|
|
899
|
-
*
|
|
900
|
-
*
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
this.
|
|
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
|
-
this.
|
|
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
|
-
this.
|
|
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
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
return
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
this.
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
this.isPaused
|
|
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
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
this.
|
|
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
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
if (newState.status === AudioPlayerStatus.
|
|
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
|
-
return this.
|
|
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
|
-
|
|
1694
|
-
|
|
1695
|
-
*
|
|
1696
|
-
*
|
|
1697
|
-
*
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
/**
|
|
1707
|
-
* Get the previous tracks
|
|
1708
|
-
*
|
|
1709
|
-
* @returns {Track[]} The previous tracks
|
|
1710
|
-
* @example
|
|
1711
|
-
* const previousTracks = player.previousTracks;
|
|
1712
|
-
* console.log(`Previous tracks: ${previousTracks.length}`);
|
|
1713
|
-
*/
|
|
1714
|
-
get previousTracks(): Track[] {
|
|
1715
|
-
return this.queue.previousTracks;
|
|
1716
|
-
}
|
|
1717
|
-
|
|
1718
|
-
/**
|
|
1719
|
-
* Get the available plugins
|
|
1720
|
-
*
|
|
1721
|
-
* @returns {string[]} The available plugins
|
|
1722
|
-
* @example
|
|
1723
|
-
* const availablePlugins = player.availablePlugins;
|
|
1724
|
-
* console.log(`Available plugins: ${availablePlugins.length}`);
|
|
1725
|
-
*/
|
|
1726
|
-
get availablePlugins(): string[] {
|
|
1727
|
-
return this.pluginManager.getAll().map((p) => p.name);
|
|
1728
|
-
}
|
|
1729
|
-
|
|
1730
|
-
/**
|
|
1731
|
-
* Get the related tracks
|
|
1732
|
-
*
|
|
1733
|
-
* @returns {Track[] | null} The related tracks or null
|
|
1734
|
-
* @example
|
|
1735
|
-
* const relatedTracks = player.relatedTracks;
|
|
1736
|
-
* console.log(`Related tracks: ${relatedTracks?.length}`);
|
|
1737
|
-
*/
|
|
1738
|
-
get relatedTracks(): Track[] | null {
|
|
1739
|
-
return this.queue.relatedTracks();
|
|
1740
|
-
}
|
|
1741
|
-
|
|
1742
|
-
//#endregion
|
|
1743
|
-
}
|
|
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 type { BaseExtension } from "../extensions";
|
|
18
|
+
import type {
|
|
19
|
+
Track,
|
|
20
|
+
PlayerOptions,
|
|
21
|
+
PlayerEvents,
|
|
22
|
+
SourcePlugin,
|
|
23
|
+
SearchResult,
|
|
24
|
+
ProgressBarOptions,
|
|
25
|
+
LoopMode,
|
|
26
|
+
StreamInfo,
|
|
27
|
+
SaveOptions,
|
|
28
|
+
VoiceChannel,
|
|
29
|
+
ExtensionPlayRequest,
|
|
30
|
+
ExtensionPlayResponse,
|
|
31
|
+
ExtensionAfterPlayPayload,
|
|
32
|
+
} from "../types";
|
|
33
|
+
import type { PlayerManager } from "./PlayerManager";
|
|
34
|
+
|
|
35
|
+
import { Queue } from "./Queue";
|
|
36
|
+
import { PluginManager } from "../plugins";
|
|
37
|
+
import { ExtensionManager } from "../extensions";
|
|
38
|
+
import { withTimeout } from "../utils/timeout";
|
|
39
|
+
import { FilterManager } from "./FilterManager";
|
|
40
|
+
|
|
41
|
+
export declare interface Player {
|
|
42
|
+
on<K extends keyof PlayerEvents>(event: K, listener: (...args: PlayerEvents[K]) => void): this;
|
|
43
|
+
emit<K extends keyof PlayerEvents>(event: K, ...args: PlayerEvents[K]): boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Represents a music player for a specific Discord guild.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* // Create and configure player
|
|
51
|
+
* const player = await manager.create(guildId, {
|
|
52
|
+
* tts: { interrupt: true, volume: 1 },
|
|
53
|
+
* leaveOnEnd: true,
|
|
54
|
+
* leaveTimeout: 30000
|
|
55
|
+
* });
|
|
56
|
+
*
|
|
57
|
+
* // Connect to voice channel
|
|
58
|
+
* await player.connect(voiceChannel);
|
|
59
|
+
*
|
|
60
|
+
* // Play different types of content
|
|
61
|
+
* await player.play("Never Gonna Give You Up", userId); // Search query
|
|
62
|
+
* await player.play("https://youtube.com/watch?v=dQw4w9WgXcQ", userId); // Direct URL
|
|
63
|
+
* await player.play("tts: Hello everyone!", userId); // Text-to-Speech
|
|
64
|
+
*
|
|
65
|
+
* // Player controls
|
|
66
|
+
* player.pause(); // Pause current track
|
|
67
|
+
* player.resume(); // Resume paused track
|
|
68
|
+
* player.skip(); // Skip to next track
|
|
69
|
+
* player.stop(); // Stop and clear queue
|
|
70
|
+
* player.setVolume(0.5); // Set volume to 50%
|
|
71
|
+
*
|
|
72
|
+
* // Event handling
|
|
73
|
+
* player.on("trackStart", (player, track) => {
|
|
74
|
+
* console.log(`Now playing: ${track.title}`);
|
|
75
|
+
* });
|
|
76
|
+
*
|
|
77
|
+
* player.on("queueEnd", (player) => {
|
|
78
|
+
* console.log("Queue finished");
|
|
79
|
+
* });
|
|
80
|
+
*
|
|
81
|
+
*/
|
|
82
|
+
export class Player extends EventEmitter {
|
|
83
|
+
public readonly guildId: string;
|
|
84
|
+
public connection: VoiceConnection | null = null;
|
|
85
|
+
public audioPlayer: DiscordAudioPlayer;
|
|
86
|
+
public queue: Queue;
|
|
87
|
+
public volume: number = 100;
|
|
88
|
+
public isPlaying: boolean = false;
|
|
89
|
+
public isPaused: boolean = false;
|
|
90
|
+
public options: PlayerOptions;
|
|
91
|
+
public pluginManager: PluginManager;
|
|
92
|
+
public extensionManager: ExtensionManager;
|
|
93
|
+
public userdata?: Record<string, any>;
|
|
94
|
+
private manager: PlayerManager;
|
|
95
|
+
private leaveTimeout: NodeJS.Timeout | null = null;
|
|
96
|
+
private currentResource: AudioResource | null = null;
|
|
97
|
+
private volumeInterval: NodeJS.Timeout | null = null;
|
|
98
|
+
private skipLoop = false;
|
|
99
|
+
private filter!: FilterManager;
|
|
100
|
+
|
|
101
|
+
// Cache for search results to avoid duplicate calls
|
|
102
|
+
private searchCache = new Map<string, SearchResult>();
|
|
103
|
+
private readonly SEARCH_CACHE_TTL = 2 * 60 * 1000; // 2 minutes
|
|
104
|
+
private searchCacheTimestamps = new Map<string, number>();
|
|
105
|
+
private ttsPlayer: DiscordAudioPlayer | null = null;
|
|
106
|
+
|
|
107
|
+
constructor(guildId: string, options: PlayerOptions = {}, manager: PlayerManager) {
|
|
108
|
+
super();
|
|
109
|
+
this.debug(`[Player] Constructor called for guildId: ${guildId}`);
|
|
110
|
+
this.guildId = guildId;
|
|
111
|
+
this.queue = new Queue();
|
|
112
|
+
this.manager = manager;
|
|
113
|
+
this.audioPlayer = createAudioPlayer({
|
|
114
|
+
behaviors: {
|
|
115
|
+
noSubscriber: NoSubscriberBehavior.Pause,
|
|
116
|
+
maxMissedFrames: 100,
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
this.options = {
|
|
121
|
+
leaveOnEnd: true,
|
|
122
|
+
leaveOnEmpty: true,
|
|
123
|
+
leaveTimeout: 100000,
|
|
124
|
+
volume: 100,
|
|
125
|
+
quality: "high",
|
|
126
|
+
extractorTimeout: 50000,
|
|
127
|
+
selfDeaf: true,
|
|
128
|
+
selfMute: false,
|
|
129
|
+
...options,
|
|
130
|
+
tts: {
|
|
131
|
+
createPlayer: false,
|
|
132
|
+
interrupt: true,
|
|
133
|
+
volume: 100,
|
|
134
|
+
Max_Time_TTS: 60_000,
|
|
135
|
+
...(options?.tts || {}),
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
this.filter = new FilterManager(this, this.manager);
|
|
139
|
+
this.extensionManager = new ExtensionManager(this, this.manager);
|
|
140
|
+
this.pluginManager = new PluginManager(this, this.manager, {
|
|
141
|
+
extractorTimeout: this.options.extractorTimeout,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
this.volume = this.options.volume || 100;
|
|
145
|
+
this.userdata = this.options.userdata;
|
|
146
|
+
this.setupEventListeners();
|
|
147
|
+
|
|
148
|
+
// Initialize filters from options
|
|
149
|
+
if (this.options.filters && this.options.filters.length > 0) {
|
|
150
|
+
this.debug(`[Player] Initializing ${this.options.filters.length} filters from options`);
|
|
151
|
+
// Use async version but don't await in constructor
|
|
152
|
+
this.filter.applyFilters(this.options.filters).catch((error: any) => {
|
|
153
|
+
this.debug(`[Player] Error initializing filters:`, error);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Optionally pre-create the TTS AudioPlayer
|
|
158
|
+
if (this.options?.tts?.createPlayer) {
|
|
159
|
+
this.ensureTTSPlayer();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Destroy current stream to prevent memory leaks
|
|
165
|
+
* @private
|
|
166
|
+
*/
|
|
167
|
+
private destroyCurrentStream(): void {
|
|
168
|
+
if (!this.currentResource) return;
|
|
169
|
+
|
|
170
|
+
const stream = (this.currentResource as any)?.metadata?.stream ?? (this.currentResource as any)?.stream;
|
|
171
|
+
|
|
172
|
+
if (stream?.destroy) {
|
|
173
|
+
stream.destroy();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
this.currentResource = null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
//#region Search
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Search for tracks using the player's extensions and plugins
|
|
183
|
+
*
|
|
184
|
+
* @param {string} query - The query to search for
|
|
185
|
+
* @param {string} requestedBy - The user ID who requested the search
|
|
186
|
+
* @returns {Promise<SearchResult>} The search result
|
|
187
|
+
* @example
|
|
188
|
+
* const result = await player.search("Never Gonna Give You Up", userId);
|
|
189
|
+
* console.log(`Search result: ${result.tracks.length} tracks`);
|
|
190
|
+
*/
|
|
191
|
+
async search(query: string, requestedBy: string): Promise<SearchResult> {
|
|
192
|
+
this.debug(`[Player] Search called with query: ${query}, requestedBy: ${requestedBy}`);
|
|
193
|
+
|
|
194
|
+
// Clear expired search cache periodically
|
|
195
|
+
if (Math.random() < 0.1) {
|
|
196
|
+
// 10% chance to clean cache
|
|
197
|
+
this.clearExpiredSearchCache();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Check cache first
|
|
201
|
+
const cachedResult = this.getCachedSearchResult(query);
|
|
202
|
+
if (cachedResult) {
|
|
203
|
+
return cachedResult;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Try extensions first
|
|
207
|
+
const extensionResult = await this.extensionManager.provideSearch(query, requestedBy);
|
|
208
|
+
if (extensionResult && Array.isArray(extensionResult.tracks) && extensionResult.tracks.length > 0) {
|
|
209
|
+
this.debug(`[Player] Extension handled search for query: ${query}`);
|
|
210
|
+
this.cacheSearchResult(query, extensionResult);
|
|
211
|
+
return extensionResult;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Get plugins and filter out TTS for regular searches
|
|
215
|
+
const allPlugins = this.pluginManager.getAll();
|
|
216
|
+
const plugins = allPlugins.filter((p) => {
|
|
217
|
+
// Skip TTS plugin for regular searches (unless query starts with "tts:")
|
|
218
|
+
if (p.name.toLowerCase() === "tts" && !query.toLowerCase().startsWith("tts:")) {
|
|
219
|
+
this.debug(`[Player] Skipping TTS plugin for regular search: ${query}`);
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
return true;
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
this.debug(`[Player] Using ${plugins.length} plugins for search (filtered from ${allPlugins.length})`);
|
|
226
|
+
|
|
227
|
+
let lastError: any = null;
|
|
228
|
+
let searchAttempts = 0;
|
|
229
|
+
|
|
230
|
+
for (const p of plugins) {
|
|
231
|
+
searchAttempts++;
|
|
232
|
+
try {
|
|
233
|
+
this.debug(`[Player] Trying plugin for search: ${p.name} (attempt ${searchAttempts}/${plugins.length})`);
|
|
234
|
+
const startTime = Date.now();
|
|
235
|
+
const res = await withTimeout(
|
|
236
|
+
p.search(query, requestedBy),
|
|
237
|
+
this.options.extractorTimeout ?? 15000,
|
|
238
|
+
`Search operation timed out for ${p.name}`,
|
|
239
|
+
);
|
|
240
|
+
const duration = Date.now() - startTime;
|
|
241
|
+
|
|
242
|
+
if (res && Array.isArray(res.tracks) && res.tracks.length > 0) {
|
|
243
|
+
this.debug(`[Player] Plugin '${p.name}' returned ${res.tracks.length} tracks in ${duration}ms`);
|
|
244
|
+
this.cacheSearchResult(query, res);
|
|
245
|
+
return res;
|
|
246
|
+
}
|
|
247
|
+
this.debug(`[Player] Plugin '${p.name}' returned no tracks in ${duration}ms`);
|
|
248
|
+
} catch (error) {
|
|
249
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
250
|
+
this.debug(`[Player] Search via plugin '${p.name}' failed: ${errorMessage}`);
|
|
251
|
+
lastError = error;
|
|
252
|
+
// Continue to next plugin
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
this.debug(`[Player] No plugins returned results for query: ${query} (tried ${searchAttempts} plugins)`);
|
|
257
|
+
if (lastError) this.emit("playerError", lastError as Error);
|
|
258
|
+
throw new Error(`No plugin found to handle: ${query}`);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Get cached search result or null if not found/expired
|
|
263
|
+
* @param query The search query
|
|
264
|
+
* @returns Cached search result or null
|
|
265
|
+
*/
|
|
266
|
+
private getCachedSearchResult(query: string): SearchResult | null {
|
|
267
|
+
const cacheKey = query.toLowerCase().trim();
|
|
268
|
+
const now = Date.now();
|
|
269
|
+
|
|
270
|
+
const cachedTimestamp = this.searchCacheTimestamps.get(cacheKey);
|
|
271
|
+
if (cachedTimestamp && now - cachedTimestamp < this.SEARCH_CACHE_TTL) {
|
|
272
|
+
const cachedResult = this.searchCache.get(cacheKey);
|
|
273
|
+
if (cachedResult) {
|
|
274
|
+
this.debug(`[SearchCache] Using cached search result for: ${query}`);
|
|
275
|
+
return cachedResult;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Cache search result
|
|
284
|
+
* @param query The search query
|
|
285
|
+
* @param result The search result to cache
|
|
286
|
+
*/
|
|
287
|
+
private cacheSearchResult(query: string, result: SearchResult): void {
|
|
288
|
+
const cacheKey = query.toLowerCase().trim();
|
|
289
|
+
const now = Date.now();
|
|
290
|
+
|
|
291
|
+
this.searchCache.set(cacheKey, result);
|
|
292
|
+
this.searchCacheTimestamps.set(cacheKey, now);
|
|
293
|
+
this.debug(`[SearchCache] Cached search result for: ${query} (${result.tracks.length} tracks)`);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Clear expired search cache entries
|
|
298
|
+
*/
|
|
299
|
+
private clearExpiredSearchCache(): void {
|
|
300
|
+
const now = Date.now();
|
|
301
|
+
for (const [key, timestamp] of this.searchCacheTimestamps.entries()) {
|
|
302
|
+
if (now - timestamp >= this.SEARCH_CACHE_TTL) {
|
|
303
|
+
this.searchCache.delete(key);
|
|
304
|
+
this.searchCacheTimestamps.delete(key);
|
|
305
|
+
this.debug(`[SearchCache] Cleared expired cache entry: ${key}`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Clear all search cache entries
|
|
312
|
+
* @example
|
|
313
|
+
* player.clearSearchCache();
|
|
314
|
+
*/
|
|
315
|
+
public clearSearchCache(): void {
|
|
316
|
+
const cacheSize = this.searchCache.size;
|
|
317
|
+
this.searchCache.clear();
|
|
318
|
+
this.searchCacheTimestamps.clear();
|
|
319
|
+
this.debug(`[SearchCache] Cleared all ${cacheSize} search cache entries`);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Debug method to check for duplicate search calls
|
|
324
|
+
* @param query The search query to check
|
|
325
|
+
* @returns Debug information about the query
|
|
326
|
+
*/
|
|
327
|
+
public debugSearchQuery(query: string): {
|
|
328
|
+
isCached: boolean;
|
|
329
|
+
cacheAge?: number;
|
|
330
|
+
pluginCount: number;
|
|
331
|
+
ttsFiltered: boolean;
|
|
332
|
+
} {
|
|
333
|
+
const cacheKey = query.toLowerCase().trim();
|
|
334
|
+
const now = Date.now();
|
|
335
|
+
const cachedTimestamp = this.searchCacheTimestamps.get(cacheKey);
|
|
336
|
+
const isCached = cachedTimestamp && now - cachedTimestamp < this.SEARCH_CACHE_TTL;
|
|
337
|
+
|
|
338
|
+
const allPlugins = this.pluginManager.getAll();
|
|
339
|
+
const plugins = allPlugins.filter((p) => {
|
|
340
|
+
if (p.name.toLowerCase() === "tts" && !query.toLowerCase().startsWith("tts:")) {
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
return true;
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
return {
|
|
347
|
+
isCached: !!isCached,
|
|
348
|
+
cacheAge: cachedTimestamp ? now - cachedTimestamp : undefined,
|
|
349
|
+
pluginCount: plugins.length,
|
|
350
|
+
ttsFiltered: allPlugins.length > plugins.length,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
private async generateWillNext(): Promise<void> {
|
|
355
|
+
const lastTrack = this.queue.previousTracks[this.queue.previousTracks.length - 1] ?? this.queue.currentTrack;
|
|
356
|
+
if (!lastTrack) return;
|
|
357
|
+
const related = await this.pluginManager.getRelatedTracks(lastTrack);
|
|
358
|
+
if (!related || related.length === 0) return;
|
|
359
|
+
const randomchoice = Math.floor(Math.random() * related.length);
|
|
360
|
+
const nextTrack = this.queue.nextTrack ? this.queue.nextTrack : related[randomchoice];
|
|
361
|
+
this.queue.willNextTrack(nextTrack);
|
|
362
|
+
this.queue.relatedTracks(related);
|
|
363
|
+
this.debug(`[Player] Will next track if autoplay: ${nextTrack?.title}]`);
|
|
364
|
+
this.emit("willPlay", nextTrack, related);
|
|
365
|
+
}
|
|
366
|
+
//#endregion
|
|
367
|
+
//#region Play
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Play a track, search query, search result, or play from queue
|
|
371
|
+
*
|
|
372
|
+
* @param {string | Track | SearchResult | null} query - Track URL, search query, Track object, SearchResult, or null for play
|
|
373
|
+
* @param {string} requestedBy - User ID who requested the track
|
|
374
|
+
* @returns {Promise<boolean>} True if playback started successfully
|
|
375
|
+
* @example
|
|
376
|
+
* await player.play("Never Gonna Give You Up", userId); // Search query
|
|
377
|
+
* await player.play("https://youtube.com/watch?v=dQw4w9WgXcQ", userId); // Direct URL
|
|
378
|
+
* await player.play("tts: Hello everyone!", userId); // Text-to-Speech
|
|
379
|
+
* await player.play(trackObject, userId); // Track object
|
|
380
|
+
* await player.play(searchResult, userId); // SearchResult object
|
|
381
|
+
* await player.play(null); // play from queue
|
|
382
|
+
*/
|
|
383
|
+
async play(query: string | Track | SearchResult | null, requestedBy?: string): Promise<boolean> {
|
|
384
|
+
const debugInfo =
|
|
385
|
+
query === null ? "null"
|
|
386
|
+
: typeof query === "string" ? query
|
|
387
|
+
: "tracks" in query ? `${query.tracks.length} tracks`
|
|
388
|
+
: query.title || "unknown";
|
|
389
|
+
this.debug(`[Player] Play called with query: ${debugInfo}`);
|
|
390
|
+
this.clearLeaveTimeout();
|
|
391
|
+
let tracksToAdd: Track[] = [];
|
|
392
|
+
let isPlaylist = false;
|
|
393
|
+
let effectiveRequest: ExtensionPlayRequest = { query: query as string | Track, requestedBy };
|
|
394
|
+
let hookResponse: ExtensionPlayResponse = {};
|
|
395
|
+
|
|
396
|
+
try {
|
|
397
|
+
// Handle null query - play from queue
|
|
398
|
+
if (query === null) {
|
|
399
|
+
this.debug(`[Player] Play from queue requested`);
|
|
400
|
+
if (this.queue.isEmpty) {
|
|
401
|
+
this.debug(`[Player] Queue is empty, nothing to play`);
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (!this.isPlaying) {
|
|
406
|
+
return await this.playNext();
|
|
407
|
+
}
|
|
408
|
+
return true;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Handle SearchResult
|
|
412
|
+
if (query && typeof query === "object" && "tracks" in query && Array.isArray(query.tracks)) {
|
|
413
|
+
this.debug(`[Player] Playing SearchResult with ${query.tracks.length} tracks`);
|
|
414
|
+
tracksToAdd = query.tracks;
|
|
415
|
+
isPlaylist = !!query.playlist || query.tracks.length > 1;
|
|
416
|
+
|
|
417
|
+
if (query.playlist) {
|
|
418
|
+
this.debug(`[Player] Added playlist: ${query.playlist.name} (${tracksToAdd.length} tracks)`);
|
|
419
|
+
}
|
|
420
|
+
} else {
|
|
421
|
+
// Handle other types (string, Track)
|
|
422
|
+
const hookOutcome = await this.extensionManager.BeforePlayHooks(effectiveRequest);
|
|
423
|
+
effectiveRequest = hookOutcome.request;
|
|
424
|
+
hookResponse = hookOutcome.response;
|
|
425
|
+
if (effectiveRequest.requestedBy === undefined) {
|
|
426
|
+
effectiveRequest.requestedBy = requestedBy;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const hookTracks = Array.isArray(hookResponse.tracks) ? hookResponse.tracks : undefined;
|
|
430
|
+
|
|
431
|
+
if (hookResponse.handled && (!hookTracks || hookTracks.length === 0)) {
|
|
432
|
+
const handledPayload: ExtensionAfterPlayPayload = {
|
|
433
|
+
success: hookResponse.success ?? true,
|
|
434
|
+
query: effectiveRequest.query,
|
|
435
|
+
requestedBy: effectiveRequest.requestedBy,
|
|
436
|
+
tracks: [],
|
|
437
|
+
isPlaylist: hookResponse.isPlaylist ?? false,
|
|
438
|
+
error: hookResponse.error,
|
|
439
|
+
};
|
|
440
|
+
await this.extensionManager.AfterPlayHooks(handledPayload);
|
|
441
|
+
if (hookResponse.error) {
|
|
442
|
+
this.emit("playerError", hookResponse.error);
|
|
443
|
+
}
|
|
444
|
+
return hookResponse.success ?? true;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (hookTracks && hookTracks.length > 0) {
|
|
448
|
+
tracksToAdd = hookTracks;
|
|
449
|
+
isPlaylist = hookResponse.isPlaylist ?? hookTracks.length > 1;
|
|
450
|
+
} else if (typeof effectiveRequest.query === "string") {
|
|
451
|
+
const searchResult = await this.search(effectiveRequest.query, effectiveRequest.requestedBy || "Unknown");
|
|
452
|
+
tracksToAdd = searchResult.tracks;
|
|
453
|
+
if (searchResult.playlist) {
|
|
454
|
+
isPlaylist = true;
|
|
455
|
+
this.debug(`[Player] Added playlist: ${searchResult.playlist.name} (${tracksToAdd.length} tracks)`);
|
|
456
|
+
}
|
|
457
|
+
} else if (effectiveRequest.query) {
|
|
458
|
+
tracksToAdd = [effectiveRequest.query as Track];
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (tracksToAdd.length === 0) {
|
|
463
|
+
this.debug(`[Player] No tracks found for play`);
|
|
464
|
+
throw new Error("No tracks found");
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const isTTS = (t: Track | undefined) => {
|
|
468
|
+
if (!t) return false;
|
|
469
|
+
try {
|
|
470
|
+
return typeof t.source === "string" && t.source.toLowerCase().includes("tts");
|
|
471
|
+
} catch {
|
|
472
|
+
return false;
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
const queryLooksTTS =
|
|
477
|
+
typeof effectiveRequest.query === "string" && effectiveRequest.query.trim().toLowerCase().startsWith("tts");
|
|
478
|
+
|
|
479
|
+
if (
|
|
480
|
+
!isPlaylist &&
|
|
481
|
+
tracksToAdd.length > 0 &&
|
|
482
|
+
this.options?.tts?.interrupt !== false &&
|
|
483
|
+
(isTTS(tracksToAdd[0]) || queryLooksTTS)
|
|
484
|
+
) {
|
|
485
|
+
this.debug(`[Player] Interrupting with TTS: ${tracksToAdd[0].title}`);
|
|
486
|
+
await this.interruptWithTTSTrack(tracksToAdd[0]);
|
|
487
|
+
await this.extensionManager.AfterPlayHooks({
|
|
488
|
+
success: true,
|
|
489
|
+
query: effectiveRequest.query,
|
|
490
|
+
requestedBy: effectiveRequest.requestedBy,
|
|
491
|
+
tracks: tracksToAdd,
|
|
492
|
+
isPlaylist,
|
|
493
|
+
});
|
|
494
|
+
return true;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (isPlaylist) {
|
|
498
|
+
this.queue.addMultiple(tracksToAdd);
|
|
499
|
+
this.emit("queueAddList", tracksToAdd);
|
|
500
|
+
} else {
|
|
501
|
+
this.queue.add(tracksToAdd[0]);
|
|
502
|
+
this.emit("queueAdd", tracksToAdd[0]);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const started = !this.isPlaying ? await this.playNext() : true;
|
|
506
|
+
|
|
507
|
+
await this.extensionManager.AfterPlayHooks({
|
|
508
|
+
success: started,
|
|
509
|
+
query: effectiveRequest.query,
|
|
510
|
+
requestedBy: effectiveRequest.requestedBy,
|
|
511
|
+
tracks: tracksToAdd,
|
|
512
|
+
isPlaylist,
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
return started;
|
|
516
|
+
} catch (error) {
|
|
517
|
+
await this.extensionManager.AfterPlayHooks({
|
|
518
|
+
success: false,
|
|
519
|
+
query: effectiveRequest.query,
|
|
520
|
+
requestedBy: effectiveRequest.requestedBy,
|
|
521
|
+
tracks: tracksToAdd,
|
|
522
|
+
isPlaylist,
|
|
523
|
+
error: error as Error,
|
|
524
|
+
});
|
|
525
|
+
this.debug(`[Player] Play error:`, error);
|
|
526
|
+
this.emit("playerError", error as Error);
|
|
527
|
+
return false;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Create AudioResource with filters and seek applied
|
|
533
|
+
*
|
|
534
|
+
* @param {StreamInfo} streamInfo - The stream information
|
|
535
|
+
* @param {Track} track - The track being processed
|
|
536
|
+
* @param {number} position - Position in milliseconds to seek to (0 = no seek)
|
|
537
|
+
* @returns {Promise<AudioResource>} The AudioResource with filters and seek applied
|
|
538
|
+
*/
|
|
539
|
+
private async createResource(streamInfo: StreamInfo, track: Track, position: number = 0): Promise<AudioResource> {
|
|
540
|
+
const filterString = this.filter.getFilterString();
|
|
541
|
+
|
|
542
|
+
this.debug(`[Player] Creating AudioResource with filters: ${filterString || "none"}, seek: ${position}ms`);
|
|
543
|
+
|
|
544
|
+
try {
|
|
545
|
+
let stream: Readable = streamInfo.stream;
|
|
546
|
+
// Apply filters and seek if needed
|
|
547
|
+
if (filterString || position > 0) {
|
|
548
|
+
stream = await this.filter.applyFiltersAndSeek(streamInfo.stream, position);
|
|
549
|
+
streamInfo.type = StreamType.Arbitrary;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// Create AudioResource with better error handling
|
|
553
|
+
const resource = createAudioResource(stream, {
|
|
554
|
+
metadata: track,
|
|
555
|
+
inputType:
|
|
556
|
+
streamInfo.type === "webm/opus" ? StreamType.WebmOpus
|
|
557
|
+
: streamInfo.type === "ogg/opus" ? StreamType.OggOpus
|
|
558
|
+
: StreamType.Arbitrary,
|
|
559
|
+
inlineVolume: true,
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
return resource;
|
|
563
|
+
} catch (error) {
|
|
564
|
+
this.debug(`[Player] Error creating AudioResource with filters+seek:`, error);
|
|
565
|
+
// Fallback to basic AudioResource
|
|
566
|
+
try {
|
|
567
|
+
const resource = createAudioResource(streamInfo.stream, {
|
|
568
|
+
metadata: track,
|
|
569
|
+
inputType:
|
|
570
|
+
streamInfo.type === "webm/opus" ? StreamType.WebmOpus
|
|
571
|
+
: streamInfo.type === "ogg/opus" ? StreamType.OggOpus
|
|
572
|
+
: StreamType.Arbitrary,
|
|
573
|
+
inlineVolume: true,
|
|
574
|
+
});
|
|
575
|
+
return resource;
|
|
576
|
+
} catch (fallbackError) {
|
|
577
|
+
this.debug(`[Player] Fallback AudioResource creation failed:`, fallbackError);
|
|
578
|
+
throw fallbackError;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
private async getStream(track: Track): Promise<StreamInfo | null> {
|
|
583
|
+
let stream = await this.extensionManager.provideStream(track);
|
|
584
|
+
if (stream?.stream) return stream;
|
|
585
|
+
stream = await this.pluginManager.getStream(track);
|
|
586
|
+
if (stream?.stream) return stream;
|
|
587
|
+
throw new Error(`No stream available for track: ${track.title}`);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Start playing a specific track immediately, replacing the current resource.
|
|
592
|
+
*/
|
|
593
|
+
private async startTrack(track: Track): Promise<boolean> {
|
|
594
|
+
try {
|
|
595
|
+
let streamInfo: StreamInfo | null = await this.getStream(track);
|
|
596
|
+
this.debug(`[Player] Using stream for track: ${track.title}`);
|
|
597
|
+
// Kiểm tra nếu có stream thực sự để tạo AudioResource
|
|
598
|
+
if (streamInfo && (streamInfo as any).stream) {
|
|
599
|
+
try {
|
|
600
|
+
// Destroy the old stream and resource before creating a new one
|
|
601
|
+
this.destroyCurrentStream();
|
|
602
|
+
|
|
603
|
+
this.currentResource = await this.createResource(streamInfo, track, 0);
|
|
604
|
+
if (this.volumeInterval) {
|
|
605
|
+
clearInterval(this.volumeInterval);
|
|
606
|
+
this.volumeInterval = null;
|
|
607
|
+
}
|
|
608
|
+
this.currentResource.volume?.setVolume(this.volume / 100);
|
|
609
|
+
|
|
610
|
+
this.debug(`[Player] Playing resource for track: ${track.title}`);
|
|
611
|
+
this.audioPlayer.play(this.currentResource);
|
|
612
|
+
|
|
613
|
+
await entersState(this.audioPlayer, AudioPlayerStatus.Playing, 5_000);
|
|
614
|
+
return true;
|
|
615
|
+
} catch (resourceError) {
|
|
616
|
+
this.debug(`[Player] Error creating/playing resource:`, resourceError);
|
|
617
|
+
// Try fallback without filters
|
|
618
|
+
try {
|
|
619
|
+
this.debug(`[Player] Attempting fallback without filters`);
|
|
620
|
+
const fallbackResource = createAudioResource(streamInfo.stream, {
|
|
621
|
+
metadata: track,
|
|
622
|
+
inputType:
|
|
623
|
+
streamInfo.type === "webm/opus" ? StreamType.WebmOpus
|
|
624
|
+
: streamInfo.type === "ogg/opus" ? StreamType.OggOpus
|
|
625
|
+
: StreamType.Arbitrary,
|
|
626
|
+
inlineVolume: true,
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
this.currentResource = fallbackResource;
|
|
630
|
+
this.currentResource.volume?.setVolume(this.volume / 100);
|
|
631
|
+
this.audioPlayer.play(this.currentResource);
|
|
632
|
+
await entersState(this.audioPlayer, AudioPlayerStatus.Playing, 5_000);
|
|
633
|
+
return true;
|
|
634
|
+
} catch (fallbackError) {
|
|
635
|
+
this.debug(`[Player] Fallback also failed:`, fallbackError);
|
|
636
|
+
throw fallbackError;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
} else if (streamInfo && !(streamInfo as any).stream) {
|
|
640
|
+
// Extension đang xử lý phát nhạc (như Lavalink) - chỉ đánh dấu đang phát
|
|
641
|
+
this.debug(`[Player] Extension is handling playback for track: ${track.title}`);
|
|
642
|
+
this.isPlaying = true;
|
|
643
|
+
this.isPaused = false;
|
|
644
|
+
this.emit("trackStart", track);
|
|
645
|
+
return true;
|
|
646
|
+
} else {
|
|
647
|
+
throw new Error(`No stream available for track: ${track.title}`);
|
|
648
|
+
}
|
|
649
|
+
} catch (error) {
|
|
650
|
+
this.debug(`[Player] startTrack error:`, error);
|
|
651
|
+
this.emit("playerError", error as Error, track);
|
|
652
|
+
return false;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
private async playNext(): Promise<boolean> {
|
|
657
|
+
this.debug(`[Player] playNext called by ${new Error().stack?.split("\n")[2]?.trim()}`);
|
|
658
|
+
while (true) {
|
|
659
|
+
const track = this.queue.next(this.skipLoop);
|
|
660
|
+
this.skipLoop = false;
|
|
661
|
+
|
|
662
|
+
if (!track) {
|
|
663
|
+
if (this.queue.autoPlay()) {
|
|
664
|
+
const willnext = this.queue.willNextTrack();
|
|
665
|
+
if (willnext) {
|
|
666
|
+
this.queue.addMultiple([willnext]);
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
this.debug(`[Player] No next track in queue`);
|
|
671
|
+
this.isPlaying = false;
|
|
672
|
+
this.emit("queueEnd");
|
|
673
|
+
|
|
674
|
+
if (this.options.leaveOnEnd) {
|
|
675
|
+
this.scheduleLeave();
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
return false;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
this.generateWillNext();
|
|
682
|
+
this.clearLeaveTimeout();
|
|
683
|
+
this.debug(`[Player] playNext called for track: ${track.title}`);
|
|
684
|
+
|
|
685
|
+
try {
|
|
686
|
+
return await this.startTrack(track);
|
|
687
|
+
} catch (err) {
|
|
688
|
+
this.debug(`[Player] playNext error:`, err);
|
|
689
|
+
this.emit("playerError", err as Error, track);
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
//#endregion
|
|
696
|
+
//#region TTS
|
|
697
|
+
|
|
698
|
+
private ensureTTSPlayer(): DiscordAudioPlayer {
|
|
699
|
+
if (this.ttsPlayer) return this.ttsPlayer;
|
|
700
|
+
this.ttsPlayer = createAudioPlayer({
|
|
701
|
+
behaviors: {
|
|
702
|
+
noSubscriber: NoSubscriberBehavior.Pause,
|
|
703
|
+
maxMissedFrames: 100,
|
|
704
|
+
},
|
|
705
|
+
});
|
|
706
|
+
this.ttsPlayer.on("error", (e) => this.debug("[TTS] error:", e));
|
|
707
|
+
return this.ttsPlayer;
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* Interrupt current music with a TTS track. Pauses music, swaps the
|
|
711
|
+
* subscription to a dedicated TTS player, plays TTS, then resumes.
|
|
712
|
+
*
|
|
713
|
+
* @param {Track} track - The track to interrupt with
|
|
714
|
+
* @returns {Promise<void>}
|
|
715
|
+
* @example
|
|
716
|
+
* await player.interruptWithTTSTrack(track);
|
|
717
|
+
*/
|
|
718
|
+
public async interruptWithTTSTrack(track: Track): Promise<void> {
|
|
719
|
+
const wasPlaying =
|
|
720
|
+
this.audioPlayer.state.status === AudioPlayerStatus.Playing ||
|
|
721
|
+
this.audioPlayer.state.status === AudioPlayerStatus.Buffering;
|
|
722
|
+
|
|
723
|
+
let ttsResource: AudioResource | null = null;
|
|
724
|
+
let ttsStream: any = null;
|
|
725
|
+
|
|
726
|
+
try {
|
|
727
|
+
if (!this.connection) throw new Error("No voice connection for TTS");
|
|
728
|
+
const ttsPlayer = this.ensureTTSPlayer();
|
|
729
|
+
|
|
730
|
+
// Build resource from plugin stream
|
|
731
|
+
const streamInfo = await this.pluginManager.getStream(track);
|
|
732
|
+
if (!streamInfo) {
|
|
733
|
+
throw new Error("No stream available for track: ${track.title}");
|
|
734
|
+
}
|
|
735
|
+
ttsStream = streamInfo.stream;
|
|
736
|
+
const resource = await this.createResource(streamInfo as StreamInfo, track);
|
|
737
|
+
if (!resource) {
|
|
738
|
+
throw new Error("No resource available for track: ${track.title}");
|
|
739
|
+
}
|
|
740
|
+
ttsResource = resource;
|
|
741
|
+
if (resource.volume) {
|
|
742
|
+
resource.volume.setVolume((this.options?.tts?.volume ?? this?.volume ?? 100) / 100);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// Pause current music if any
|
|
746
|
+
try {
|
|
747
|
+
this.pause();
|
|
748
|
+
} catch {}
|
|
749
|
+
|
|
750
|
+
// Swap subscription and play TTS
|
|
751
|
+
this.connection.subscribe(ttsPlayer);
|
|
752
|
+
this.emit("ttsStart", { track });
|
|
753
|
+
ttsPlayer.play(resource);
|
|
754
|
+
|
|
755
|
+
// Wait until TTS starts then finishes
|
|
756
|
+
await entersState(ttsPlayer, AudioPlayerStatus.Playing, 5_000).catch(() => null);
|
|
757
|
+
// Derive timeoutMs from resource/track duration when available, with a sensible cap
|
|
758
|
+
const md: any = (resource as any)?.metadata ?? {};
|
|
759
|
+
const declared =
|
|
760
|
+
typeof md.duration === "number" ? md.duration
|
|
761
|
+
: typeof track?.duration === "number" ? track.duration
|
|
762
|
+
: undefined;
|
|
763
|
+
const declaredMs =
|
|
764
|
+
declared ?
|
|
765
|
+
declared > 1000 ?
|
|
766
|
+
declared
|
|
767
|
+
: declared * 1000
|
|
768
|
+
: undefined;
|
|
769
|
+
const cap = this.options?.tts?.Max_Time_TTS ?? 60_000;
|
|
770
|
+
const idleTimeout = declaredMs ? Math.min(cap, Math.max(1_000, declaredMs + 1_500)) : cap;
|
|
771
|
+
await entersState(ttsPlayer, AudioPlayerStatus.Idle, idleTimeout).catch(() => null);
|
|
772
|
+
|
|
773
|
+
// Swap back and resume if needed
|
|
774
|
+
this.connection.subscribe(this.audioPlayer);
|
|
775
|
+
} catch (err) {
|
|
776
|
+
this.debug("[TTS] error while playing:", err);
|
|
777
|
+
this.emit("playerError", err as Error);
|
|
778
|
+
} finally {
|
|
779
|
+
// Clean up TTS stream and resource
|
|
780
|
+
try {
|
|
781
|
+
if (ttsStream && typeof ttsStream.destroy === "function") {
|
|
782
|
+
ttsStream.destroy();
|
|
783
|
+
}
|
|
784
|
+
} catch (error) {
|
|
785
|
+
this.debug("[TTS] Error destroying stream:", error);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
if (wasPlaying) {
|
|
789
|
+
try {
|
|
790
|
+
this.resume();
|
|
791
|
+
} catch {}
|
|
792
|
+
}
|
|
793
|
+
this.emit("ttsEnd");
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
//#endregion
|
|
798
|
+
//#region Player Function
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* Connect to a voice channel
|
|
802
|
+
*
|
|
803
|
+
* @param {VoiceChannel} channel - Discord voice channel
|
|
804
|
+
* @returns {Promise<VoiceConnection>} The voice connection
|
|
805
|
+
* @example
|
|
806
|
+
* await player.connect(voiceChannel);
|
|
807
|
+
*/
|
|
808
|
+
async connect(channel: VoiceChannel): Promise<VoiceConnection> {
|
|
809
|
+
try {
|
|
810
|
+
this.debug(`[Player] Connecting to voice channel: ${channel.id}`);
|
|
811
|
+
const connection = joinVoiceChannel({
|
|
812
|
+
channelId: channel.id,
|
|
813
|
+
guildId: channel.guildId,
|
|
814
|
+
adapterCreator: channel.guild.voiceAdapterCreator as any,
|
|
815
|
+
selfDeaf: this.options.selfDeaf ?? true,
|
|
816
|
+
selfMute: this.options.selfMute ?? false,
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
await entersState(connection, VoiceConnectionStatus.Ready, 50_000);
|
|
820
|
+
this.connection = connection;
|
|
821
|
+
|
|
822
|
+
connection.on(VoiceConnectionStatus.Disconnected, async () => {
|
|
823
|
+
try {
|
|
824
|
+
// move channel
|
|
825
|
+
await Promise.race([
|
|
826
|
+
entersState(connection, VoiceConnectionStatus.Signalling, 5_000),
|
|
827
|
+
entersState(connection, VoiceConnectionStatus.Connecting, 5_000),
|
|
828
|
+
]);
|
|
829
|
+
// Signalling/Connecting → reconnect
|
|
830
|
+
this.debug(`[Player] Reconnecting after channel move...`);
|
|
831
|
+
} catch {
|
|
832
|
+
// no reconnect in 5 giây → disconnect
|
|
833
|
+
this.debug(`[Player] Truly disconnected, destroying player`);
|
|
834
|
+
this.destroy();
|
|
835
|
+
}
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
connection.on("error", (error) => {
|
|
839
|
+
this.debug(`[Player] Voice connection error:`, error);
|
|
840
|
+
this.emit("connectionError", error);
|
|
841
|
+
});
|
|
842
|
+
connection.subscribe(this.audioPlayer);
|
|
843
|
+
|
|
844
|
+
this.clearLeaveTimeout();
|
|
845
|
+
return this.connection;
|
|
846
|
+
} catch (error) {
|
|
847
|
+
this.debug(`[Player] Connection error:`, error);
|
|
848
|
+
this.emit("connectionError", error as Error);
|
|
849
|
+
this.connection?.destroy();
|
|
850
|
+
throw error;
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
/**
|
|
855
|
+
* Pause the current track
|
|
856
|
+
*
|
|
857
|
+
* @returns {boolean} True if paused successfully
|
|
858
|
+
* @example
|
|
859
|
+
* const paused = player.pause();
|
|
860
|
+
* console.log(`Paused: ${paused}`);
|
|
861
|
+
*/
|
|
862
|
+
pause(): boolean {
|
|
863
|
+
this.debug(`[Player] pause called`);
|
|
864
|
+
if (this.isPlaying && !this.isPaused) {
|
|
865
|
+
return this.audioPlayer.pause();
|
|
866
|
+
}
|
|
867
|
+
return false;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/**
|
|
871
|
+
* Resume the current track
|
|
872
|
+
*
|
|
873
|
+
* @returns {boolean} True if resumed successfully
|
|
874
|
+
* @example
|
|
875
|
+
* const resumed = player.resume();
|
|
876
|
+
* console.log(`Resumed: ${resumed}`);
|
|
877
|
+
*/
|
|
878
|
+
resume(): boolean {
|
|
879
|
+
this.debug(`[Player] resume called`);
|
|
880
|
+
if (this.isPaused) {
|
|
881
|
+
const result = this.audioPlayer.unpause();
|
|
882
|
+
if (result) {
|
|
883
|
+
const track = this.queue.currentTrack;
|
|
884
|
+
if (track) {
|
|
885
|
+
this.debug(`[Player] Player resumed on track: ${track.title}`);
|
|
886
|
+
this.emit("playerResume", track);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
return result;
|
|
890
|
+
}
|
|
891
|
+
return false;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* Stop the current track
|
|
896
|
+
*
|
|
897
|
+
* @returns {boolean} True if stopped successfully
|
|
898
|
+
* @example
|
|
899
|
+
* const stopped = player.stop();
|
|
900
|
+
* console.log(`Stopped: ${stopped}`);
|
|
901
|
+
*/
|
|
902
|
+
stop(): boolean {
|
|
903
|
+
this.debug(`[Player] stop called`);
|
|
904
|
+
this.queue.clear();
|
|
905
|
+
const result = this.audioPlayer.stop();
|
|
906
|
+
this.destroyCurrentStream();
|
|
907
|
+
this.currentResource = null;
|
|
908
|
+
|
|
909
|
+
this.isPlaying = false;
|
|
910
|
+
this.isPaused = false;
|
|
911
|
+
this.emit("playerStop");
|
|
912
|
+
return result;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
/**
|
|
916
|
+
* Seek to a specific position in the current track
|
|
917
|
+
*
|
|
918
|
+
* @param {number} position - Position in milliseconds to seek to
|
|
919
|
+
* @returns {Promise<boolean>} True if seek was successful
|
|
920
|
+
* @example
|
|
921
|
+
* // Seek to 30 seconds (30000ms)
|
|
922
|
+
* const success = await player.seek(30000);
|
|
923
|
+
* console.log(`Seek successful: ${success}`);
|
|
924
|
+
*
|
|
925
|
+
* // Seek to 1 minute 30 seconds (90000ms)
|
|
926
|
+
* await player.seek(90000);
|
|
927
|
+
*/
|
|
928
|
+
async seek(position: number): Promise<boolean> {
|
|
929
|
+
this.debug(`[Player] seek called with position: ${position}ms`);
|
|
930
|
+
|
|
931
|
+
const track = this.queue.currentTrack;
|
|
932
|
+
if (!track) {
|
|
933
|
+
this.debug(`[Player] No current track to seek`);
|
|
934
|
+
return false;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
const totalDuration = track.duration > 1000 ? track.duration : track.duration * 1000;
|
|
938
|
+
if (position < 0 || position > totalDuration) {
|
|
939
|
+
this.debug(`[Player] Invalid seek position: ${position}ms (track duration: ${totalDuration}ms)`);
|
|
940
|
+
return false;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
const streaminfo = await this.getStream(track);
|
|
944
|
+
if (!streaminfo?.stream) {
|
|
945
|
+
this.debug(`[Player] No stream to seek`);
|
|
946
|
+
return false;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
await this.refeshPlayerResource(true, position);
|
|
950
|
+
|
|
951
|
+
return true;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
/**
|
|
955
|
+
* Skip to the next track or skip to a specific index
|
|
956
|
+
*
|
|
957
|
+
* @param {number} index - Optional index to skip to (0 = next track)
|
|
958
|
+
* @returns {boolean} True if skipped successfully
|
|
959
|
+
* @example
|
|
960
|
+
* const skipped = player.skip(); // Skip to next track
|
|
961
|
+
* const skippedToIndex = player.skip(2); // Skip to track at index 2
|
|
962
|
+
* console.log(`Skipped: ${skipped}`);
|
|
963
|
+
*/
|
|
964
|
+
skip(index?: number): boolean {
|
|
965
|
+
this.debug(`[Player] skip called with index: ${index}`);
|
|
966
|
+
try {
|
|
967
|
+
if (typeof index === "number" && index >= 0) {
|
|
968
|
+
// Skip to specific index
|
|
969
|
+
const targetTrack = this.queue.getTrack(index);
|
|
970
|
+
if (!targetTrack) {
|
|
971
|
+
this.debug(`[Player] No track found at index ${index}`);
|
|
972
|
+
return false;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// Remove tracks from 0 to index-1
|
|
976
|
+
for (let i = 0; i < index; i++) {
|
|
977
|
+
this.queue.remove(0);
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
this.debug(`[Player] Skipped to track at index ${index}: ${targetTrack.title}`);
|
|
981
|
+
if (this.isPlaying || this.isPaused) {
|
|
982
|
+
this.skipLoop = true;
|
|
983
|
+
return this.audioPlayer.stop();
|
|
984
|
+
}
|
|
985
|
+
return true;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
if (this.isPlaying || this.isPaused) {
|
|
989
|
+
this.skipLoop = true;
|
|
990
|
+
return this.audioPlayer.stop();
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
return true;
|
|
994
|
+
} catch (error) {
|
|
995
|
+
this.debug(`[Player] skip error:`, error);
|
|
996
|
+
return false;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
/**
|
|
1001
|
+
* Go back to the previous track in history and play it.
|
|
1002
|
+
*
|
|
1003
|
+
* @returns {Promise<boolean>} True if previous track was played successfully
|
|
1004
|
+
* @example
|
|
1005
|
+
* const previous = await player.previous();
|
|
1006
|
+
* console.log(`Previous: ${previous}`);
|
|
1007
|
+
*/
|
|
1008
|
+
async previous(): Promise<boolean> {
|
|
1009
|
+
this.debug(`[Player] previous called`);
|
|
1010
|
+
const track = this.queue.previous();
|
|
1011
|
+
if (!track) return false;
|
|
1012
|
+
if (this.queue.currentTrack) this.insert(this.queue.currentTrack, 0);
|
|
1013
|
+
this.clearLeaveTimeout();
|
|
1014
|
+
return this.startTrack(track);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
/**
|
|
1018
|
+
* Save a track's stream to a file and return a Readable stream
|
|
1019
|
+
*
|
|
1020
|
+
* @param {Track} track - The track to save
|
|
1021
|
+
* @param {SaveOptions | string} options - Save options or filename string (for backward compatibility)
|
|
1022
|
+
* @returns {Promise<Readable>} A Readable stream containing the audio data
|
|
1023
|
+
* @example
|
|
1024
|
+
* // Save current track to file
|
|
1025
|
+
* const track = player.currentTrack;
|
|
1026
|
+
* if (track) {
|
|
1027
|
+
* const stream = await player.save(track);
|
|
1028
|
+
*
|
|
1029
|
+
* // Use fs to write the stream to file
|
|
1030
|
+
* const fs = require('fs');
|
|
1031
|
+
* const writeStream = fs.createWriteStream('saved-song.mp3');
|
|
1032
|
+
* stream.pipe(writeStream);
|
|
1033
|
+
*
|
|
1034
|
+
* writeStream.on('finish', () => {
|
|
1035
|
+
* console.log('File saved successfully!');
|
|
1036
|
+
* });
|
|
1037
|
+
* }
|
|
1038
|
+
*
|
|
1039
|
+
* // Save any track by URL
|
|
1040
|
+
* const searchResult = await player.search("Never Gonna Give You Up", userId);
|
|
1041
|
+
* if (searchResult.tracks.length > 0) {
|
|
1042
|
+
* const stream = await player.save(searchResult.tracks[0]);
|
|
1043
|
+
* // Handle the stream...
|
|
1044
|
+
* }
|
|
1045
|
+
*
|
|
1046
|
+
* // Backward compatibility - filename as string
|
|
1047
|
+
* const stream = await player.save(track, "my-song.mp3");
|
|
1048
|
+
*/
|
|
1049
|
+
async save(track: Track, options?: SaveOptions | string): Promise<Readable> {
|
|
1050
|
+
this.debug(`[Player] save called for track: ${track.title}`);
|
|
1051
|
+
|
|
1052
|
+
// Parse options - support both SaveOptions object and filename string (backward compatibility)
|
|
1053
|
+
let saveOptions: SaveOptions = {};
|
|
1054
|
+
if (typeof options === "string") {
|
|
1055
|
+
saveOptions = { filename: options };
|
|
1056
|
+
} else if (options) {
|
|
1057
|
+
saveOptions = options;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
try {
|
|
1061
|
+
// Try extensions first
|
|
1062
|
+
let streamInfo: StreamInfo | null = await this.pluginManager.getStream(track);
|
|
1063
|
+
|
|
1064
|
+
if (!streamInfo || !streamInfo.stream) {
|
|
1065
|
+
throw new Error(`No save stream available for track: ${track.title}`);
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
this.debug(`[Player] Save stream obtained for track: ${track.title}`);
|
|
1069
|
+
if (saveOptions.filename) {
|
|
1070
|
+
this.debug(`[Player] Save options - filename: ${saveOptions.filename}, quality: ${saveOptions.quality || "default"}`);
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// Apply filters if any are active
|
|
1074
|
+
let finalStream = streamInfo.stream;
|
|
1075
|
+
|
|
1076
|
+
if (saveOptions.filter || saveOptions.seek) {
|
|
1077
|
+
try {
|
|
1078
|
+
this.filter.clearAll();
|
|
1079
|
+
this.filter.applyFilters(saveOptions.filter || []);
|
|
1080
|
+
} catch (err) {
|
|
1081
|
+
this.debug(`[Player] Error applying save filters:`, err);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
this.debug(`[Player] Applying filters to save stream: ${this.filter.getFilterString() || "none"}`);
|
|
1085
|
+
finalStream = await this.filter.applyFiltersAndSeek(streamInfo.stream, saveOptions.seek || 0).catch((err) => {
|
|
1086
|
+
this.debug(`[Player] Error applying filters to save stream:`, err);
|
|
1087
|
+
return streamInfo!.stream; // Fallback to original stream
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
// Return the stream directly - caller can pipe it to fs.createWriteStream()
|
|
1092
|
+
return finalStream;
|
|
1093
|
+
} catch (error) {
|
|
1094
|
+
this.debug(`[Player] save error:`, error);
|
|
1095
|
+
this.emit("playerError", error as Error, track);
|
|
1096
|
+
throw error;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
/**
|
|
1101
|
+
* Loop the current track or queue
|
|
1102
|
+
*
|
|
1103
|
+
* @param {LoopMode | number} mode - The loop mode to set ("off", "track", "queue") or number (0=off, 1=track, 2=queue)
|
|
1104
|
+
* @returns {LoopMode} The loop mode
|
|
1105
|
+
* @example
|
|
1106
|
+
* const loopMode = player.loop("track"); // Loop current track
|
|
1107
|
+
* const loopQueue = player.loop("queue"); // Loop entire queue
|
|
1108
|
+
* const loopTrack = player.loop(1); // Loop current track (same as "track")
|
|
1109
|
+
* const loopQueueNum = player.loop(2); // Loop entire queue (same as "queue")
|
|
1110
|
+
* const noLoop = player.loop("off"); // No loop
|
|
1111
|
+
* const noLoopNum = player.loop(0); // No loop (same as "off")
|
|
1112
|
+
* console.log(`Loop mode: ${loopMode}`);
|
|
1113
|
+
*/
|
|
1114
|
+
loop(mode?: LoopMode | number): LoopMode {
|
|
1115
|
+
this.debug(`[Player] loop called with mode: ${mode}`);
|
|
1116
|
+
|
|
1117
|
+
if (typeof mode === "number") {
|
|
1118
|
+
// Number mode: convert to text mode
|
|
1119
|
+
switch (mode) {
|
|
1120
|
+
case 0:
|
|
1121
|
+
return this.queue.loop("off");
|
|
1122
|
+
case 1:
|
|
1123
|
+
return this.queue.loop("track");
|
|
1124
|
+
case 2:
|
|
1125
|
+
return this.queue.loop("queue");
|
|
1126
|
+
default:
|
|
1127
|
+
this.debug(`[Player] Invalid loop number: ${mode}, using "off"`);
|
|
1128
|
+
return this.queue.loop("off");
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
return this.queue.loop(mode as LoopMode);
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
/**
|
|
1136
|
+
* Set the auto-play mode
|
|
1137
|
+
*
|
|
1138
|
+
* @param {boolean} mode - The auto-play mode to set
|
|
1139
|
+
* @returns {boolean} The auto-play mode
|
|
1140
|
+
* @example
|
|
1141
|
+
* const autoPlayMode = player.autoPlay(true);
|
|
1142
|
+
* console.log(`Auto-play mode: ${autoPlayMode}`);
|
|
1143
|
+
*/
|
|
1144
|
+
autoPlay(mode?: boolean): boolean {
|
|
1145
|
+
return this.queue.autoPlay(mode);
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
/**
|
|
1149
|
+
* Set the volume of the current track
|
|
1150
|
+
*
|
|
1151
|
+
* @param {number} volume - The volume to set
|
|
1152
|
+
* @returns {boolean} True if volume was set successfully
|
|
1153
|
+
* @example
|
|
1154
|
+
* const volumeSet = player.setVolume(50);
|
|
1155
|
+
* console.log(`Volume set: ${volumeSet}`);
|
|
1156
|
+
*/
|
|
1157
|
+
setVolume(volume: number): boolean {
|
|
1158
|
+
this.debug(`[Player] setVolume called: ${volume}`);
|
|
1159
|
+
if (volume < 0 || volume > 200) return false;
|
|
1160
|
+
|
|
1161
|
+
const oldVolume = this.volume;
|
|
1162
|
+
this.volume = volume;
|
|
1163
|
+
const resourceVolume = this.currentResource?.volume;
|
|
1164
|
+
|
|
1165
|
+
if (resourceVolume) {
|
|
1166
|
+
if (this.volumeInterval) clearInterval(this.volumeInterval);
|
|
1167
|
+
|
|
1168
|
+
const start = resourceVolume.volume;
|
|
1169
|
+
const target = this.volume / 100;
|
|
1170
|
+
const steps = 10;
|
|
1171
|
+
let currentStep = 0;
|
|
1172
|
+
|
|
1173
|
+
this.volumeInterval = setInterval(() => {
|
|
1174
|
+
currentStep++;
|
|
1175
|
+
const value = start + ((target - start) * currentStep) / steps;
|
|
1176
|
+
resourceVolume.setVolume(value);
|
|
1177
|
+
if (currentStep >= steps) {
|
|
1178
|
+
clearInterval(this.volumeInterval!);
|
|
1179
|
+
this.volumeInterval = null;
|
|
1180
|
+
}
|
|
1181
|
+
}, 300);
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
this.emit("volumeChange", oldVolume, volume);
|
|
1185
|
+
return true;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
/**
|
|
1189
|
+
* Shuffle the queue
|
|
1190
|
+
*
|
|
1191
|
+
* @returns {void}
|
|
1192
|
+
* @example
|
|
1193
|
+
* player.shuffle();
|
|
1194
|
+
*/
|
|
1195
|
+
shuffle(): void {
|
|
1196
|
+
this.debug(`[Player] shuffle called`);
|
|
1197
|
+
this.queue.shuffle();
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
/**
|
|
1201
|
+
* Clear the queue
|
|
1202
|
+
*
|
|
1203
|
+
* @returns {void}
|
|
1204
|
+
* @example
|
|
1205
|
+
* player.clearQueue();
|
|
1206
|
+
*/
|
|
1207
|
+
clearQueue(): void {
|
|
1208
|
+
this.debug(`[Player] clearQueue called`);
|
|
1209
|
+
this.queue.clear();
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
/**
|
|
1213
|
+
* Insert a track or list of tracks into the upcoming queue at a specific position (0 = play after current).
|
|
1214
|
+
* - If `query` is a string, performs a search and inserts resulting tracks (playlist supported).
|
|
1215
|
+
* - If a Track or Track[] is provided, inserts directly.
|
|
1216
|
+
* Does not auto-start playback; it only modifies the queue.
|
|
1217
|
+
*
|
|
1218
|
+
* @param {string | Track | Track[]} query - The track or tracks to insert
|
|
1219
|
+
* @param {number} index - The index to insert the tracks at
|
|
1220
|
+
* @param {string} requestedBy - The user ID who requested the insert
|
|
1221
|
+
* @returns {Promise<boolean>} True if the tracks were inserted successfully
|
|
1222
|
+
* @example
|
|
1223
|
+
* const inserted = await player.insert("Song Name", 0, userId);
|
|
1224
|
+
* console.log(`Inserted: ${inserted}`);
|
|
1225
|
+
*/
|
|
1226
|
+
async insert(query: string | Track | Track[], index: number, requestedBy?: string): Promise<boolean> {
|
|
1227
|
+
try {
|
|
1228
|
+
this.debug(`[Player] insert called at index ${index} with type: ${typeof query}`);
|
|
1229
|
+
let tracksToAdd: Track[] = [];
|
|
1230
|
+
let isPlaylist = false;
|
|
1231
|
+
|
|
1232
|
+
if (typeof query === "string") {
|
|
1233
|
+
const searchResult = await this.search(query, requestedBy || "Unknown");
|
|
1234
|
+
tracksToAdd = searchResult.tracks || [];
|
|
1235
|
+
isPlaylist = !!searchResult.playlist;
|
|
1236
|
+
} else if (Array.isArray(query)) {
|
|
1237
|
+
tracksToAdd = query;
|
|
1238
|
+
isPlaylist = query.length > 1;
|
|
1239
|
+
} else if (query) {
|
|
1240
|
+
tracksToAdd = [query];
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
if (!tracksToAdd || tracksToAdd.length === 0) {
|
|
1244
|
+
this.debug(`[Player] insert: no tracks resolved`);
|
|
1245
|
+
throw new Error("No tracks to insert");
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
if (tracksToAdd.length === 1) {
|
|
1249
|
+
this.queue.insert(tracksToAdd[0], index);
|
|
1250
|
+
this.emit("queueAdd", tracksToAdd[0]);
|
|
1251
|
+
this.debug(`[Player] Inserted track at index ${index}: ${tracksToAdd[0].title}`);
|
|
1252
|
+
} else {
|
|
1253
|
+
this.queue.insertMultiple(tracksToAdd, index);
|
|
1254
|
+
this.emit("queueAddList", tracksToAdd);
|
|
1255
|
+
this.debug(`[Player] Inserted ${tracksToAdd.length} ${isPlaylist ? "playlist " : ""}tracks at index ${index}`);
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
return true;
|
|
1259
|
+
} catch (error) {
|
|
1260
|
+
this.debug(`[Player] insert error:`, error);
|
|
1261
|
+
this.emit("playerError", error as Error);
|
|
1262
|
+
return false;
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
/**
|
|
1267
|
+
* Remove a track from the queue
|
|
1268
|
+
*
|
|
1269
|
+
* @param {number} index - The index of the track to remove
|
|
1270
|
+
* @returns {Track | null} The removed track or null
|
|
1271
|
+
* @example
|
|
1272
|
+
* const removed = player.remove(0);
|
|
1273
|
+
* console.log(`Removed: ${removed?.title}`);
|
|
1274
|
+
*/
|
|
1275
|
+
remove(index: number): Track | null {
|
|
1276
|
+
this.debug(`[Player] remove called for index: ${index}`);
|
|
1277
|
+
const track = this.queue.remove(index);
|
|
1278
|
+
if (track) {
|
|
1279
|
+
this.emit("queueRemove", track, index);
|
|
1280
|
+
}
|
|
1281
|
+
return track;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
/**
|
|
1285
|
+
* Get the progress bar of the current track
|
|
1286
|
+
*
|
|
1287
|
+
* @param {ProgressBarOptions} options - The options for the progress bar
|
|
1288
|
+
* @returns {string} The progress bar
|
|
1289
|
+
* @example
|
|
1290
|
+
* const progressBar = player.getProgressBar();
|
|
1291
|
+
* console.log(`Progress bar: ${progressBar}`);
|
|
1292
|
+
*/
|
|
1293
|
+
getProgressBar(options: ProgressBarOptions = {}): string {
|
|
1294
|
+
const { size = 20, barChar = "▬", progressChar = "🔘" } = options;
|
|
1295
|
+
const track = this.queue.currentTrack;
|
|
1296
|
+
const resource = this.currentResource;
|
|
1297
|
+
if (!track || !resource) return "";
|
|
1298
|
+
|
|
1299
|
+
const total = track.duration > 1000 ? track.duration : track.duration * 1000;
|
|
1300
|
+
if (!total) return this.formatTime(resource.playbackDuration);
|
|
1301
|
+
|
|
1302
|
+
const current = resource.playbackDuration;
|
|
1303
|
+
const ratio = Math.min(current / total, 1);
|
|
1304
|
+
const progress = Math.round(ratio * size);
|
|
1305
|
+
const bar = barChar.repeat(progress) + progressChar + barChar.repeat(size - progress);
|
|
1306
|
+
|
|
1307
|
+
return `${this.formatTime(current)} | ${bar} | ${this.formatTime(total)}`;
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
/**
|
|
1311
|
+
* Get the time of the current track
|
|
1312
|
+
*
|
|
1313
|
+
* @returns {Object} The time of the current track
|
|
1314
|
+
* @example
|
|
1315
|
+
* const time = player.getTime();
|
|
1316
|
+
* console.log(`Time: ${time.current}`);
|
|
1317
|
+
*/
|
|
1318
|
+
getTime() {
|
|
1319
|
+
const resource = this.currentResource;
|
|
1320
|
+
const track = this.queue.currentTrack;
|
|
1321
|
+
if (!track || !resource)
|
|
1322
|
+
return {
|
|
1323
|
+
current: 0,
|
|
1324
|
+
total: 0,
|
|
1325
|
+
format: "00:00",
|
|
1326
|
+
};
|
|
1327
|
+
|
|
1328
|
+
const total = track.duration > 1000 ? track.duration : track.duration * 1000;
|
|
1329
|
+
|
|
1330
|
+
return {
|
|
1331
|
+
current: resource?.playbackDuration,
|
|
1332
|
+
total: total,
|
|
1333
|
+
format: this.formatTime(resource.playbackDuration),
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
/**
|
|
1338
|
+
* Format the time in the format of HH:MM:SS
|
|
1339
|
+
*
|
|
1340
|
+
* @param {number} ms - The time in milliseconds
|
|
1341
|
+
* @returns {string} The formatted time
|
|
1342
|
+
* @example
|
|
1343
|
+
* const formattedTime = player.formatTime(1000);
|
|
1344
|
+
* console.log(`Formatted time: ${formattedTime}`);
|
|
1345
|
+
*/
|
|
1346
|
+
formatTime(ms: number): string {
|
|
1347
|
+
const totalSeconds = Math.floor(ms / 1000);
|
|
1348
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
1349
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
1350
|
+
const seconds = totalSeconds % 60;
|
|
1351
|
+
const parts: string[] = [];
|
|
1352
|
+
if (hours > 0) parts.push(String(hours).padStart(2, "0"));
|
|
1353
|
+
parts.push(String(minutes).padStart(2, "0"));
|
|
1354
|
+
parts.push(String(seconds).padStart(2, "0"));
|
|
1355
|
+
return parts.join(":");
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
/**
|
|
1359
|
+
* Destroy the player
|
|
1360
|
+
*
|
|
1361
|
+
* @returns {void}
|
|
1362
|
+
* @example
|
|
1363
|
+
* player.destroy();
|
|
1364
|
+
*/
|
|
1365
|
+
destroy(): void {
|
|
1366
|
+
this.debug(`[Player] destroy called`);
|
|
1367
|
+
if (this.leaveTimeout) {
|
|
1368
|
+
clearTimeout(this.leaveTimeout);
|
|
1369
|
+
this.leaveTimeout = null;
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
// Destroy current stream before stopping audio
|
|
1373
|
+
this.destroyCurrentStream();
|
|
1374
|
+
|
|
1375
|
+
this.audioPlayer.removeAllListeners();
|
|
1376
|
+
this.audioPlayer.stop(true);
|
|
1377
|
+
|
|
1378
|
+
if (this.ttsPlayer) {
|
|
1379
|
+
try {
|
|
1380
|
+
this.ttsPlayer.stop(true);
|
|
1381
|
+
} catch {}
|
|
1382
|
+
this.ttsPlayer = null;
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
if (this.connection) {
|
|
1386
|
+
this.connection.destroy();
|
|
1387
|
+
this.connection = null;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
this.queue.clear();
|
|
1391
|
+
this.pluginManager.clear();
|
|
1392
|
+
this.filter.destroy();
|
|
1393
|
+
this.extensionManager.destroy();
|
|
1394
|
+
this.isPlaying = false;
|
|
1395
|
+
this.isPaused = false;
|
|
1396
|
+
|
|
1397
|
+
// Clear any remaining intervals
|
|
1398
|
+
if (this.volumeInterval) {
|
|
1399
|
+
clearInterval(this.volumeInterval);
|
|
1400
|
+
this.volumeInterval = null;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
this.emit("playerDestroy");
|
|
1404
|
+
this.removeAllListeners();
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
//#endregion
|
|
1408
|
+
//#region utils
|
|
1409
|
+
private scheduleLeave(): void {
|
|
1410
|
+
this.debug(`[Player] scheduleLeave called`);
|
|
1411
|
+
if (this.leaveTimeout) {
|
|
1412
|
+
clearTimeout(this.leaveTimeout);
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
if (this.options.leaveOnEmpty && this.options.leaveTimeout) {
|
|
1416
|
+
this.leaveTimeout = setTimeout(() => {
|
|
1417
|
+
this.debug(`[Player] Leaving voice channel after timeoutMs`);
|
|
1418
|
+
this.destroy();
|
|
1419
|
+
}, this.options.leaveTimeout);
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
/**
|
|
1424
|
+
* Refesh player resource (apply filter)
|
|
1425
|
+
*
|
|
1426
|
+
* @param {boolean} applyToCurrent - Apply filter for curent track
|
|
1427
|
+
* @param {number} position - Position to seek to in milliseconds
|
|
1428
|
+
* @returns {Promise<boolean>}
|
|
1429
|
+
* @example
|
|
1430
|
+
* const refreshed = await player.refeshPlayerResource(true, 1000);
|
|
1431
|
+
* console.log(`Refreshed: ${refreshed}`);
|
|
1432
|
+
*/
|
|
1433
|
+
public async refeshPlayerResource(applyToCurrent: boolean = true, position: number = -1): Promise<boolean> {
|
|
1434
|
+
if (!applyToCurrent || !this.queue.currentTrack || !(this.isPlaying || this.isPaused)) {
|
|
1435
|
+
return false;
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
try {
|
|
1439
|
+
const track = this.queue.currentTrack;
|
|
1440
|
+
this.debug(`[Player] Refreshing player resource for track: ${track.title}`);
|
|
1441
|
+
|
|
1442
|
+
// Get current position for seeking
|
|
1443
|
+
const currentPosition = position > 0 ? position : this.currentResource?.playbackDuration || 0;
|
|
1444
|
+
|
|
1445
|
+
const streaminfo = await this.getStream(track);
|
|
1446
|
+
if (!streaminfo?.stream) {
|
|
1447
|
+
this.debug(`[Player] No stream to refresh`);
|
|
1448
|
+
return false;
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
// Create AudioResource with filters and seek to current position
|
|
1452
|
+
const resource = await this.createResource(streaminfo, track, currentPosition);
|
|
1453
|
+
|
|
1454
|
+
// Stop current playback and destroy old resource/stream
|
|
1455
|
+
const wasPlaying = this.isPlaying;
|
|
1456
|
+
const wasPaused = this.isPaused;
|
|
1457
|
+
|
|
1458
|
+
this.audioPlayer.stop();
|
|
1459
|
+
|
|
1460
|
+
// Properly destroy the old resource and stream
|
|
1461
|
+
try {
|
|
1462
|
+
if (this.currentResource) {
|
|
1463
|
+
const oldStream = (this.currentResource as any)._readableState?.stream || (this.currentResource as any).stream;
|
|
1464
|
+
if (oldStream && typeof oldStream.destroy === "function") {
|
|
1465
|
+
oldStream.destroy();
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
} catch (error) {
|
|
1469
|
+
this.debug(`[Player] Error destroying old stream in refeshPlayerResource:`, error);
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
this.currentResource = resource;
|
|
1473
|
+
|
|
1474
|
+
// Subscribe to new resource
|
|
1475
|
+
if (this.connection) {
|
|
1476
|
+
this.connection.subscribe(this.audioPlayer);
|
|
1477
|
+
this.audioPlayer.play(resource);
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
// Restore playing state
|
|
1481
|
+
if (wasPlaying && !wasPaused) {
|
|
1482
|
+
this.isPlaying = true;
|
|
1483
|
+
this.isPaused = false;
|
|
1484
|
+
} else if (wasPaused) {
|
|
1485
|
+
this.isPlaying = false;
|
|
1486
|
+
this.isPaused = true;
|
|
1487
|
+
this.audioPlayer.pause();
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
this.debug(`[Player] Successfully applied filter to current track at position ${currentPosition}ms`);
|
|
1491
|
+
return true;
|
|
1492
|
+
} catch (error) {
|
|
1493
|
+
this.debug(`[Player] Error applying filter to current track:`, error);
|
|
1494
|
+
// Filter was still added to active filters, so return true
|
|
1495
|
+
return true;
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
/**
|
|
1500
|
+
* Attach an extension to the player
|
|
1501
|
+
*
|
|
1502
|
+
* @param {BaseExtension} extension - The extension to attach
|
|
1503
|
+
* @example
|
|
1504
|
+
* player.attachExtension(new MyExtension());
|
|
1505
|
+
*/
|
|
1506
|
+
public attachExtension(extension: BaseExtension): void {
|
|
1507
|
+
this.extensionManager.register(extension);
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
/**
|
|
1511
|
+
* Detach an extension from the player
|
|
1512
|
+
*
|
|
1513
|
+
* @param {BaseExtension} extension - The extension to detach
|
|
1514
|
+
* @example
|
|
1515
|
+
* player.detachExtension(new MyExtension());
|
|
1516
|
+
*/
|
|
1517
|
+
public detachExtension(extension: BaseExtension): void {
|
|
1518
|
+
this.extensionManager.unregister(extension);
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
/**
|
|
1522
|
+
* Get all extensions attached to the player
|
|
1523
|
+
*
|
|
1524
|
+
* @returns {readonly BaseExtension[]} All attached extensions
|
|
1525
|
+
* @example
|
|
1526
|
+
* const extensions = player.getExtensions();
|
|
1527
|
+
* console.log(`Extensions: ${extensions.length}`);
|
|
1528
|
+
*/
|
|
1529
|
+
public getExtensions(): readonly BaseExtension[] {
|
|
1530
|
+
return this.extensionManager.getAll();
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
private clearLeaveTimeout(): void {
|
|
1534
|
+
if (this.leaveTimeout) {
|
|
1535
|
+
clearTimeout(this.leaveTimeout);
|
|
1536
|
+
this.leaveTimeout = null;
|
|
1537
|
+
this.debug(`[Player] Cleared leave timeoutMs`);
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
private debug(message?: any, ...optionalParams: any[]): void {
|
|
1542
|
+
if (this.listenerCount("debug") > 0) {
|
|
1543
|
+
this.emit("debug", message, ...optionalParams);
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
private setupEventListeners(): void {
|
|
1548
|
+
this.audioPlayer.on("stateChange", (oldState, newState) => {
|
|
1549
|
+
this.debug(`[Player] AudioPlayer stateChange from ${oldState.status} to ${newState.status}`);
|
|
1550
|
+
if (newState.status === AudioPlayerStatus.Idle && oldState.status !== AudioPlayerStatus.Idle) {
|
|
1551
|
+
// Track ended
|
|
1552
|
+
const track = this.queue.currentTrack;
|
|
1553
|
+
if (track) {
|
|
1554
|
+
this.debug(`[Player] Track ended: ${track.title}`);
|
|
1555
|
+
this.emit("trackEnd", track);
|
|
1556
|
+
}
|
|
1557
|
+
this.playNext();
|
|
1558
|
+
} else if (
|
|
1559
|
+
newState.status === AudioPlayerStatus.Playing &&
|
|
1560
|
+
(oldState.status === AudioPlayerStatus.Idle || oldState.status === AudioPlayerStatus.Buffering)
|
|
1561
|
+
) {
|
|
1562
|
+
// Track started
|
|
1563
|
+
this.clearLeaveTimeout();
|
|
1564
|
+
this.isPlaying = true;
|
|
1565
|
+
this.isPaused = false;
|
|
1566
|
+
const track = this.queue.currentTrack;
|
|
1567
|
+
if (track) {
|
|
1568
|
+
this.debug(`[Player] Track started: ${track.title}`);
|
|
1569
|
+
this.emit("trackStart", track);
|
|
1570
|
+
}
|
|
1571
|
+
} else if (newState.status === AudioPlayerStatus.Paused && oldState.status !== AudioPlayerStatus.Paused) {
|
|
1572
|
+
// Track paused
|
|
1573
|
+
this.isPaused = true;
|
|
1574
|
+
const track = this.queue.currentTrack;
|
|
1575
|
+
if (track) {
|
|
1576
|
+
this.debug(`[Player] Player paused on track: ${track.title}`);
|
|
1577
|
+
this.emit("playerPause", track);
|
|
1578
|
+
}
|
|
1579
|
+
} else if (newState.status !== AudioPlayerStatus.Paused && oldState.status === AudioPlayerStatus.Paused) {
|
|
1580
|
+
// Track resumed
|
|
1581
|
+
this.isPaused = false;
|
|
1582
|
+
const track = this.queue.currentTrack;
|
|
1583
|
+
if (track) {
|
|
1584
|
+
this.debug(`[Player] Player resumed on track: ${track.title}`);
|
|
1585
|
+
this.emit("playerResume", track);
|
|
1586
|
+
}
|
|
1587
|
+
} else if (newState.status === AudioPlayerStatus.AutoPaused) {
|
|
1588
|
+
this.debug(`[Player] AudioPlayerStatus.AutoPaused`);
|
|
1589
|
+
} else if (newState.status === AudioPlayerStatus.Buffering) {
|
|
1590
|
+
this.debug(`[Player] AudioPlayerStatus.Buffering`);
|
|
1591
|
+
}
|
|
1592
|
+
});
|
|
1593
|
+
this.audioPlayer.on("error", (error) => {
|
|
1594
|
+
this.debug(`[Player] AudioPlayer error:`, error);
|
|
1595
|
+
this.emit("playerError", error, this.queue.currentTrack || undefined);
|
|
1596
|
+
this.playNext();
|
|
1597
|
+
});
|
|
1598
|
+
|
|
1599
|
+
this.audioPlayer.on("debug", (...args) => {
|
|
1600
|
+
if (this.manager.debugEnabled) {
|
|
1601
|
+
this.emit("debug", ...args);
|
|
1602
|
+
}
|
|
1603
|
+
});
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
addPlugin(plugin: SourcePlugin): void {
|
|
1607
|
+
this.debug(`[Player] Adding plugin: ${plugin.name}`);
|
|
1608
|
+
this.pluginManager.register(plugin);
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
removePlugin(name: string): boolean {
|
|
1612
|
+
this.debug(`[Player] Removing plugin: ${name}`);
|
|
1613
|
+
return this.pluginManager.unregister(name);
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
//#endregion
|
|
1617
|
+
//#region Getters
|
|
1618
|
+
|
|
1619
|
+
/**
|
|
1620
|
+
* Get the size of the queue
|
|
1621
|
+
*
|
|
1622
|
+
* @returns {number} The size of the queue
|
|
1623
|
+
* @example
|
|
1624
|
+
* const queueSize = player.queueSize;
|
|
1625
|
+
* console.log(`Queue size: ${queueSize}`);
|
|
1626
|
+
*/
|
|
1627
|
+
get queueSize(): number {
|
|
1628
|
+
return this.queue.size;
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
/**
|
|
1632
|
+
* Get the current track
|
|
1633
|
+
*
|
|
1634
|
+
* @returns {Track | null} The current track or null
|
|
1635
|
+
* @example
|
|
1636
|
+
* const currentTrack = player.currentTrack;
|
|
1637
|
+
* console.log(`Current track: ${currentTrack?.title}`);
|
|
1638
|
+
*/
|
|
1639
|
+
get currentTrack(): Track | null {
|
|
1640
|
+
return this.queue.currentTrack;
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
/**
|
|
1644
|
+
* Get the previous track
|
|
1645
|
+
*
|
|
1646
|
+
* @returns {Track | null} The previous track or null
|
|
1647
|
+
* @example
|
|
1648
|
+
* const previousTrack = player.previousTrack;
|
|
1649
|
+
* console.log(`Previous track: ${previousTrack?.title}`);
|
|
1650
|
+
*/
|
|
1651
|
+
get previousTrack(): Track | null {
|
|
1652
|
+
return this.queue.previousTracks?.at(-1) ?? null;
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
/**
|
|
1656
|
+
* Get the upcoming tracks
|
|
1657
|
+
*
|
|
1658
|
+
* @returns {Track[]} The upcoming tracks
|
|
1659
|
+
* @example
|
|
1660
|
+
* const upcomingTracks = player.upcomingTracks;
|
|
1661
|
+
* console.log(`Upcoming tracks: ${upcomingTracks.length}`);
|
|
1662
|
+
*/
|
|
1663
|
+
get upcomingTracks(): Track[] {
|
|
1664
|
+
return this.queue.getTracks();
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
/**
|
|
1668
|
+
* Get the previous tracks
|
|
1669
|
+
*
|
|
1670
|
+
* @returns {Track[]} The previous tracks
|
|
1671
|
+
* @example
|
|
1672
|
+
* const previousTracks = player.previousTracks;
|
|
1673
|
+
* console.log(`Previous tracks: ${previousTracks.length}`);
|
|
1674
|
+
*/
|
|
1675
|
+
get previousTracks(): Track[] {
|
|
1676
|
+
return this.queue.previousTracks;
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
/**
|
|
1680
|
+
* Get the available plugins
|
|
1681
|
+
*
|
|
1682
|
+
* @returns {string[]} The available plugins
|
|
1683
|
+
* @example
|
|
1684
|
+
* const availablePlugins = player.availablePlugins;
|
|
1685
|
+
* console.log(`Available plugins: ${availablePlugins.length}`);
|
|
1686
|
+
*/
|
|
1687
|
+
get availablePlugins(): string[] {
|
|
1688
|
+
return this.pluginManager.getAll().map((p) => p.name);
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
/**
|
|
1692
|
+
* Get the related tracks
|
|
1693
|
+
*
|
|
1694
|
+
* @returns {Track[] | null} The related tracks or null
|
|
1695
|
+
* @example
|
|
1696
|
+
* const relatedTracks = player.relatedTracks;
|
|
1697
|
+
* console.log(`Related tracks: ${relatedTracks?.length}`);
|
|
1698
|
+
*/
|
|
1699
|
+
get relatedTracks(): Track[] | null {
|
|
1700
|
+
return this.queue.relatedTracks();
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
//#endregion
|
|
1704
|
+
}
|