ziplayer 0.0.8 → 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)
@@ -429,7 +731,7 @@ class Player extends events_1.EventEmitter {
429
731
  throw new Error(`No plugin found for track: ${track.title}`);
430
732
  let streamInfo;
431
733
  try {
432
- 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");
433
735
  }
434
736
  catch (streamError) {
435
737
  // try fallbacks
@@ -438,7 +740,7 @@ class Player extends events_1.EventEmitter {
438
740
  if (typeof p.getFallback !== "function")
439
741
  continue;
440
742
  try {
441
- 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}`);
442
744
  if (!streamInfo?.stream)
443
745
  continue;
444
746
  break;
@@ -481,10 +783,10 @@ class Player extends events_1.EventEmitter {
481
783
  for (const p of candidates) {
482
784
  try {
483
785
  this.debug(`[Player] Trying related from plugin: ${p.name}`);
484
- const related = await this.withTimeout(p.getRelatedTracks(lastTrack.url, {
786
+ const related = await (0, timeout_1.withTimeout)(p.getRelatedTracks(lastTrack.url, {
485
787
  limit: 10,
486
788
  history: this.queue.previousTracks,
487
- }), `getRelatedTracks timed out for ${p.name}`);
789
+ }), this.options.extractorTimeout ?? 15000, `getRelatedTracks timed out for ${p.name}`);
488
790
  if (Array.isArray(related) && related.length > 0) {
489
791
  const randomchoice = Math.floor(Math.random() * related.length);
490
792
  const nextTrack = this.queue.nextTrack ? this.queue.nextTrack : related[randomchoice];
@@ -535,6 +837,14 @@ class Player extends events_1.EventEmitter {
535
837
  return this.playNext();
536
838
  }
537
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
+ */
538
848
  pause() {
539
849
  this.debug(`[Player] pause called`);
540
850
  if (this.isPlaying && !this.isPaused) {
@@ -542,6 +852,14 @@ class Player extends events_1.EventEmitter {
542
852
  }
543
853
  return false;
544
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
+ */
545
863
  resume() {
546
864
  this.debug(`[Player] resume called`);
547
865
  if (this.isPaused) {
@@ -557,6 +875,14 @@ class Player extends events_1.EventEmitter {
557
875
  }
558
876
  return false;
559
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
+ */
560
886
  stop() {
561
887
  this.debug(`[Player] stop called`);
562
888
  this.queue.clear();
@@ -566,6 +892,14 @@ class Player extends events_1.EventEmitter {
566
892
  this.emit("playerStop");
567
893
  return result;
568
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
+ */
569
903
  skip() {
570
904
  this.debug(`[Player] skip called`);
571
905
  if (this.isPlaying || this.isPaused) {
@@ -576,6 +910,11 @@ class Player extends events_1.EventEmitter {
576
910
  }
577
911
  /**
578
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}`);
579
918
  */
580
919
  async previous() {
581
920
  this.debug(`[Player] previous called`);
@@ -587,12 +926,39 @@ class Player extends events_1.EventEmitter {
587
926
  this.clearLeaveTimeout();
588
927
  return this.startTrack(track);
589
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
+ */
590
938
  loop(mode) {
591
939
  return this.queue.loop(mode);
592
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
+ */
593
950
  autoPlay(mode) {
594
951
  return this.queue.autoPlay(mode);
595
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
+ */
596
962
  setVolume(volume) {
597
963
  this.debug(`[Player] setVolume called: ${volume}`);
598
964
  if (volume < 0 || volume > 200)
@@ -620,10 +986,24 @@ class Player extends events_1.EventEmitter {
620
986
  this.emit("volumeChange", oldVolume, volume);
621
987
  return true;
622
988
  }
989
+ /**
990
+ * Shuffle the queue
991
+ *
992
+ * @returns {void}
993
+ * @example
994
+ * player.shuffle();
995
+ */
623
996
  shuffle() {
624
997
  this.debug(`[Player] shuffle called`);
625
998
  this.queue.shuffle();
626
999
  }
1000
+ /**
1001
+ * Clear the queue
1002
+ *
1003
+ * @returns {void}
1004
+ * @example
1005
+ * player.clearQueue();
1006
+ */
627
1007
  clearQueue() {
628
1008
  this.debug(`[Player] clearQueue called`);
629
1009
  this.queue.clear();
@@ -633,6 +1013,14 @@ class Player extends events_1.EventEmitter {
633
1013
  * - If `query` is a string, performs a search and inserts resulting tracks (playlist supported).
634
1014
  * - If a Track or Track[] is provided, inserts directly.
635
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}`);
636
1024
  */
637
1025
  async insert(query, index, requestedBy) {
638
1026
  try {
@@ -673,6 +1061,15 @@ class Player extends events_1.EventEmitter {
673
1061
  return false;
674
1062
  }
675
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
+ */
676
1073
  remove(index) {
677
1074
  this.debug(`[Player] remove called for index: ${index}`);
678
1075
  const track = this.queue.remove(index);
@@ -681,6 +1078,15 @@ class Player extends events_1.EventEmitter {
681
1078
  }
682
1079
  return track;
683
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
+ */
684
1090
  getProgressBar(options = {}) {
685
1091
  const { size = 20, barChar = "▬", progressChar = "🔘" } = options;
686
1092
  const track = this.queue.currentTrack;
@@ -696,6 +1102,39 @@ class Player extends events_1.EventEmitter {
696
1102
  const bar = barChar.repeat(progress) + progressChar + barChar.repeat(size - progress);
697
1103
  return `${this.formatTime(current)} | ${bar} | ${this.formatTime(total)}`;
698
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
+ */
1113
+ getTime() {
1114
+ const resource = this.currentResource;
1115
+ const track = this.queue.currentTrack;
1116
+ if (!track || !resource)
1117
+ return {
1118
+ current: 0,
1119
+ total: 0,
1120
+ format: "00:00",
1121
+ };
1122
+ const total = track.duration > 1000 ? track.duration : track.duration * 1000;
1123
+ return {
1124
+ current: resource?.playbackDuration,
1125
+ total: total,
1126
+ format: this.formatTime(resource.playbackDuration),
1127
+ };
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
+ */
699
1138
  formatTime(ms) {
700
1139
  const totalSeconds = Math.floor(ms / 1000);
701
1140
  const hours = Math.floor(totalSeconds / 3600);
@@ -720,6 +1159,13 @@ class Player extends events_1.EventEmitter {
720
1159
  }, this.options.leaveTimeout);
721
1160
  }
722
1161
  }
1162
+ /**
1163
+ * Destroy the player
1164
+ *
1165
+ * @returns {void}
1166
+ * @example
1167
+ * player.destroy();
1168
+ */
723
1169
  destroy() {
724
1170
  this.debug(`[Player] destroy called`);
725
1171
  if (this.leaveTimeout) {
@@ -740,30 +1186,92 @@ class Player extends events_1.EventEmitter {
740
1186
  }
741
1187
  this.queue.clear();
742
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 = [];
743
1196
  this.isPlaying = false;
744
1197
  this.isPaused = false;
745
1198
  this.emit("playerDestroy");
746
1199
  this.removeAllListeners();
747
1200
  }
748
- // 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
+ */
749
1209
  get queueSize() {
750
1210
  return this.queue.size;
751
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
+ */
752
1220
  get currentTrack() {
753
1221
  return this.queue.currentTrack;
754
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
+ */
755
1231
  get previousTrack() {
756
1232
  return this.queue.previousTracks?.at(-1) ?? null;
757
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
+ */
758
1242
  get upcomingTracks() {
759
1243
  return this.queue.getTracks();
760
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
+ */
761
1253
  get previousTracks() {
762
1254
  return this.queue.previousTracks;
763
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
+ */
764
1264
  get availablePlugins() {
765
1265
  return this.pluginManager.getAll().map((p) => p.name);
766
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
+ */
767
1275
  get relatedTracks() {
768
1276
  return this.queue.relatedTracks();
769
1277
  }