ziplayer 0.0.5 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +181 -169
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/structures/Player.d.ts +21 -0
- package/dist/structures/Player.d.ts.map +1 -1
- package/dist/structures/Player.js +225 -9
- package/dist/structures/Player.js.map +1 -1
- package/dist/structures/PlayerManager.d.ts +3 -0
- package/dist/structures/PlayerManager.d.ts.map +1 -1
- package/dist/structures/PlayerManager.js +40 -1
- package/dist/structures/PlayerManager.js.map +1 -1
- package/dist/structures/Queue.d.ts +4 -0
- package/dist/structures/Queue.d.ts.map +1 -1
- package/dist/structures/Queue.js +36 -0
- package/dist/structures/Queue.js.map +1 -1
- package/dist/types/index.d.ts +22 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +6 -1
- package/src/structures/Player.ts +808 -578
- package/src/structures/PlayerManager.ts +196 -158
- package/src/structures/Queue.ts +37 -0
- package/src/types/index.ts +19 -0
package/src/structures/Player.ts
CHANGED
|
@@ -1,578 +1,808 @@
|
|
|
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 { VoiceChannel } from "discord.js";
|
|
17
|
-
import { Readable } from "stream";
|
|
18
|
-
import { Track, PlayerOptions, PlayerEvents, SourcePlugin, SearchResult, ProgressBarOptions, LoopMode } from "../types";
|
|
19
|
-
import { Queue } from "./Queue";
|
|
20
|
-
import { PluginManager } from "../plugins";
|
|
21
|
-
import type { PlayerManager } from "./PlayerManager";
|
|
22
|
-
export declare interface Player {
|
|
23
|
-
on<K extends keyof PlayerEvents>(event: K, listener: (...args: PlayerEvents[K]) => void): this;
|
|
24
|
-
emit<K extends keyof PlayerEvents>(event: K, ...args: PlayerEvents[K]): boolean;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export class Player extends EventEmitter {
|
|
28
|
-
public readonly guildId: string;
|
|
29
|
-
public connection: VoiceConnection | null = null;
|
|
30
|
-
public audioPlayer: DiscordAudioPlayer;
|
|
31
|
-
public queue: Queue;
|
|
32
|
-
public volume: number = 100;
|
|
33
|
-
public isPlaying: boolean = false;
|
|
34
|
-
public isPaused: boolean = false;
|
|
35
|
-
public options: PlayerOptions;
|
|
36
|
-
public pluginManager: PluginManager;
|
|
37
|
-
public userdata?: Record<string, any>;
|
|
38
|
-
private manager: PlayerManager;
|
|
39
|
-
private leaveTimeout: NodeJS.Timeout | null = null;
|
|
40
|
-
private currentResource: AudioResource | null = null;
|
|
41
|
-
private volumeInterval: NodeJS.Timeout | null = null;
|
|
42
|
-
private skipLoop = false;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
private
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
// Track
|
|
120
|
-
this.
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
this.
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
this.
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
this.debug(`[Player]
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
this.
|
|
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
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
"
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
this.
|
|
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
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
-
this.
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
this.
|
|
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
|
-
this.
|
|
427
|
-
return
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
this.
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
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 { VoiceChannel } from "discord.js";
|
|
17
|
+
import { Readable } from "stream";
|
|
18
|
+
import { Track, PlayerOptions, PlayerEvents, SourcePlugin, SearchResult, ProgressBarOptions, LoopMode } from "../types";
|
|
19
|
+
import { Queue } from "./Queue";
|
|
20
|
+
import { PluginManager } from "../plugins";
|
|
21
|
+
import type { PlayerManager } from "./PlayerManager";
|
|
22
|
+
export declare interface Player {
|
|
23
|
+
on<K extends keyof PlayerEvents>(event: K, listener: (...args: PlayerEvents[K]) => void): this;
|
|
24
|
+
emit<K extends keyof PlayerEvents>(event: K, ...args: PlayerEvents[K]): boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class Player extends EventEmitter {
|
|
28
|
+
public readonly guildId: string;
|
|
29
|
+
public connection: VoiceConnection | null = null;
|
|
30
|
+
public audioPlayer: DiscordAudioPlayer;
|
|
31
|
+
public queue: Queue;
|
|
32
|
+
public volume: number = 100;
|
|
33
|
+
public isPlaying: boolean = false;
|
|
34
|
+
public isPaused: boolean = false;
|
|
35
|
+
public options: PlayerOptions;
|
|
36
|
+
public pluginManager: PluginManager;
|
|
37
|
+
public userdata?: Record<string, any>;
|
|
38
|
+
private manager: PlayerManager;
|
|
39
|
+
private leaveTimeout: NodeJS.Timeout | null = null;
|
|
40
|
+
private currentResource: AudioResource | null = null;
|
|
41
|
+
private volumeInterval: NodeJS.Timeout | null = null;
|
|
42
|
+
private skipLoop = false;
|
|
43
|
+
|
|
44
|
+
// TTS support
|
|
45
|
+
private ttsPlayer: DiscordAudioPlayer | null = null;
|
|
46
|
+
private ttsQueue: Array<Track> = [];
|
|
47
|
+
private ttsActive = false;
|
|
48
|
+
|
|
49
|
+
private withTimeout<T>(promise: Promise<T>, message: string): Promise<T> {
|
|
50
|
+
const timeout = this.options.extractorTimeout ?? 15000;
|
|
51
|
+
return Promise.race([promise, new Promise<never>((_, reject) => setTimeout(() => reject(new Error(message)), timeout))]);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private debug(message?: any, ...optionalParams: any[]): void {
|
|
55
|
+
if (this.listenerCount("debug") > 0) {
|
|
56
|
+
this.emit("debug", message, ...optionalParams);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
constructor(guildId: string, options: PlayerOptions = {}, manager: PlayerManager) {
|
|
61
|
+
super();
|
|
62
|
+
this.debug(`[Player] Constructor called for guildId: ${guildId}`);
|
|
63
|
+
this.guildId = guildId;
|
|
64
|
+
this.queue = new Queue();
|
|
65
|
+
this.manager = manager;
|
|
66
|
+
this.audioPlayer = createAudioPlayer({
|
|
67
|
+
behaviors: {
|
|
68
|
+
noSubscriber: NoSubscriberBehavior.Pause,
|
|
69
|
+
maxMissedFrames: 100,
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
this.pluginManager = new PluginManager();
|
|
74
|
+
|
|
75
|
+
this.options = {
|
|
76
|
+
leaveOnEnd: true,
|
|
77
|
+
leaveOnEmpty: true,
|
|
78
|
+
leaveTimeout: 100000,
|
|
79
|
+
volume: 100,
|
|
80
|
+
quality: "high",
|
|
81
|
+
extractorTimeout: 50000,
|
|
82
|
+
selfDeaf: true,
|
|
83
|
+
selfMute: false,
|
|
84
|
+
...options,
|
|
85
|
+
tts: {
|
|
86
|
+
createPlayer: false,
|
|
87
|
+
interrupt: true,
|
|
88
|
+
volume: 100,
|
|
89
|
+
Max_Time_TTS: 60_000,
|
|
90
|
+
...(options?.tts || {}),
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
this.volume = this.options.volume || 100;
|
|
95
|
+
this.userdata = this.options.userdata;
|
|
96
|
+
this.setupEventListeners();
|
|
97
|
+
|
|
98
|
+
// Optionally pre-create the TTS AudioPlayer
|
|
99
|
+
if (this.options?.tts?.createPlayer) {
|
|
100
|
+
this.ensureTTSPlayer();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private setupEventListeners(): void {
|
|
105
|
+
this.audioPlayer.on("stateChange", (oldState, newState) => {
|
|
106
|
+
this.debug(`[Player] AudioPlayer stateChange from ${oldState.status} to ${newState.status}`);
|
|
107
|
+
if (newState.status === AudioPlayerStatus.Idle && oldState.status !== AudioPlayerStatus.Idle) {
|
|
108
|
+
// Track ended
|
|
109
|
+
const track = this.queue.currentTrack;
|
|
110
|
+
if (track) {
|
|
111
|
+
this.debug(`[Player] Track ended: ${track.title}`);
|
|
112
|
+
this.emit("trackEnd", track);
|
|
113
|
+
}
|
|
114
|
+
this.playNext();
|
|
115
|
+
} else if (
|
|
116
|
+
newState.status === AudioPlayerStatus.Playing &&
|
|
117
|
+
(oldState.status === AudioPlayerStatus.Idle || oldState.status === AudioPlayerStatus.Buffering)
|
|
118
|
+
) {
|
|
119
|
+
// Track started
|
|
120
|
+
this.isPlaying = true;
|
|
121
|
+
this.isPaused = false;
|
|
122
|
+
const track = this.queue.currentTrack;
|
|
123
|
+
if (track) {
|
|
124
|
+
this.debug(`[Player] Track started: ${track.title}`);
|
|
125
|
+
this.emit("trackStart", track);
|
|
126
|
+
}
|
|
127
|
+
} else if (newState.status === AudioPlayerStatus.Paused && oldState.status !== AudioPlayerStatus.Paused) {
|
|
128
|
+
// Track paused
|
|
129
|
+
this.isPaused = true;
|
|
130
|
+
const track = this.queue.currentTrack;
|
|
131
|
+
if (track) {
|
|
132
|
+
this.debug(`[Player] Player paused on track: ${track.title}`);
|
|
133
|
+
this.emit("playerPause", track);
|
|
134
|
+
}
|
|
135
|
+
} else if (newState.status !== AudioPlayerStatus.Paused && oldState.status === AudioPlayerStatus.Paused) {
|
|
136
|
+
// Track resumed
|
|
137
|
+
this.isPaused = false;
|
|
138
|
+
const track = this.queue.currentTrack;
|
|
139
|
+
if (track) {
|
|
140
|
+
this.debug(`[Player] Player resumed on track: ${track.title}`);
|
|
141
|
+
this.emit("playerResume", track);
|
|
142
|
+
}
|
|
143
|
+
} else if (newState.status === AudioPlayerStatus.AutoPaused) {
|
|
144
|
+
this.debug(`[Player] AudioPlayerStatus.AutoPaused`);
|
|
145
|
+
} else if (newState.status === AudioPlayerStatus.Buffering) {
|
|
146
|
+
this.debug(`[Player] AudioPlayerStatus.Buffering`);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
this.audioPlayer.on("error", (error) => {
|
|
150
|
+
this.debug(`[Player] AudioPlayer error:`, error);
|
|
151
|
+
this.emit("playerError", error, this.queue.currentTrack || undefined);
|
|
152
|
+
this.playNext();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
this.audioPlayer.on("debug", (...args) => {
|
|
156
|
+
if (this.manager.debugEnabled) {
|
|
157
|
+
this.emit("debug", ...args);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private ensureTTSPlayer(): DiscordAudioPlayer {
|
|
163
|
+
if (this.ttsPlayer) return this.ttsPlayer;
|
|
164
|
+
this.ttsPlayer = createAudioPlayer({
|
|
165
|
+
behaviors: {
|
|
166
|
+
noSubscriber: NoSubscriberBehavior.Pause,
|
|
167
|
+
maxMissedFrames: 100,
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
this.ttsPlayer.on("error", (e) => this.debug("[TTS] error:", e));
|
|
171
|
+
return this.ttsPlayer;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
addPlugin(plugin: SourcePlugin): void {
|
|
175
|
+
this.debug(`[Player] Adding plugin: ${plugin.name}`);
|
|
176
|
+
this.pluginManager.register(plugin);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
removePlugin(name: string): boolean {
|
|
180
|
+
this.debug(`[Player] Removing plugin: ${name}`);
|
|
181
|
+
return this.pluginManager.unregister(name);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async connect(channel: VoiceChannel): Promise<VoiceConnection> {
|
|
185
|
+
try {
|
|
186
|
+
this.debug(`[Player] Connecting to voice channel: ${channel.id}`);
|
|
187
|
+
const connection = joinVoiceChannel({
|
|
188
|
+
channelId: channel.id,
|
|
189
|
+
guildId: channel.guildId,
|
|
190
|
+
adapterCreator: channel.guild.voiceAdapterCreator as any,
|
|
191
|
+
selfDeaf: this.options.selfDeaf ?? true,
|
|
192
|
+
selfMute: this.options.selfMute ?? false,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
await entersState(connection, VoiceConnectionStatus.Ready, 50_000);
|
|
196
|
+
this.connection = connection;
|
|
197
|
+
|
|
198
|
+
connection.on(VoiceConnectionStatus.Disconnected, () => {
|
|
199
|
+
this.debug(`[Player] VoiceConnectionStatus.Disconnected`);
|
|
200
|
+
this.destroy();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
connection.on("error", (error) => {
|
|
204
|
+
this.debug(`[Player] Voice connection error:`, error);
|
|
205
|
+
this.emit("connectionError", error);
|
|
206
|
+
});
|
|
207
|
+
connection.subscribe(this.audioPlayer);
|
|
208
|
+
|
|
209
|
+
if (this.leaveTimeout) {
|
|
210
|
+
clearTimeout(this.leaveTimeout);
|
|
211
|
+
this.leaveTimeout = null;
|
|
212
|
+
}
|
|
213
|
+
return this.connection;
|
|
214
|
+
} catch (error) {
|
|
215
|
+
this.debug(`[Player] Connection error:`, error);
|
|
216
|
+
this.emit("connectionError", error as Error);
|
|
217
|
+
this.connection?.destroy();
|
|
218
|
+
throw error;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async search(query: string, requestedBy: string): Promise<SearchResult> {
|
|
223
|
+
this.debug(`[Player] Search called with query: ${query}, requestedBy: ${requestedBy}`);
|
|
224
|
+
const plugins = this.pluginManager.getAll();
|
|
225
|
+
let lastError: any = null;
|
|
226
|
+
|
|
227
|
+
for (const p of plugins) {
|
|
228
|
+
try {
|
|
229
|
+
this.debug(`[Player] Trying plugin for search: ${p.name}`);
|
|
230
|
+
const res = await this.withTimeout(p.search(query, requestedBy), `Search operation timed out for ${p.name}`);
|
|
231
|
+
if (res && Array.isArray(res.tracks) && res.tracks.length > 0) {
|
|
232
|
+
this.debug(`[Player] Plugin '${p.name}' returned ${res.tracks.length} tracks`);
|
|
233
|
+
return res;
|
|
234
|
+
}
|
|
235
|
+
this.debug(`[Player] Plugin '${p.name}' returned no tracks`);
|
|
236
|
+
} catch (error) {
|
|
237
|
+
lastError = error;
|
|
238
|
+
this.debug(`[Player] Search via plugin '${p.name}' failed:`, error);
|
|
239
|
+
// Continue to next plugin
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
this.debug(`[Player] No plugins returned results for query: ${query}`);
|
|
244
|
+
if (lastError) this.emit("playerError", lastError as Error);
|
|
245
|
+
throw new Error(`No plugin found to handle: ${query}`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async play(query: string | Track, requestedBy?: string): Promise<boolean> {
|
|
249
|
+
try {
|
|
250
|
+
this.debug(`[Player] Play called with query: ${typeof query === "string" ? query : query?.title}`);
|
|
251
|
+
let tracksToAdd: Track[] = [];
|
|
252
|
+
let isPlaylist = false;
|
|
253
|
+
if (typeof query === "string") {
|
|
254
|
+
const searchResult = await this.search(query, requestedBy || "Unknown");
|
|
255
|
+
tracksToAdd = searchResult.tracks;
|
|
256
|
+
|
|
257
|
+
if (searchResult.playlist) {
|
|
258
|
+
isPlaylist = true;
|
|
259
|
+
this.debug(`[Player] Added playlist: ${searchResult.playlist.name} (${tracksToAdd.length} tracks)`);
|
|
260
|
+
}
|
|
261
|
+
} else {
|
|
262
|
+
tracksToAdd = [query];
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (tracksToAdd.length === 0) {
|
|
266
|
+
this.debug(`[Player] No tracks found for play`);
|
|
267
|
+
throw new Error("No tracks found");
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// If a TTS track is requested and interrupt mode is enabled, handle it separately
|
|
271
|
+
const isTTS = (t: Track | undefined) => {
|
|
272
|
+
if (!t) return false;
|
|
273
|
+
try {
|
|
274
|
+
return typeof t.source === "string" && t.source.toLowerCase().includes("tts");
|
|
275
|
+
} catch {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const queryLooksTTS = typeof query === "string" && query.trim().toLowerCase().startsWith("tts");
|
|
281
|
+
|
|
282
|
+
if (
|
|
283
|
+
!isPlaylist &&
|
|
284
|
+
tracksToAdd.length > 0 &&
|
|
285
|
+
this.options?.tts?.interrupt !== false &&
|
|
286
|
+
(isTTS(tracksToAdd[0]) || queryLooksTTS)
|
|
287
|
+
) {
|
|
288
|
+
// Interrupt music playback with TTS (do not modify the music queue)
|
|
289
|
+
this.debug(`[Player] Interrupting with TTS: ${tracksToAdd[0].title}`);
|
|
290
|
+
await this.interruptWithTTSTrack(tracksToAdd[0]);
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (isPlaylist) {
|
|
295
|
+
this.queue.addMultiple(tracksToAdd);
|
|
296
|
+
this.emit("queueAddList", tracksToAdd);
|
|
297
|
+
} else {
|
|
298
|
+
this.queue.add(tracksToAdd?.[0]);
|
|
299
|
+
this.emit("queueAdd", tracksToAdd?.[0]);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Start playing if not already playing
|
|
303
|
+
if (!this.isPlaying) {
|
|
304
|
+
return this.playNext();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return true;
|
|
308
|
+
} catch (error) {
|
|
309
|
+
this.debug(`[Player] Play error:`, error);
|
|
310
|
+
this.emit("playerError", error as Error);
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Interrupt current music with a TTS track. Pauses music, swaps the
|
|
317
|
+
* subscription to a dedicated TTS player, plays TTS, then resumes.
|
|
318
|
+
*/
|
|
319
|
+
public async interruptWithTTSTrack(track: Track): Promise<void> {
|
|
320
|
+
this.ttsQueue.push(track);
|
|
321
|
+
if (!this.ttsActive) {
|
|
322
|
+
void this.playNextTTS();
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** Play queued TTS items sequentially */
|
|
327
|
+
private async playNextTTS(): Promise<void> {
|
|
328
|
+
const next = this.ttsQueue.shift();
|
|
329
|
+
if (!next) return;
|
|
330
|
+
this.ttsActive = true;
|
|
331
|
+
|
|
332
|
+
try {
|
|
333
|
+
if (!this.connection) throw new Error("No voice connection for TTS");
|
|
334
|
+
const ttsPlayer = this.ensureTTSPlayer();
|
|
335
|
+
|
|
336
|
+
// Build resource from plugin stream
|
|
337
|
+
const resource = await this.resourceFromTrack(next);
|
|
338
|
+
if (resource.volume) {
|
|
339
|
+
resource.volume.setVolume((this.options?.tts?.volume ?? this?.volume ?? 100) / 100);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const wasPlaying =
|
|
343
|
+
this.audioPlayer.state.status === AudioPlayerStatus.Playing ||
|
|
344
|
+
this.audioPlayer.state.status === AudioPlayerStatus.Buffering;
|
|
345
|
+
|
|
346
|
+
// Pause current music if any
|
|
347
|
+
try {
|
|
348
|
+
this.audioPlayer.pause(true);
|
|
349
|
+
} catch {}
|
|
350
|
+
|
|
351
|
+
// Swap subscription and play TTS
|
|
352
|
+
this.connection.subscribe(ttsPlayer);
|
|
353
|
+
this.emit("ttsStart", { track: next });
|
|
354
|
+
ttsPlayer.play(resource);
|
|
355
|
+
|
|
356
|
+
// Wait until TTS starts then finishes
|
|
357
|
+
await entersState(ttsPlayer, AudioPlayerStatus.Playing, 5_000).catch(() => null);
|
|
358
|
+
await entersState(ttsPlayer, AudioPlayerStatus.Idle, this.options?.tts?.Max_Time_TTS || 60_000).catch(() => null);
|
|
359
|
+
|
|
360
|
+
// Swap back and resume if needed
|
|
361
|
+
this.connection.subscribe(this.audioPlayer);
|
|
362
|
+
if (wasPlaying) {
|
|
363
|
+
try {
|
|
364
|
+
this.audioPlayer.unpause();
|
|
365
|
+
} catch {}
|
|
366
|
+
}
|
|
367
|
+
this.emit("ttsEnd");
|
|
368
|
+
} catch (err) {
|
|
369
|
+
this.debug("[TTS] error while playing:", err);
|
|
370
|
+
this.emit("playerError", err as Error);
|
|
371
|
+
} finally {
|
|
372
|
+
this.ttsActive = false;
|
|
373
|
+
if (this.ttsQueue.length > 0) {
|
|
374
|
+
await this.playNextTTS();
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** Build AudioResource for a given track using the plugin pipeline */
|
|
380
|
+
private async resourceFromTrack(track: Track): Promise<AudioResource> {
|
|
381
|
+
// Resolve plugin similar to playNext
|
|
382
|
+
const plugin = this.pluginManager.findPlugin(track.url) || this.pluginManager.get(track.source);
|
|
383
|
+
if (!plugin) throw new Error(`No plugin found for track: ${track.title}`);
|
|
384
|
+
|
|
385
|
+
let streamInfo: any;
|
|
386
|
+
try {
|
|
387
|
+
streamInfo = await this.withTimeout(plugin.getStream(track), "getStream timed out");
|
|
388
|
+
} catch (streamError) {
|
|
389
|
+
// try fallbacks
|
|
390
|
+
const allplugs = this.pluginManager.getAll();
|
|
391
|
+
for (const p of allplugs) {
|
|
392
|
+
if (typeof (p as any).getFallback !== "function") continue;
|
|
393
|
+
try {
|
|
394
|
+
streamInfo = await this.withTimeout(
|
|
395
|
+
(p as any).getFallback(track),
|
|
396
|
+
`getFallback timed out for plugin ${(p as any).name}`,
|
|
397
|
+
);
|
|
398
|
+
if (!streamInfo?.stream) continue;
|
|
399
|
+
break;
|
|
400
|
+
} catch {}
|
|
401
|
+
}
|
|
402
|
+
if (!streamInfo?.stream) throw new Error(`All getFallback attempts failed for track: ${track.title}`);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const mapToStreamType = (type: string): StreamType => {
|
|
406
|
+
switch (type) {
|
|
407
|
+
case "webm/opus":
|
|
408
|
+
return StreamType.WebmOpus;
|
|
409
|
+
case "ogg/opus":
|
|
410
|
+
return StreamType.OggOpus;
|
|
411
|
+
case "arbitrary":
|
|
412
|
+
default:
|
|
413
|
+
return StreamType.Arbitrary;
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
const inputType = mapToStreamType(streamInfo.type);
|
|
418
|
+
return createAudioResource(streamInfo.stream, {
|
|
419
|
+
metadata: track,
|
|
420
|
+
inputType,
|
|
421
|
+
inlineVolume: true,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
private async generateWillNext(): Promise<void> {
|
|
426
|
+
const lastTrack = this.queue.previousTracks[this.queue.previousTracks.length - 1] ?? this.queue.currentTrack;
|
|
427
|
+
if (!lastTrack) return;
|
|
428
|
+
|
|
429
|
+
// Build list of candidate plugins: preferred first, then others with getRelatedTracks
|
|
430
|
+
const preferred = this.pluginManager.findPlugin(lastTrack.url) || this.pluginManager.get(lastTrack.source);
|
|
431
|
+
const all = this.pluginManager.getAll();
|
|
432
|
+
const candidates = [...(preferred ? [preferred] : []), ...all.filter((p) => p !== preferred)].filter(
|
|
433
|
+
(p) => typeof (p as any).getRelatedTracks === "function",
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
for (const p of candidates) {
|
|
437
|
+
try {
|
|
438
|
+
this.debug(`[Player] Trying related from plugin: ${p.name}`);
|
|
439
|
+
const related = await this.withTimeout(
|
|
440
|
+
(p as any).getRelatedTracks(lastTrack.url, {
|
|
441
|
+
limit: 10,
|
|
442
|
+
history: this.queue.previousTracks,
|
|
443
|
+
}),
|
|
444
|
+
`getRelatedTracks timed out for ${p.name}`,
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
if (Array.isArray(related) && related.length > 0) {
|
|
448
|
+
const randomchoice = Math.floor(Math.random() * related.length);
|
|
449
|
+
const nextTrack = this.queue.nextTrack ? this.queue.nextTrack : related[randomchoice];
|
|
450
|
+
this.queue.willNextTrack(nextTrack);
|
|
451
|
+
this.debug(`[Player] Will next track if autoplay: ${nextTrack?.title} (via ${p.name})`);
|
|
452
|
+
this.emit("willPlay", nextTrack, related);
|
|
453
|
+
return; // success
|
|
454
|
+
}
|
|
455
|
+
this.debug(`[Player] ${p.name} returned no related tracks`);
|
|
456
|
+
} catch (err) {
|
|
457
|
+
this.debug(`[Player] getRelatedTracks error from ${p.name}:`, err);
|
|
458
|
+
// try next candidate
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
private async playNext(): Promise<boolean> {
|
|
464
|
+
this.debug(`[Player] playNext called`);
|
|
465
|
+
const track = this.queue.next(this.skipLoop);
|
|
466
|
+
this.skipLoop = false;
|
|
467
|
+
if (!track) {
|
|
468
|
+
if (this.queue.autoPlay()) {
|
|
469
|
+
const willnext = this.queue.willNextTrack();
|
|
470
|
+
console.log("willnext", willnext);
|
|
471
|
+
if (willnext) {
|
|
472
|
+
this.debug(`[Player] Auto-playing next track: ${willnext.title}`);
|
|
473
|
+
this.queue.addMultiple([willnext]);
|
|
474
|
+
return this.playNext();
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
this.debug(`[Player] No next track in queue`);
|
|
479
|
+
this.isPlaying = false;
|
|
480
|
+
this.emit("queueEnd");
|
|
481
|
+
|
|
482
|
+
if (this.options.leaveOnEnd) {
|
|
483
|
+
this.scheduleLeave();
|
|
484
|
+
}
|
|
485
|
+
return false;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
this.generateWillNext();
|
|
489
|
+
|
|
490
|
+
try {
|
|
491
|
+
// Find plugin that can handle this track
|
|
492
|
+
const plugin = this.pluginManager.findPlugin(track.url) || this.pluginManager.get(track.source);
|
|
493
|
+
|
|
494
|
+
if (!plugin) {
|
|
495
|
+
this.debug(`[Player] No plugin found for track: ${track.title}`);
|
|
496
|
+
throw new Error(`No plugin found for track: ${track.title}`);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
this.debug(`[Player] Getting stream for track: ${track.title}`);
|
|
500
|
+
this.debug(`[Player] Using plugin: ${plugin.name}`);
|
|
501
|
+
this.debug(`[Track] Track Info:`, track);
|
|
502
|
+
let streamInfo;
|
|
503
|
+
try {
|
|
504
|
+
streamInfo = await this.withTimeout(plugin.getStream(track), "getStream timed out");
|
|
505
|
+
} catch (streamError) {
|
|
506
|
+
this.debug(`[Player] getStream failed, trying getFallback:`, streamError);
|
|
507
|
+
const allplugs = this.pluginManager.getAll();
|
|
508
|
+
for (const p of allplugs) {
|
|
509
|
+
if (typeof p.getFallback !== "function") {
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
try {
|
|
513
|
+
streamInfo = await this.withTimeout(p.getFallback(track), `getFallback timed out for plugin ${p.name}`);
|
|
514
|
+
if (!streamInfo.stream) continue;
|
|
515
|
+
this.debug(`[Player] getFallback succeeded with plugin ${p.name} for track: ${track.title}`);
|
|
516
|
+
break;
|
|
517
|
+
} catch (fallbackError) {
|
|
518
|
+
this.debug(`[Player] getFallback failed with plugin ${p.name}:`, fallbackError);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
if (!streamInfo?.stream) {
|
|
522
|
+
throw new Error(`All getFallback attempts failed for track: ${track.title}`);
|
|
523
|
+
}
|
|
524
|
+
this.debug(streamInfo);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function mapToStreamType(type: string): StreamType {
|
|
528
|
+
switch (type) {
|
|
529
|
+
case "webm/opus":
|
|
530
|
+
return StreamType.WebmOpus;
|
|
531
|
+
case "ogg/opus":
|
|
532
|
+
return StreamType.OggOpus;
|
|
533
|
+
case "arbitrary":
|
|
534
|
+
return StreamType.Arbitrary;
|
|
535
|
+
default:
|
|
536
|
+
return StreamType.Arbitrary;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
let stream: Readable = streamInfo.stream;
|
|
541
|
+
let inputType = mapToStreamType(streamInfo.type);
|
|
542
|
+
|
|
543
|
+
this.currentResource = createAudioResource(stream, {
|
|
544
|
+
metadata: track,
|
|
545
|
+
inputType,
|
|
546
|
+
inlineVolume: true,
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
// Apply initial volume using the resource's VolumeTransformer
|
|
550
|
+
if (this.volumeInterval) {
|
|
551
|
+
clearInterval(this.volumeInterval);
|
|
552
|
+
this.volumeInterval = null;
|
|
553
|
+
}
|
|
554
|
+
this.currentResource.volume?.setVolume(this.volume / 100);
|
|
555
|
+
|
|
556
|
+
this.debug(`[Player] Playing resource for track: ${track.title}`);
|
|
557
|
+
this.audioPlayer.play(this.currentResource);
|
|
558
|
+
|
|
559
|
+
await entersState(this.audioPlayer, AudioPlayerStatus.Playing, 5_000);
|
|
560
|
+
|
|
561
|
+
return true;
|
|
562
|
+
} catch (error) {
|
|
563
|
+
this.debug(`[Player] playNext error:`, error);
|
|
564
|
+
this.emit("playerError", error as Error, track);
|
|
565
|
+
return this.playNext();
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
pause(): boolean {
|
|
570
|
+
this.debug(`[Player] pause called`);
|
|
571
|
+
if (this.isPlaying && !this.isPaused) {
|
|
572
|
+
return this.audioPlayer.pause();
|
|
573
|
+
}
|
|
574
|
+
return false;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
resume(): boolean {
|
|
578
|
+
this.debug(`[Player] resume called`);
|
|
579
|
+
if (this.isPaused) {
|
|
580
|
+
const result = this.audioPlayer.unpause();
|
|
581
|
+
if (result) {
|
|
582
|
+
const track = this.queue.currentTrack;
|
|
583
|
+
if (track) {
|
|
584
|
+
this.debug(`[Player] Player resumed on track: ${track.title}`);
|
|
585
|
+
this.emit("playerResume", track);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
return result;
|
|
589
|
+
}
|
|
590
|
+
return false;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
stop(): boolean {
|
|
594
|
+
this.debug(`[Player] stop called`);
|
|
595
|
+
this.queue.clear();
|
|
596
|
+
const result = this.audioPlayer.stop();
|
|
597
|
+
this.isPlaying = false;
|
|
598
|
+
this.isPaused = false;
|
|
599
|
+
this.emit("playerStop");
|
|
600
|
+
return result;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
skip(): boolean {
|
|
604
|
+
this.debug(`[Player] skip called`);
|
|
605
|
+
if (this.isPlaying || this.isPaused) {
|
|
606
|
+
this.skipLoop = true;
|
|
607
|
+
return this.audioPlayer.stop();
|
|
608
|
+
}
|
|
609
|
+
return !!this.playNext();
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
loop(mode?: LoopMode): LoopMode {
|
|
613
|
+
return this.queue.loop(mode);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
autoPlay(mode?: boolean): boolean {
|
|
617
|
+
return this.queue.autoPlay(mode);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
setVolume(volume: number): boolean {
|
|
621
|
+
this.debug(`[Player] setVolume called: ${volume}`);
|
|
622
|
+
if (volume < 0 || volume > 200) return false;
|
|
623
|
+
|
|
624
|
+
const oldVolume = this.volume;
|
|
625
|
+
this.volume = volume;
|
|
626
|
+
const resourceVolume = this.currentResource?.volume;
|
|
627
|
+
|
|
628
|
+
if (resourceVolume) {
|
|
629
|
+
if (this.volumeInterval) clearInterval(this.volumeInterval);
|
|
630
|
+
|
|
631
|
+
const start = resourceVolume.volume;
|
|
632
|
+
const target = this.volume / 100;
|
|
633
|
+
const steps = 10;
|
|
634
|
+
let currentStep = 0;
|
|
635
|
+
|
|
636
|
+
this.volumeInterval = setInterval(() => {
|
|
637
|
+
currentStep++;
|
|
638
|
+
const value = start + ((target - start) * currentStep) / steps;
|
|
639
|
+
resourceVolume.setVolume(value);
|
|
640
|
+
if (currentStep >= steps) {
|
|
641
|
+
clearInterval(this.volumeInterval!);
|
|
642
|
+
this.volumeInterval = null;
|
|
643
|
+
}
|
|
644
|
+
}, 300);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
this.emit("volumeChange", oldVolume, volume);
|
|
648
|
+
return true;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
shuffle(): void {
|
|
652
|
+
this.debug(`[Player] shuffle called`);
|
|
653
|
+
this.queue.shuffle();
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
clearQueue(): void {
|
|
657
|
+
this.debug(`[Player] clearQueue called`);
|
|
658
|
+
this.queue.clear();
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Insert a track or list of tracks into the upcoming queue at a specific position (0 = play after current).
|
|
663
|
+
* - If `query` is a string, performs a search and inserts resulting tracks (playlist supported).
|
|
664
|
+
* - If a Track or Track[] is provided, inserts directly.
|
|
665
|
+
* Does not auto-start playback; it only modifies the queue.
|
|
666
|
+
*/
|
|
667
|
+
async insert(query: string | Track | Track[], index: number, requestedBy?: string): Promise<boolean> {
|
|
668
|
+
try {
|
|
669
|
+
this.debug(`[Player] insert called at index ${index} with type: ${typeof query}`);
|
|
670
|
+
let tracksToAdd: Track[] = [];
|
|
671
|
+
let isPlaylist = false;
|
|
672
|
+
|
|
673
|
+
if (typeof query === "string") {
|
|
674
|
+
const searchResult = await this.search(query, requestedBy || "Unknown");
|
|
675
|
+
tracksToAdd = searchResult.tracks || [];
|
|
676
|
+
isPlaylist = !!searchResult.playlist;
|
|
677
|
+
} else if (Array.isArray(query)) {
|
|
678
|
+
tracksToAdd = query;
|
|
679
|
+
isPlaylist = query.length > 1;
|
|
680
|
+
} else if (query) {
|
|
681
|
+
tracksToAdd = [query];
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
if (!tracksToAdd || tracksToAdd.length === 0) {
|
|
685
|
+
this.debug(`[Player] insert: no tracks resolved`);
|
|
686
|
+
throw new Error("No tracks to insert");
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
if (tracksToAdd.length === 1) {
|
|
690
|
+
this.queue.insert(tracksToAdd[0], index);
|
|
691
|
+
this.emit("queueAdd", tracksToAdd[0]);
|
|
692
|
+
this.debug(`[Player] Inserted track at index ${index}: ${tracksToAdd[0].title}`);
|
|
693
|
+
} else {
|
|
694
|
+
this.queue.insertMultiple(tracksToAdd, index);
|
|
695
|
+
this.emit("queueAddList", tracksToAdd);
|
|
696
|
+
this.debug(`[Player] Inserted ${tracksToAdd.length} ${isPlaylist ? "playlist " : ""}tracks at index ${index}`);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
return true;
|
|
700
|
+
} catch (error) {
|
|
701
|
+
this.debug(`[Player] insert error:`, error);
|
|
702
|
+
this.emit("playerError", error as Error);
|
|
703
|
+
return false;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
remove(index: number): Track | null {
|
|
708
|
+
this.debug(`[Player] remove called for index: ${index}`);
|
|
709
|
+
const track = this.queue.remove(index);
|
|
710
|
+
if (track) {
|
|
711
|
+
this.emit("queueRemove", track, index);
|
|
712
|
+
}
|
|
713
|
+
return track;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
getProgressBar(options: ProgressBarOptions = {}): string {
|
|
717
|
+
const { size = 20, barChar = "▬", progressChar = "🔘" } = options;
|
|
718
|
+
const track = this.queue.currentTrack;
|
|
719
|
+
const resource = this.currentResource;
|
|
720
|
+
if (!track || !resource) return "";
|
|
721
|
+
|
|
722
|
+
const total = track.duration > 1000 ? track.duration : track.duration * 1000;
|
|
723
|
+
if (!total) return this.formatTime(resource.playbackDuration);
|
|
724
|
+
|
|
725
|
+
const current = resource.playbackDuration;
|
|
726
|
+
const ratio = Math.min(current / total, 1);
|
|
727
|
+
const progress = Math.round(ratio * size);
|
|
728
|
+
const bar = barChar.repeat(progress) + progressChar + barChar.repeat(size - progress);
|
|
729
|
+
|
|
730
|
+
return `${this.formatTime(current)} ${bar} ${this.formatTime(total)}`;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
private formatTime(ms: number): string {
|
|
734
|
+
const totalSeconds = Math.floor(ms / 1000);
|
|
735
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
736
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
737
|
+
const seconds = totalSeconds % 60;
|
|
738
|
+
const parts: string[] = [];
|
|
739
|
+
if (hours > 0) parts.push(String(hours).padStart(2, "0"));
|
|
740
|
+
parts.push(String(minutes).padStart(2, "0"));
|
|
741
|
+
parts.push(String(seconds).padStart(2, "0"));
|
|
742
|
+
return parts.join(":");
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
private scheduleLeave(): void {
|
|
746
|
+
this.debug(`[Player] scheduleLeave called`);
|
|
747
|
+
if (this.leaveTimeout) {
|
|
748
|
+
clearTimeout(this.leaveTimeout);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
if (this.options.leaveOnEmpty && this.options.leaveTimeout) {
|
|
752
|
+
this.leaveTimeout = setTimeout(() => {
|
|
753
|
+
this.debug(`[Player] Leaving voice channel after timeout`);
|
|
754
|
+
this.destroy();
|
|
755
|
+
}, this.options.leaveTimeout);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
destroy(): void {
|
|
760
|
+
this.debug(`[Player] destroy called`);
|
|
761
|
+
if (this.leaveTimeout) {
|
|
762
|
+
clearTimeout(this.leaveTimeout);
|
|
763
|
+
this.leaveTimeout = null;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
this.audioPlayer.stop(true);
|
|
767
|
+
|
|
768
|
+
if (this.ttsPlayer) {
|
|
769
|
+
try {
|
|
770
|
+
this.ttsPlayer.stop(true);
|
|
771
|
+
} catch {}
|
|
772
|
+
this.ttsPlayer = null;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
if (this.connection) {
|
|
776
|
+
this.connection.destroy();
|
|
777
|
+
this.connection = null;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
this.queue.clear();
|
|
781
|
+
this.pluginManager.clear();
|
|
782
|
+
this.isPlaying = false;
|
|
783
|
+
this.isPaused = false;
|
|
784
|
+
this.emit("playerDestroy");
|
|
785
|
+
this.removeAllListeners();
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// Getters
|
|
789
|
+
get queueSize(): number {
|
|
790
|
+
return this.queue.size;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
get currentTrack(): Track | null {
|
|
794
|
+
return this.queue.currentTrack;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
get upcomingTracks(): Track[] {
|
|
798
|
+
return this.queue.getTracks();
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
get previousTracks(): Track[] {
|
|
802
|
+
return this.queue.previousTracks;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
get availablePlugins(): string[] {
|
|
806
|
+
return this.pluginManager.getAll().map((p) => p.name);
|
|
807
|
+
}
|
|
808
|
+
}
|