ziplayer 0.0.9 → 0.1.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.
@@ -5,77 +5,292 @@ const events_1 = require("events");
5
5
  const voice_1 = require("@discordjs/voice");
6
6
  const Queue_1 = require("./Queue");
7
7
  const plugins_1 = require("../plugins");
8
+ const timeout_1 = require("../utils/timeout");
9
+ /**
10
+ * Represents a music player for a specific Discord guild.
11
+ *
12
+ * @example
13
+ * // Create and configure player
14
+ * const player = await manager.create(guildId, {
15
+ * tts: { interrupt: true, volume: 1 },
16
+ * leaveOnEnd: true,
17
+ * leaveTimeout: 30000
18
+ * });
19
+ *
20
+ * // Connect to voice channel
21
+ * await player.connect(voiceChannel);
22
+ *
23
+ * // Play different types of content
24
+ * await player.play("Never Gonna Give You Up", userId); // Search query
25
+ * await player.play("https://youtube.com/watch?v=dQw4w9WgXcQ", userId); // Direct URL
26
+ * await player.play("tts: Hello everyone!", userId); // Text-to-Speech
27
+ *
28
+ * // Player controls
29
+ * player.pause(); // Pause current track
30
+ * player.resume(); // Resume paused track
31
+ * player.skip(); // Skip to next track
32
+ * player.stop(); // Stop and clear queue
33
+ * player.setVolume(0.5); // Set volume to 50%
34
+ *
35
+ * // Event handling
36
+ * player.on("trackStart", (player, track) => {
37
+ * console.log(`Now playing: ${track.title}`);
38
+ * });
39
+ *
40
+ * player.on("queueEnd", (player) => {
41
+ * console.log("Queue finished");
42
+ * });
43
+ *
44
+ */
8
45
  class Player extends events_1.EventEmitter {
9
46
  /**
10
- * Start playing a specific track immediately, replacing the current resource.
47
+ * Attach an extension to the player
48
+ *
49
+ * @param {BaseExtension} extension - The extension to attach
50
+ * @example
51
+ * player.attachExtension(new MyExtension());
11
52
  */
12
- async startTrack(track) {
53
+ attachExtension(extension) {
54
+ if (this.extensions.includes(extension))
55
+ return;
56
+ if (!extension.player)
57
+ extension.player = this;
58
+ this.extensions.push(extension);
59
+ this.invokeExtensionLifecycle(extension, "onRegister");
60
+ }
61
+ /**
62
+ * Detach an extension from the player
63
+ *
64
+ * @param {BaseExtension} extension - The extension to detach
65
+ * @example
66
+ * player.detachExtension(new MyExtension());
67
+ */
68
+ detachExtension(extension) {
69
+ const index = this.extensions.indexOf(extension);
70
+ if (index === -1)
71
+ return;
72
+ this.extensions.splice(index, 1);
73
+ this.invokeExtensionLifecycle(extension, "onDestroy");
74
+ if (extension.player === this) {
75
+ extension.player = null;
76
+ }
77
+ }
78
+ /**
79
+ * Get all extensions attached to the player
80
+ *
81
+ * @returns {readonly BaseExtension[]} All attached extensions
82
+ * @example
83
+ * const extensions = player.getExtensions();
84
+ * console.log(`Extensions: ${extensions.length}`);
85
+ */
86
+ getExtensions() {
87
+ return this.extensions;
88
+ }
89
+ invokeExtensionLifecycle(extension, hook) {
90
+ const fn = extension[hook];
91
+ if (typeof fn !== "function")
92
+ return;
13
93
  try {
14
- // Find plugin that can handle this track
15
- const plugin = this.pluginManager.findPlugin(track.url) || this.pluginManager.get(track.source);
16
- if (!plugin) {
17
- this.debug(`[Player] No plugin found for track: ${track.title}`);
18
- throw new Error(`No plugin found for track: ${track.title}`);
19
- }
20
- this.debug(`[Player] Getting stream for track: ${track.title}`);
21
- this.debug(`[Player] Using plugin: ${plugin.name}`);
22
- this.debug(`[Track] Track Info:`, track);
23
- let streamInfo;
94
+ const result = fn.call(extension, this.extensionContext);
95
+ if (result && typeof result.then === "function") {
96
+ result.catch((err) => this.debug(`[Player] Extension ${extension.name} ${hook} error:`, err));
97
+ }
98
+ }
99
+ catch (err) {
100
+ this.debug(`[Player] Extension ${extension.name} ${hook} error:`, err);
101
+ }
102
+ }
103
+ async runBeforePlayHooks(initial) {
104
+ const request = { ...initial };
105
+ const response = {};
106
+ for (const extension of this.extensions) {
107
+ const hook = extension.beforePlay;
108
+ if (typeof hook !== "function")
109
+ continue;
110
+ try {
111
+ const result = await Promise.resolve(hook.call(extension, this.extensionContext, request));
112
+ if (!result)
113
+ continue;
114
+ if (result.query !== undefined) {
115
+ request.query = result.query;
116
+ response.query = result.query;
117
+ }
118
+ if (result.requestedBy !== undefined) {
119
+ request.requestedBy = result.requestedBy;
120
+ response.requestedBy = result.requestedBy;
121
+ }
122
+ if (Array.isArray(result.tracks)) {
123
+ response.tracks = result.tracks;
124
+ }
125
+ if (typeof result.isPlaylist === "boolean") {
126
+ response.isPlaylist = result.isPlaylist;
127
+ }
128
+ if (typeof result.success === "boolean") {
129
+ response.success = result.success;
130
+ }
131
+ if (result.error instanceof Error) {
132
+ response.error = result.error;
133
+ }
134
+ if (typeof result.handled === "boolean") {
135
+ response.handled = result.handled;
136
+ if (result.handled)
137
+ break;
138
+ }
139
+ }
140
+ catch (err) {
141
+ this.debug(`[Player] Extension ${extension.name} beforePlay error:`, err);
142
+ }
143
+ }
144
+ return { request, response };
145
+ }
146
+ async runAfterPlayHooks(payload) {
147
+ if (this.extensions.length === 0)
148
+ return;
149
+ const safeTracks = payload.tracks ? [...payload.tracks] : undefined;
150
+ if (safeTracks) {
151
+ Object.freeze(safeTracks);
152
+ }
153
+ const immutablePayload = Object.freeze({ ...payload, tracks: safeTracks });
154
+ for (const extension of this.extensions) {
155
+ const hook = extension.afterPlay;
156
+ if (typeof hook !== "function")
157
+ continue;
24
158
  try {
25
- streamInfo = await this.withTimeout(plugin.getStream(track), "getStream timed out");
159
+ await Promise.resolve(hook.call(extension, this.extensionContext, immutablePayload));
26
160
  }
27
- catch (streamError) {
28
- this.debug(`[Player] getStream failed, trying getFallback:`, streamError);
29
- const allplugs = this.pluginManager.getAll();
30
- for (const p of allplugs) {
31
- if (typeof p.getFallback !== "function") {
32
- continue;
33
- }
34
- try {
35
- streamInfo = await this.withTimeout(p.getFallback(track), `getFallback timed out for plugin ${p.name}`);
36
- if (!streamInfo.stream)
161
+ catch (err) {
162
+ this.debug(`[Player] Extension ${extension.name} afterPlay error:`, err);
163
+ }
164
+ }
165
+ }
166
+ async extensionsProvideSearch(query, requestedBy) {
167
+ const request = { query, requestedBy };
168
+ for (const extension of this.extensions) {
169
+ const hook = extension.provideSearch;
170
+ if (typeof hook !== "function")
171
+ continue;
172
+ try {
173
+ const result = await Promise.resolve(hook.call(extension, this.extensionContext, request));
174
+ if (result && Array.isArray(result.tracks) && result.tracks.length > 0) {
175
+ this.debug(`[Player] Extension ${extension.name} handled search for query: ${query}`);
176
+ return result;
177
+ }
178
+ }
179
+ catch (err) {
180
+ this.debug(`[Player] Extension ${extension.name} provideSearch error:`, err);
181
+ }
182
+ }
183
+ return null;
184
+ }
185
+ async extensionsProvideStream(track) {
186
+ const request = { track };
187
+ for (const extension of this.extensions) {
188
+ const hook = extension.provideStream;
189
+ if (typeof hook !== "function")
190
+ continue;
191
+ try {
192
+ const result = await Promise.resolve(hook.call(extension, this.extensionContext, request));
193
+ if (result && result.stream) {
194
+ this.debug(`[Player] Extension ${extension.name} provided stream for track: ${track.title}`);
195
+ return result;
196
+ }
197
+ }
198
+ catch (err) {
199
+ this.debug(`[Player] Extension ${extension.name} provideStream error:`, err);
200
+ }
201
+ }
202
+ return null;
203
+ }
204
+ /**
205
+ * Start playing a specific track immediately, replacing the current resource.
206
+ */
207
+ async startTrack(track) {
208
+ try {
209
+ let streamInfo = await this.extensionsProvideStream(track);
210
+ let plugin;
211
+ if (!streamInfo) {
212
+ plugin = this.pluginManager.findPlugin(track.url) || this.pluginManager.get(track.source);
213
+ if (!plugin) {
214
+ this.debug(`[Player] No plugin found for track: ${track.title}`);
215
+ throw new Error(`No plugin found for track: ${track.title}`);
216
+ }
217
+ this.debug(`[Player] Getting stream for track: ${track.title}`);
218
+ this.debug(`[Player] Using plugin: ${plugin.name}`);
219
+ this.debug(`[Track] Track Info:`, track);
220
+ try {
221
+ streamInfo = await (0, timeout_1.withTimeout)(plugin.getStream(track), this.options.extractorTimeout ?? 15000, "getStream timed out");
222
+ }
223
+ catch (streamError) {
224
+ this.debug(`[Player] getStream failed, trying getFallback:`, streamError);
225
+ const allplugs = this.pluginManager.getAll();
226
+ for (const p of allplugs) {
227
+ if (typeof p.getFallback !== "function") {
37
228
  continue;
38
- this.debug(`[Player] getFallback succeeded with plugin ${p.name} for track: ${track.title}`);
39
- break;
229
+ }
230
+ try {
231
+ streamInfo = await (0, timeout_1.withTimeout)(p.getFallback(track), this.options.extractorTimeout ?? 15000, `getFallback timed out for plugin ${p.name}`);
232
+ if (!streamInfo?.stream)
233
+ continue;
234
+ this.debug(`[Player] getFallback succeeded with plugin ${p.name} for track: ${track.title}`);
235
+ break;
236
+ }
237
+ catch (fallbackError) {
238
+ this.debug(`[Player] getFallback failed with plugin ${p.name}:`, fallbackError);
239
+ }
40
240
  }
41
- catch (fallbackError) {
42
- this.debug(`[Player] getFallback failed with plugin ${p.name}:`, fallbackError);
241
+ if (!streamInfo?.stream) {
242
+ throw new Error(`All getFallback attempts failed for track: ${track.title}`);
43
243
  }
44
244
  }
45
- if (!streamInfo?.stream) {
46
- throw new Error(`All getFallback attempts failed for track: ${track.title}`);
47
- }
245
+ }
246
+ else {
247
+ this.debug(`[Player] Using extension-provided stream for track: ${track.title}`);
248
+ }
249
+ if (plugin) {
48
250
  this.debug(streamInfo);
49
251
  }
50
- function mapToStreamType(type) {
51
- switch (type) {
52
- case "webm/opus":
53
- return voice_1.StreamType.WebmOpus;
54
- case "ogg/opus":
55
- return voice_1.StreamType.OggOpus;
56
- case "arbitrary":
57
- return voice_1.StreamType.Arbitrary;
58
- default:
59
- return voice_1.StreamType.Arbitrary;
252
+ // Kiểm tra nếu có stream thực sự để tạo AudioResource
253
+ if (streamInfo && streamInfo.stream) {
254
+ function mapToStreamType(type) {
255
+ switch (type) {
256
+ case "webm/opus":
257
+ return voice_1.StreamType.WebmOpus;
258
+ case "ogg/opus":
259
+ return voice_1.StreamType.OggOpus;
260
+ case "arbitrary":
261
+ default:
262
+ return voice_1.StreamType.Arbitrary;
263
+ }
264
+ }
265
+ const stream = streamInfo.stream;
266
+ const inputType = mapToStreamType(streamInfo.type);
267
+ this.currentResource = (0, voice_1.createAudioResource)(stream, {
268
+ metadata: track,
269
+ inputType,
270
+ inlineVolume: true,
271
+ });
272
+ // Apply initial volume using the resource's VolumeTransformer
273
+ if (this.volumeInterval) {
274
+ clearInterval(this.volumeInterval);
275
+ this.volumeInterval = null;
60
276
  }
277
+ this.currentResource.volume?.setVolume(this.volume / 100);
278
+ this.debug(`[Player] Playing resource for track: ${track.title}`);
279
+ this.audioPlayer.play(this.currentResource);
280
+ await (0, voice_1.entersState)(this.audioPlayer, voice_1.AudioPlayerStatus.Playing, 5000);
281
+ return true;
61
282
  }
62
- let stream = streamInfo.stream;
63
- let inputType = mapToStreamType(streamInfo.type);
64
- this.currentResource = (0, voice_1.createAudioResource)(stream, {
65
- metadata: track,
66
- inputType,
67
- inlineVolume: true,
68
- });
69
- // Apply initial volume using the resource's VolumeTransformer
70
- if (this.volumeInterval) {
71
- clearInterval(this.volumeInterval);
72
- this.volumeInterval = null;
283
+ else if (streamInfo && !streamInfo.stream) {
284
+ // Extension đang xử lý phát nhạc (như Lavalink) - chỉ đánh dấu đang phát
285
+ this.debug(`[Player] Extension is handling playback for track: ${track.title}`);
286
+ this.isPlaying = true;
287
+ this.isPaused = false;
288
+ this.emit("trackStart", track);
289
+ return true;
290
+ }
291
+ else {
292
+ throw new Error(`No stream available for track: ${track.title}`);
73
293
  }
74
- this.currentResource.volume?.setVolume(this.volume / 100);
75
- this.debug(`[Player] Playing resource for track: ${track.title}`);
76
- this.audioPlayer.play(this.currentResource);
77
- await (0, voice_1.entersState)(this.audioPlayer, voice_1.AudioPlayerStatus.Playing, 5000);
78
- return true;
79
294
  }
80
295
  catch (error) {
81
296
  this.debug(`[Player] startTrack error:`, error);
@@ -90,10 +305,6 @@ class Player extends events_1.EventEmitter {
90
305
  this.debug(`[Player] Cleared leave timeout`);
91
306
  }
92
307
  }
93
- withTimeout(promise, message) {
94
- const timeout = this.options.extractorTimeout ?? 15000;
95
- return Promise.race([promise, new Promise((_, reject) => setTimeout(() => reject(new Error(message)), timeout))]);
96
- }
97
308
  debug(message, ...optionalParams) {
98
309
  if (this.listenerCount("debug") > 0) {
99
310
  this.emit("debug", message, ...optionalParams);
@@ -109,6 +320,7 @@ class Player extends events_1.EventEmitter {
109
320
  this.currentResource = null;
110
321
  this.volumeInterval = null;
111
322
  this.skipLoop = false;
323
+ this.extensions = [];
112
324
  // TTS support
113
325
  this.ttsPlayer = null;
114
326
  this.ttsQueue = [];
@@ -145,6 +357,7 @@ class Player extends events_1.EventEmitter {
145
357
  this.volume = this.options.volume || 100;
146
358
  this.userdata = this.options.userdata;
147
359
  this.setupEventListeners();
360
+ this.extensionContext = Object.freeze({ player: this, manager });
148
361
  // Optionally pre-create the TTS AudioPlayer
149
362
  if (this.options?.tts?.createPlayer) {
150
363
  this.ensureTTSPlayer();
@@ -230,6 +443,14 @@ class Player extends events_1.EventEmitter {
230
443
  this.debug(`[Player] Removing plugin: ${name}`);
231
444
  return this.pluginManager.unregister(name);
232
445
  }
446
+ /**
447
+ * Connect to a voice channel
448
+ *
449
+ * @param {VoiceChannel} channel - Discord voice channel
450
+ * @returns {Promise<VoiceConnection>} The voice connection
451
+ * @example
452
+ * await player.connect(voiceChannel);
453
+ */
233
454
  async connect(channel) {
234
455
  try {
235
456
  this.debug(`[Player] Connecting to voice channel: ${channel.id}`);
@@ -261,14 +482,29 @@ class Player extends events_1.EventEmitter {
261
482
  throw error;
262
483
  }
263
484
  }
485
+ /**
486
+ * Search for tracks using the player's extensions and plugins
487
+ *
488
+ * @param {string} query - The query to search for
489
+ * @param {string} requestedBy - The user ID who requested the search
490
+ * @returns {Promise<SearchResult>} The search result
491
+ * @example
492
+ * const result = await player.search("Never Gonna Give You Up", userId);
493
+ * console.log(`Search result: ${result.tracks.length} tracks`);
494
+ */
264
495
  async search(query, requestedBy) {
265
496
  this.debug(`[Player] Search called with query: ${query}, requestedBy: ${requestedBy}`);
497
+ const extensionResult = await this.extensionsProvideSearch(query, requestedBy);
498
+ if (extensionResult && Array.isArray(extensionResult.tracks) && extensionResult.tracks.length > 0) {
499
+ this.debug(`[Player] Extension handled search for query: ${query}`);
500
+ return extensionResult;
501
+ }
266
502
  const plugins = this.pluginManager.getAll();
267
503
  let lastError = null;
268
504
  for (const p of plugins) {
269
505
  try {
270
506
  this.debug(`[Player] Trying plugin for search: ${p.name}`);
271
- const res = await this.withTimeout(p.search(query, requestedBy), `Search operation timed out for ${p.name}`);
507
+ const res = await (0, timeout_1.withTimeout)(p.search(query, requestedBy), this.options.extractorTimeout ?? 15000, `Search operation timed out for ${p.name}`);
272
508
  if (res && Array.isArray(res.tracks) && res.tracks.length > 0) {
273
509
  this.debug(`[Player] Plugin '${p.name}' returned ${res.tracks.length} tracks`);
274
510
  return res;
@@ -286,29 +522,66 @@ class Player extends events_1.EventEmitter {
286
522
  this.emit("playerError", lastError);
287
523
  throw new Error(`No plugin found to handle: ${query}`);
288
524
  }
525
+ /**
526
+ * Play a track or search query
527
+ *
528
+ * @param {string | Track} query - Track URL, search query, or Track object
529
+ * @param {string} requestedBy - User ID who requested the track
530
+ * @returns {Promise<boolean>} True if playback started successfully
531
+ * @example
532
+ * await player.play("Never Gonna Give You Up", userId);
533
+ * await player.play("https://youtube.com/watch?v=dQw4w9WgXcQ", userId);
534
+ * await player.play("tts: Hello everyone!", userId);
535
+ */
289
536
  async play(query, requestedBy) {
537
+ this.debug(`[Player] Play called with query: ${typeof query === "string" ? query : query?.title}`);
538
+ this.clearLeaveTimeout();
539
+ let tracksToAdd = [];
540
+ let isPlaylist = false;
541
+ let effectiveRequest = { query, requestedBy };
542
+ let hookResponse = {};
290
543
  try {
291
- this.debug(`[Player] Play called with query: ${typeof query === "string" ? query : query?.title}`);
292
- // If a leave was scheduled due to previous idle, cancel it now
293
- this.clearLeaveTimeout();
294
- let tracksToAdd = [];
295
- let isPlaylist = false;
296
- if (typeof query === "string") {
297
- const searchResult = await this.search(query, requestedBy || "Unknown");
544
+ const hookOutcome = await this.runBeforePlayHooks(effectiveRequest);
545
+ effectiveRequest = hookOutcome.request;
546
+ hookResponse = hookOutcome.response;
547
+ if (effectiveRequest.requestedBy === undefined) {
548
+ effectiveRequest.requestedBy = requestedBy;
549
+ }
550
+ const hookTracks = Array.isArray(hookResponse.tracks) ? hookResponse.tracks : undefined;
551
+ if (hookResponse.handled && (!hookTracks || hookTracks.length === 0)) {
552
+ const handledPayload = {
553
+ success: hookResponse.success ?? true,
554
+ query: effectiveRequest.query,
555
+ requestedBy: effectiveRequest.requestedBy,
556
+ tracks: [],
557
+ isPlaylist: hookResponse.isPlaylist ?? false,
558
+ error: hookResponse.error,
559
+ };
560
+ await this.runAfterPlayHooks(handledPayload);
561
+ if (hookResponse.error) {
562
+ this.emit("playerError", hookResponse.error);
563
+ }
564
+ return hookResponse.success ?? true;
565
+ }
566
+ if (hookTracks && hookTracks.length > 0) {
567
+ tracksToAdd = hookTracks;
568
+ isPlaylist = hookResponse.isPlaylist ?? hookTracks.length > 1;
569
+ }
570
+ else if (typeof effectiveRequest.query === "string") {
571
+ const searchResult = await this.search(effectiveRequest.query, effectiveRequest.requestedBy || "Unknown");
298
572
  tracksToAdd = searchResult.tracks;
299
573
  if (searchResult.playlist) {
300
574
  isPlaylist = true;
301
575
  this.debug(`[Player] Added playlist: ${searchResult.playlist.name} (${tracksToAdd.length} tracks)`);
302
576
  }
303
577
  }
304
- else {
305
- tracksToAdd = [query];
578
+ else if (effectiveRequest.query) {
579
+ tracksToAdd = [effectiveRequest.query];
306
580
  }
307
581
  if (tracksToAdd.length === 0) {
308
582
  this.debug(`[Player] No tracks found for play`);
309
583
  throw new Error("No tracks found");
310
584
  }
311
- // If a TTS track is requested and interrupt mode is enabled, handle it separately
312
585
  const isTTS = (t) => {
313
586
  if (!t)
314
587
  return false;
@@ -319,14 +592,20 @@ class Player extends events_1.EventEmitter {
319
592
  return false;
320
593
  }
321
594
  };
322
- const queryLooksTTS = typeof query === "string" && query.trim().toLowerCase().startsWith("tts");
595
+ const queryLooksTTS = typeof effectiveRequest.query === "string" && effectiveRequest.query.trim().toLowerCase().startsWith("tts");
323
596
  if (!isPlaylist &&
324
597
  tracksToAdd.length > 0 &&
325
598
  this.options?.tts?.interrupt !== false &&
326
599
  (isTTS(tracksToAdd[0]) || queryLooksTTS)) {
327
- // Interrupt music playback with TTS (do not modify the music queue)
328
600
  this.debug(`[Player] Interrupting with TTS: ${tracksToAdd[0].title}`);
329
601
  await this.interruptWithTTSTrack(tracksToAdd[0]);
602
+ await this.runAfterPlayHooks({
603
+ success: true,
604
+ query: effectiveRequest.query,
605
+ requestedBy: effectiveRequest.requestedBy,
606
+ tracks: tracksToAdd,
607
+ isPlaylist,
608
+ });
330
609
  return true;
331
610
  }
332
611
  if (isPlaylist) {
@@ -334,16 +613,28 @@ class Player extends events_1.EventEmitter {
334
613
  this.emit("queueAddList", tracksToAdd);
335
614
  }
336
615
  else {
337
- this.queue.add(tracksToAdd?.[0]);
338
- this.emit("queueAdd", tracksToAdd?.[0]);
339
- }
340
- // Start playing if not already playing
341
- if (!this.isPlaying) {
342
- return this.playNext();
616
+ this.queue.add(tracksToAdd[0]);
617
+ this.emit("queueAdd", tracksToAdd[0]);
343
618
  }
344
- return true;
619
+ const started = !this.isPlaying ? await this.playNext() : true;
620
+ await this.runAfterPlayHooks({
621
+ success: started,
622
+ query: effectiveRequest.query,
623
+ requestedBy: effectiveRequest.requestedBy,
624
+ tracks: tracksToAdd,
625
+ isPlaylist,
626
+ });
627
+ return started;
345
628
  }
346
629
  catch (error) {
630
+ await this.runAfterPlayHooks({
631
+ success: false,
632
+ query: effectiveRequest.query,
633
+ requestedBy: effectiveRequest.requestedBy,
634
+ tracks: tracksToAdd,
635
+ isPlaylist,
636
+ error: error,
637
+ });
347
638
  this.debug(`[Player] Play error:`, error);
348
639
  this.emit("playerError", error);
349
640
  return false;
@@ -352,6 +643,11 @@ class Player extends events_1.EventEmitter {
352
643
  /**
353
644
  * Interrupt current music with a TTS track. Pauses music, swaps the
354
645
  * subscription to a dedicated TTS player, plays TTS, then resumes.
646
+ *
647
+ * @param {Track} track - The track to interrupt with
648
+ * @returns {Promise<void>}
649
+ * @example
650
+ * await player.interruptWithTTSTrack(track);
355
651
  */
356
652
  async interruptWithTTSTrack(track) {
357
653
  this.ttsQueue.push(track);
@@ -359,7 +655,13 @@ class Player extends events_1.EventEmitter {
359
655
  void this.playNextTTS();
360
656
  }
361
657
  }
362
- /** Play queued TTS items sequentially */
658
+ /**
659
+ * Play queued TTS items sequentially
660
+ *
661
+ * @returns {Promise<void>}
662
+ * @example
663
+ * await player.playNextTTS();
664
+ */
363
665
  async playNextTTS() {
364
666
  const next = this.ttsQueue.shift();
365
667
  if (!next)
@@ -389,8 +691,14 @@ class Player extends events_1.EventEmitter {
389
691
  await (0, voice_1.entersState)(ttsPlayer, voice_1.AudioPlayerStatus.Playing, 5000).catch(() => null);
390
692
  // Derive timeout from resource/track duration when available, with a sensible cap
391
693
  const md = resource?.metadata ?? {};
392
- const declared = typeof md.duration === "number" ? md.duration : typeof next?.duration === "number" ? next.duration : undefined;
393
- const declaredMs = declared ? (declared > 1000 ? declared : declared * 1000) : undefined;
694
+ const declared = typeof md.duration === "number" ? md.duration
695
+ : typeof next?.duration === "number" ? next.duration
696
+ : undefined;
697
+ const declaredMs = declared ?
698
+ declared > 1000 ?
699
+ declared
700
+ : declared * 1000
701
+ : undefined;
394
702
  const cap = this.options?.tts?.Max_Time_TTS ?? 60000;
395
703
  const idleTimeout = declaredMs ? Math.min(cap, Math.max(1000, declaredMs + 1500)) : cap;
396
704
  await (0, voice_1.entersState)(ttsPlayer, voice_1.AudioPlayerStatus.Idle, idleTimeout).catch(() => null);
@@ -423,7 +731,7 @@ class Player extends events_1.EventEmitter {
423
731
  throw new Error(`No plugin found for track: ${track.title}`);
424
732
  let streamInfo;
425
733
  try {
426
- streamInfo = await this.withTimeout(plugin.getStream(track), "getStream timed out");
734
+ streamInfo = await (0, timeout_1.withTimeout)(plugin.getStream(track), this.options.extractorTimeout ?? 15000, "getStream timed out");
427
735
  }
428
736
  catch (streamError) {
429
737
  // try fallbacks
@@ -432,7 +740,7 @@ class Player extends events_1.EventEmitter {
432
740
  if (typeof p.getFallback !== "function")
433
741
  continue;
434
742
  try {
435
- streamInfo = await this.withTimeout(p.getFallback(track), `getFallback timed out for plugin ${p.name}`);
743
+ streamInfo = await (0, timeout_1.withTimeout)(p.getFallback(track), this.options.extractorTimeout ?? 15000, `getFallback timed out for plugin ${p.name}`);
436
744
  if (!streamInfo?.stream)
437
745
  continue;
438
746
  break;
@@ -475,10 +783,10 @@ class Player extends events_1.EventEmitter {
475
783
  for (const p of candidates) {
476
784
  try {
477
785
  this.debug(`[Player] Trying related from plugin: ${p.name}`);
478
- const related = await this.withTimeout(p.getRelatedTracks(lastTrack.url, {
786
+ const related = await (0, timeout_1.withTimeout)(p.getRelatedTracks(lastTrack.url, {
479
787
  limit: 10,
480
788
  history: this.queue.previousTracks,
481
- }), `getRelatedTracks timed out for ${p.name}`);
789
+ }), this.options.extractorTimeout ?? 15000, `getRelatedTracks timed out for ${p.name}`);
482
790
  if (Array.isArray(related) && related.length > 0) {
483
791
  const randomchoice = Math.floor(Math.random() * related.length);
484
792
  const nextTrack = this.queue.nextTrack ? this.queue.nextTrack : related[randomchoice];
@@ -529,6 +837,14 @@ class Player extends events_1.EventEmitter {
529
837
  return this.playNext();
530
838
  }
531
839
  }
840
+ /**
841
+ * Pause the current track
842
+ *
843
+ * @returns {boolean} True if paused successfully
844
+ * @example
845
+ * const paused = player.pause();
846
+ * console.log(`Paused: ${paused}`);
847
+ */
532
848
  pause() {
533
849
  this.debug(`[Player] pause called`);
534
850
  if (this.isPlaying && !this.isPaused) {
@@ -536,6 +852,14 @@ class Player extends events_1.EventEmitter {
536
852
  }
537
853
  return false;
538
854
  }
855
+ /**
856
+ * Resume the current track
857
+ *
858
+ * @returns {boolean} True if resumed successfully
859
+ * @example
860
+ * const resumed = player.resume();
861
+ * console.log(`Resumed: ${resumed}`);
862
+ */
539
863
  resume() {
540
864
  this.debug(`[Player] resume called`);
541
865
  if (this.isPaused) {
@@ -551,6 +875,14 @@ class Player extends events_1.EventEmitter {
551
875
  }
552
876
  return false;
553
877
  }
878
+ /**
879
+ * Stop the current track
880
+ *
881
+ * @returns {boolean} True if stopped successfully
882
+ * @example
883
+ * const stopped = player.stop();
884
+ * console.log(`Stopped: ${stopped}`);
885
+ */
554
886
  stop() {
555
887
  this.debug(`[Player] stop called`);
556
888
  this.queue.clear();
@@ -560,6 +892,14 @@ class Player extends events_1.EventEmitter {
560
892
  this.emit("playerStop");
561
893
  return result;
562
894
  }
895
+ /**
896
+ * Skip to the next track
897
+ *
898
+ * @returns {boolean} True if skipped successfully
899
+ * @example
900
+ * const skipped = player.skip();
901
+ * console.log(`Skipped: ${skipped}`);
902
+ */
563
903
  skip() {
564
904
  this.debug(`[Player] skip called`);
565
905
  if (this.isPlaying || this.isPaused) {
@@ -570,6 +910,11 @@ class Player extends events_1.EventEmitter {
570
910
  }
571
911
  /**
572
912
  * Go back to the previous track in history and play it.
913
+ *
914
+ * @returns {Promise<boolean>} True if previous track was played successfully
915
+ * @example
916
+ * const previous = await player.previous();
917
+ * console.log(`Previous: ${previous}`);
573
918
  */
574
919
  async previous() {
575
920
  this.debug(`[Player] previous called`);
@@ -581,12 +926,39 @@ class Player extends events_1.EventEmitter {
581
926
  this.clearLeaveTimeout();
582
927
  return this.startTrack(track);
583
928
  }
929
+ /**
930
+ * Loop the current track
931
+ *
932
+ * @param {LoopMode} mode - The loop mode to set
933
+ * @returns {LoopMode} The loop mode
934
+ * @example
935
+ * const loopMode = player.loop("track");
936
+ * console.log(`Loop mode: ${loopMode}`);
937
+ */
584
938
  loop(mode) {
585
939
  return this.queue.loop(mode);
586
940
  }
941
+ /**
942
+ * Set the auto-play mode
943
+ *
944
+ * @param {boolean} mode - The auto-play mode to set
945
+ * @returns {boolean} The auto-play mode
946
+ * @example
947
+ * const autoPlayMode = player.autoPlay(true);
948
+ * console.log(`Auto-play mode: ${autoPlayMode}`);
949
+ */
587
950
  autoPlay(mode) {
588
951
  return this.queue.autoPlay(mode);
589
952
  }
953
+ /**
954
+ * Set the volume of the current track
955
+ *
956
+ * @param {number} volume - The volume to set
957
+ * @returns {boolean} True if volume was set successfully
958
+ * @example
959
+ * const volumeSet = player.setVolume(50);
960
+ * console.log(`Volume set: ${volumeSet}`);
961
+ */
590
962
  setVolume(volume) {
591
963
  this.debug(`[Player] setVolume called: ${volume}`);
592
964
  if (volume < 0 || volume > 200)
@@ -614,10 +986,24 @@ class Player extends events_1.EventEmitter {
614
986
  this.emit("volumeChange", oldVolume, volume);
615
987
  return true;
616
988
  }
989
+ /**
990
+ * Shuffle the queue
991
+ *
992
+ * @returns {void}
993
+ * @example
994
+ * player.shuffle();
995
+ */
617
996
  shuffle() {
618
997
  this.debug(`[Player] shuffle called`);
619
998
  this.queue.shuffle();
620
999
  }
1000
+ /**
1001
+ * Clear the queue
1002
+ *
1003
+ * @returns {void}
1004
+ * @example
1005
+ * player.clearQueue();
1006
+ */
621
1007
  clearQueue() {
622
1008
  this.debug(`[Player] clearQueue called`);
623
1009
  this.queue.clear();
@@ -627,6 +1013,14 @@ class Player extends events_1.EventEmitter {
627
1013
  * - If `query` is a string, performs a search and inserts resulting tracks (playlist supported).
628
1014
  * - If a Track or Track[] is provided, inserts directly.
629
1015
  * Does not auto-start playback; it only modifies the queue.
1016
+ *
1017
+ * @param {string | Track | Track[]} query - The track or tracks to insert
1018
+ * @param {number} index - The index to insert the tracks at
1019
+ * @param {string} requestedBy - The user ID who requested the insert
1020
+ * @returns {Promise<boolean>} True if the tracks were inserted successfully
1021
+ * @example
1022
+ * const inserted = await player.insert("Song Name", 0, userId);
1023
+ * console.log(`Inserted: ${inserted}`);
630
1024
  */
631
1025
  async insert(query, index, requestedBy) {
632
1026
  try {
@@ -667,6 +1061,15 @@ class Player extends events_1.EventEmitter {
667
1061
  return false;
668
1062
  }
669
1063
  }
1064
+ /**
1065
+ * Remove a track from the queue
1066
+ *
1067
+ * @param {number} index - The index of the track to remove
1068
+ * @returns {Track | null} The removed track or null
1069
+ * @example
1070
+ * const removed = player.remove(0);
1071
+ * console.log(`Removed: ${removed?.title}`);
1072
+ */
670
1073
  remove(index) {
671
1074
  this.debug(`[Player] remove called for index: ${index}`);
672
1075
  const track = this.queue.remove(index);
@@ -675,6 +1078,15 @@ class Player extends events_1.EventEmitter {
675
1078
  }
676
1079
  return track;
677
1080
  }
1081
+ /**
1082
+ * Get the progress bar of the current track
1083
+ *
1084
+ * @param {ProgressBarOptions} options - The options for the progress bar
1085
+ * @returns {string} The progress bar
1086
+ * @example
1087
+ * const progressBar = player.getProgressBar();
1088
+ * console.log(`Progress bar: ${progressBar}`);
1089
+ */
678
1090
  getProgressBar(options = {}) {
679
1091
  const { size = 20, barChar = "▬", progressChar = "🔘" } = options;
680
1092
  const track = this.queue.currentTrack;
@@ -690,6 +1102,14 @@ class Player extends events_1.EventEmitter {
690
1102
  const bar = barChar.repeat(progress) + progressChar + barChar.repeat(size - progress);
691
1103
  return `${this.formatTime(current)} | ${bar} | ${this.formatTime(total)}`;
692
1104
  }
1105
+ /**
1106
+ * Get the time of the current track
1107
+ *
1108
+ * @returns {Object} The time of the current track
1109
+ * @example
1110
+ * const time = player.getTime();
1111
+ * console.log(`Time: ${time.current}`);
1112
+ */
693
1113
  getTime() {
694
1114
  const resource = this.currentResource;
695
1115
  const track = this.queue.currentTrack;
@@ -706,6 +1126,15 @@ class Player extends events_1.EventEmitter {
706
1126
  format: this.formatTime(resource.playbackDuration),
707
1127
  };
708
1128
  }
1129
+ /**
1130
+ * Format the time in the format of HH:MM:SS
1131
+ *
1132
+ * @param {number} ms - The time in milliseconds
1133
+ * @returns {string} The formatted time
1134
+ * @example
1135
+ * const formattedTime = player.formatTime(1000);
1136
+ * console.log(`Formatted time: ${formattedTime}`);
1137
+ */
709
1138
  formatTime(ms) {
710
1139
  const totalSeconds = Math.floor(ms / 1000);
711
1140
  const hours = Math.floor(totalSeconds / 3600);
@@ -730,6 +1159,13 @@ class Player extends events_1.EventEmitter {
730
1159
  }, this.options.leaveTimeout);
731
1160
  }
732
1161
  }
1162
+ /**
1163
+ * Destroy the player
1164
+ *
1165
+ * @returns {void}
1166
+ * @example
1167
+ * player.destroy();
1168
+ */
733
1169
  destroy() {
734
1170
  this.debug(`[Player] destroy called`);
735
1171
  if (this.leaveTimeout) {
@@ -750,30 +1186,92 @@ class Player extends events_1.EventEmitter {
750
1186
  }
751
1187
  this.queue.clear();
752
1188
  this.pluginManager.clear();
1189
+ for (const extension of [...this.extensions]) {
1190
+ this.invokeExtensionLifecycle(extension, "onDestroy");
1191
+ if (extension.player === this) {
1192
+ extension.player = null;
1193
+ }
1194
+ }
1195
+ this.extensions = [];
753
1196
  this.isPlaying = false;
754
1197
  this.isPaused = false;
755
1198
  this.emit("playerDestroy");
756
1199
  this.removeAllListeners();
757
1200
  }
758
- // Getters
1201
+ /**
1202
+ * Get the size of the queue
1203
+ *
1204
+ * @returns {number} The size of the queue
1205
+ * @example
1206
+ * const queueSize = player.queueSize;
1207
+ * console.log(`Queue size: ${queueSize}`);
1208
+ */
759
1209
  get queueSize() {
760
1210
  return this.queue.size;
761
1211
  }
1212
+ /**
1213
+ * Get the current track
1214
+ *
1215
+ * @returns {Track | null} The current track or null
1216
+ * @example
1217
+ * const currentTrack = player.currentTrack;
1218
+ * console.log(`Current track: ${currentTrack?.title}`);
1219
+ */
762
1220
  get currentTrack() {
763
1221
  return this.queue.currentTrack;
764
1222
  }
1223
+ /**
1224
+ * Get the previous track
1225
+ *
1226
+ * @returns {Track | null} The previous track or null
1227
+ * @example
1228
+ * const previousTrack = player.previousTrack;
1229
+ * console.log(`Previous track: ${previousTrack?.title}`);
1230
+ */
765
1231
  get previousTrack() {
766
1232
  return this.queue.previousTracks?.at(-1) ?? null;
767
1233
  }
1234
+ /**
1235
+ * Get the upcoming tracks
1236
+ *
1237
+ * @returns {Track[]} The upcoming tracks
1238
+ * @example
1239
+ * const upcomingTracks = player.upcomingTracks;
1240
+ * console.log(`Upcoming tracks: ${upcomingTracks.length}`);
1241
+ */
768
1242
  get upcomingTracks() {
769
1243
  return this.queue.getTracks();
770
1244
  }
1245
+ /**
1246
+ * Get the previous tracks
1247
+ *
1248
+ * @returns {Track[]} The previous tracks
1249
+ * @example
1250
+ * const previousTracks = player.previousTracks;
1251
+ * console.log(`Previous tracks: ${previousTracks.length}`);
1252
+ */
771
1253
  get previousTracks() {
772
1254
  return this.queue.previousTracks;
773
1255
  }
1256
+ /**
1257
+ * Get the available plugins
1258
+ *
1259
+ * @returns {string[]} The available plugins
1260
+ * @example
1261
+ * const availablePlugins = player.availablePlugins;
1262
+ * console.log(`Available plugins: ${availablePlugins.length}`);
1263
+ */
774
1264
  get availablePlugins() {
775
1265
  return this.pluginManager.getAll().map((p) => p.name);
776
1266
  }
1267
+ /**
1268
+ * Get the related tracks
1269
+ *
1270
+ * @returns {Track[] | null} The related tracks or null
1271
+ * @example
1272
+ * const relatedTracks = player.relatedTracks;
1273
+ * console.log(`Related tracks: ${relatedTracks?.length}`);
1274
+ */
777
1275
  get relatedTracks() {
778
1276
  return this.queue.relatedTracks();
779
1277
  }