ziplayer 0.1.3 → 0.1.5

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.
Files changed (34) hide show
  1. package/README.md +212 -212
  2. package/dist/plugins/SoundCloudPlugin.d.ts +22 -0
  3. package/dist/plugins/SoundCloudPlugin.d.ts.map +1 -0
  4. package/dist/plugins/SoundCloudPlugin.js +171 -0
  5. package/dist/plugins/SoundCloudPlugin.js.map +1 -0
  6. package/dist/plugins/SpotifyPlugin.d.ts +26 -0
  7. package/dist/plugins/SpotifyPlugin.d.ts.map +1 -0
  8. package/dist/plugins/SpotifyPlugin.js +183 -0
  9. package/dist/plugins/SpotifyPlugin.js.map +1 -0
  10. package/dist/plugins/YouTubePlugin.d.ts +25 -0
  11. package/dist/plugins/YouTubePlugin.d.ts.map +1 -0
  12. package/dist/plugins/YouTubePlugin.js +314 -0
  13. package/dist/plugins/YouTubePlugin.js.map +1 -0
  14. package/dist/structures/Player.d.ts +61 -70
  15. package/dist/structures/Player.d.ts.map +1 -1
  16. package/dist/structures/Player.js +332 -355
  17. package/dist/structures/Player.js.map +1 -1
  18. package/dist/structures/PlayerManager.d.ts +5 -1
  19. package/dist/structures/PlayerManager.d.ts.map +1 -1
  20. package/dist/structures/PlayerManager.js.map +1 -1
  21. package/dist/types/index.d.ts +58 -16
  22. package/dist/types/index.d.ts.map +1 -1
  23. package/package.json +45 -45
  24. package/src/extensions/BaseExtension.ts +35 -35
  25. package/src/extensions/index.ts +32 -32
  26. package/src/index.ts +16 -16
  27. package/src/plugins/BasePlugin.ts +26 -26
  28. package/src/plugins/index.ts +32 -32
  29. package/src/structures/Player.ts +1693 -1747
  30. package/src/structures/PlayerManager.ts +416 -411
  31. package/src/structures/Queue.ts +354 -354
  32. package/src/types/index.ts +510 -470
  33. package/src/utils/timeout.ts +10 -10
  34. package/tsconfig.json +23 -23
@@ -201,6 +201,80 @@ class Player extends events_1.EventEmitter {
201
201
  }
202
202
  return null;
203
203
  }
204
+ async getStreamFromPlugin(track) {
205
+ let streamInfo = null;
206
+ const plugin = this.pluginManager.get(track.source) || this.pluginManager.findPlugin(track.url);
207
+ if (!plugin) {
208
+ this.debug(`[Player] No plugin found for track: ${track.title}`);
209
+ return null;
210
+ }
211
+ this.debug(`[Player] Getting stream for track: ${track.title}`);
212
+ this.debug(`[Player] Using plugin: ${plugin.name}`);
213
+ this.debug(`[Track] Track Info:`, track);
214
+ const timeoutMs = this.options.extractorTimeout ?? 50000;
215
+ try {
216
+ streamInfo = await (0, timeout_1.withTimeout)(plugin.getStream(track), timeoutMs, "getStream timed out");
217
+ if (!streamInfo?.stream) {
218
+ throw new Error(`No stream returned from ${plugin.name}`);
219
+ }
220
+ }
221
+ catch (streamError) {
222
+ this.debug(`[Player] getStream failed, trying getFallback:`, streamError);
223
+ const allplugs = this.pluginManager.getAll();
224
+ for (const p of allplugs) {
225
+ if (typeof p.getFallback !== "function" && typeof p.getStream !== "function") {
226
+ continue;
227
+ }
228
+ try {
229
+ streamInfo = await (0, timeout_1.withTimeout)(p.getStream(track), timeoutMs, `getStream timed out for plugin ${p.name}`);
230
+ if (streamInfo?.stream) {
231
+ this.debug(`[Player] getStream succeeded with plugin ${p.name} for track: ${track.title}`);
232
+ break;
233
+ }
234
+ streamInfo = await (0, timeout_1.withTimeout)(p.getFallback(track), timeoutMs, `getFallback timed out for plugin ${p.name}`);
235
+ if (!streamInfo?.stream)
236
+ continue;
237
+ break;
238
+ }
239
+ catch (fallbackError) {
240
+ this.debug(`[Player] getFallback failed with plugin ${p.name}:`, fallbackError);
241
+ }
242
+ }
243
+ if (!streamInfo?.stream) {
244
+ throw new Error(`All getFallback attempts failed for track: ${track.title}`);
245
+ }
246
+ }
247
+ return streamInfo;
248
+ }
249
+ async Audioresource(streamInfo, track) {
250
+ function mapToStreamType(type) {
251
+ switch (type) {
252
+ case "webm/opus":
253
+ return voice_1.StreamType.WebmOpus;
254
+ case "ogg/opus":
255
+ return voice_1.StreamType.OggOpus;
256
+ case "arbitrary":
257
+ default:
258
+ return voice_1.StreamType.Arbitrary;
259
+ }
260
+ }
261
+ const stream = streamInfo.stream;
262
+ const inputType = mapToStreamType(streamInfo.type);
263
+ const resource = (0, voice_1.createAudioResource)(stream, {
264
+ metadata: track ?? {
265
+ title: streamInfo.metadata?.title ?? "",
266
+ duration: streamInfo.metadata?.duration ?? 0,
267
+ source: streamInfo.metadata?.source ?? "",
268
+ requestedBy: streamInfo.metadata?.requestedBy ?? "",
269
+ thumbnail: streamInfo.metadata?.thumbnail ?? "",
270
+ url: streamInfo.metadata?.url ?? "",
271
+ id: streamInfo.metadata?.id ?? "",
272
+ },
273
+ inputType,
274
+ inlineVolume: true,
275
+ });
276
+ return resource;
277
+ }
204
278
  /**
205
279
  * Start playing a specific track immediately, replacing the current resource.
206
280
  */
@@ -209,70 +283,17 @@ class Player extends events_1.EventEmitter {
209
283
  let streamInfo = await this.extensionsProvideStream(track);
210
284
  let plugin;
211
285
  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" && typeof p.getStream !== "function") {
228
- continue;
229
- }
230
- try {
231
- streamInfo = await (0, timeout_1.withTimeout)(p.getStream(track), this.options.extractorTimeout ?? 15000, `getStream timed out for plugin ${p.name}`);
232
- if (streamInfo?.stream) {
233
- this.debug(`[Player] getStream succeeded with plugin ${p.name} for track: ${track.title}`);
234
- break;
235
- }
236
- streamInfo = await (0, timeout_1.withTimeout)(p.getFallback(track), this.options.extractorTimeout ?? 15000, `getFallback timed out for plugin ${p.name}`);
237
- if (!streamInfo?.stream)
238
- continue;
239
- break;
240
- }
241
- catch (fallbackError) {
242
- this.debug(`[Player] getFallback failed with plugin ${p.name}:`, fallbackError);
243
- }
244
- }
245
- if (!streamInfo?.stream) {
246
- throw new Error(`All getFallback attempts failed for track: ${track.title}`);
247
- }
286
+ streamInfo = await this.getStreamFromPlugin(track);
287
+ if (!streamInfo) {
288
+ throw new Error(`No stream available for track: ${track.title}`);
248
289
  }
249
290
  }
250
291
  else {
251
292
  this.debug(`[Player] Using extension-provided stream for track: ${track.title}`);
252
293
  }
253
- if (plugin) {
254
- this.debug(streamInfo);
255
- }
256
294
  // Kiểm tra nếu có stream thực sự để tạo AudioResource
257
295
  if (streamInfo && streamInfo.stream) {
258
- function mapToStreamType(type) {
259
- switch (type) {
260
- case "webm/opus":
261
- return voice_1.StreamType.WebmOpus;
262
- case "ogg/opus":
263
- return voice_1.StreamType.OggOpus;
264
- case "arbitrary":
265
- default:
266
- return voice_1.StreamType.Arbitrary;
267
- }
268
- }
269
- const stream = streamInfo.stream;
270
- const inputType = mapToStreamType(streamInfo.type);
271
- this.currentResource = (0, voice_1.createAudioResource)(stream, {
272
- metadata: track,
273
- inputType,
274
- inlineVolume: true,
275
- });
296
+ this.currentResource = await this.Audioresource(streamInfo, track);
276
297
  // Apply initial volume using the resource's VolumeTransformer
277
298
  if (this.volumeInterval) {
278
299
  clearInterval(this.volumeInterval);
@@ -306,7 +327,7 @@ class Player extends events_1.EventEmitter {
306
327
  if (this.leaveTimeout) {
307
328
  clearTimeout(this.leaveTimeout);
308
329
  this.leaveTimeout = null;
309
- this.debug(`[Player] Cleared leave timeout`);
330
+ this.debug(`[Player] Cleared leave timeoutMs`);
310
331
  }
311
332
  }
312
333
  debug(message, ...optionalParams) {
@@ -325,18 +346,12 @@ class Player extends events_1.EventEmitter {
325
346
  this.volumeInterval = null;
326
347
  this.skipLoop = false;
327
348
  this.extensions = [];
328
- // Cache for plugin matching to improve performance
329
- this.pluginCache = new Map();
330
- this.PLUGIN_CACHE_TTL = 5 * 60 * 1000; // 5 minutes
331
- this.pluginCacheTimestamps = new Map();
332
349
  // Cache for search results to avoid duplicate calls
333
350
  this.searchCache = new Map();
334
351
  this.SEARCH_CACHE_TTL = 2 * 60 * 1000; // 2 minutes
335
352
  this.searchCacheTimestamps = new Map();
336
353
  // TTS support
337
354
  this.ttsPlayer = null;
338
- this.ttsQueue = [];
339
- this.ttsActive = false;
340
355
  this.debug(`[Player] Constructor called for guildId: ${guildId}`);
341
356
  this.guildId = guildId;
342
357
  this.queue = new Queue_1.Queue();
@@ -563,60 +578,91 @@ class Player extends events_1.EventEmitter {
563
578
  throw new Error(`No plugin found to handle: ${query}`);
564
579
  }
565
580
  /**
566
- * Play a track or search query
581
+ * Play a track, search query, search result, or play from queue
567
582
  *
568
- * @param {string | Track} query - Track URL, search query, or Track object
583
+ * @param {string | Track | SearchResult | null} query - Track URL, search query, Track object, SearchResult, or null for play
569
584
  * @param {string} requestedBy - User ID who requested the track
570
585
  * @returns {Promise<boolean>} True if playback started successfully
571
586
  * @example
572
- * await player.play("Never Gonna Give You Up", userId);
573
- * await player.play("https://youtube.com/watch?v=dQw4w9WgXcQ", userId);
574
- * await player.play("tts: Hello everyone!", userId);
587
+ * await player.play("Never Gonna Give You Up", userId); // Search query
588
+ * await player.play("https://youtube.com/watch?v=dQw4w9WgXcQ", userId); // Direct URL
589
+ * await player.play("tts: Hello everyone!", userId); // Text-to-Speech
590
+ * await player.play(trackObject, userId); // Track object
591
+ * await player.play(searchResult, userId); // SearchResult object
592
+ * await player.play(null); // play from queue
575
593
  */
576
594
  async play(query, requestedBy) {
577
- this.debug(`[Player] Play called with query: ${typeof query === "string" ? query : query?.title}`);
595
+ const debugInfo = query === null ? "null"
596
+ : typeof query === "string" ? query
597
+ : "tracks" in query ? `${query.tracks.length} tracks`
598
+ : query.title || "unknown";
599
+ this.debug(`[Player] Play called with query: ${debugInfo}`);
578
600
  this.clearLeaveTimeout();
579
601
  let tracksToAdd = [];
580
602
  let isPlaylist = false;
581
- let effectiveRequest = { query, requestedBy };
603
+ let effectiveRequest = { query: query, requestedBy };
582
604
  let hookResponse = {};
583
605
  try {
584
- const hookOutcome = await this.runBeforePlayHooks(effectiveRequest);
585
- effectiveRequest = hookOutcome.request;
586
- hookResponse = hookOutcome.response;
587
- if (effectiveRequest.requestedBy === undefined) {
588
- effectiveRequest.requestedBy = requestedBy;
589
- }
590
- const hookTracks = Array.isArray(hookResponse.tracks) ? hookResponse.tracks : undefined;
591
- if (hookResponse.handled && (!hookTracks || hookTracks.length === 0)) {
592
- const handledPayload = {
593
- success: hookResponse.success ?? true,
594
- query: effectiveRequest.query,
595
- requestedBy: effectiveRequest.requestedBy,
596
- tracks: [],
597
- isPlaylist: hookResponse.isPlaylist ?? false,
598
- error: hookResponse.error,
599
- };
600
- await this.runAfterPlayHooks(handledPayload);
601
- if (hookResponse.error) {
602
- this.emit("playerError", hookResponse.error);
606
+ // Handle null query - play from queue
607
+ if (query === null) {
608
+ this.debug(`[Player] Play from queue requested`);
609
+ if (this.queue.isEmpty) {
610
+ this.debug(`[Player] Queue is empty, nothing to play`);
611
+ return false;
603
612
  }
604
- return hookResponse.success ?? true;
605
- }
606
- if (hookTracks && hookTracks.length > 0) {
607
- tracksToAdd = hookTracks;
608
- isPlaylist = hookResponse.isPlaylist ?? hookTracks.length > 1;
613
+ if (!this.isPlaying) {
614
+ return await this.playNext();
615
+ }
616
+ return true;
609
617
  }
610
- else if (typeof effectiveRequest.query === "string") {
611
- const searchResult = await this.search(effectiveRequest.query, effectiveRequest.requestedBy || "Unknown");
612
- tracksToAdd = searchResult.tracks;
613
- if (searchResult.playlist) {
614
- isPlaylist = true;
615
- this.debug(`[Player] Added playlist: ${searchResult.playlist.name} (${tracksToAdd.length} tracks)`);
618
+ // Handle SearchResult
619
+ if (query && typeof query === "object" && "tracks" in query && Array.isArray(query.tracks)) {
620
+ this.debug(`[Player] Playing SearchResult with ${query.tracks.length} tracks`);
621
+ tracksToAdd = query.tracks;
622
+ isPlaylist = !!query.playlist || query.tracks.length > 1;
623
+ if (query.playlist) {
624
+ this.debug(`[Player] Added playlist: ${query.playlist.name} (${tracksToAdd.length} tracks)`);
616
625
  }
617
626
  }
618
- else if (effectiveRequest.query) {
619
- tracksToAdd = [effectiveRequest.query];
627
+ else {
628
+ // Handle other types (string, Track)
629
+ const hookOutcome = await this.runBeforePlayHooks(effectiveRequest);
630
+ effectiveRequest = hookOutcome.request;
631
+ hookResponse = hookOutcome.response;
632
+ if (effectiveRequest.requestedBy === undefined) {
633
+ effectiveRequest.requestedBy = requestedBy;
634
+ }
635
+ const hookTracks = Array.isArray(hookResponse.tracks) ? hookResponse.tracks : undefined;
636
+ if (hookResponse.handled && (!hookTracks || hookTracks.length === 0)) {
637
+ const handledPayload = {
638
+ success: hookResponse.success ?? true,
639
+ query: effectiveRequest.query,
640
+ requestedBy: effectiveRequest.requestedBy,
641
+ tracks: [],
642
+ isPlaylist: hookResponse.isPlaylist ?? false,
643
+ error: hookResponse.error,
644
+ };
645
+ await this.runAfterPlayHooks(handledPayload);
646
+ if (hookResponse.error) {
647
+ this.emit("playerError", hookResponse.error);
648
+ }
649
+ return hookResponse.success ?? true;
650
+ }
651
+ if (hookTracks && hookTracks.length > 0) {
652
+ tracksToAdd = hookTracks;
653
+ isPlaylist = hookResponse.isPlaylist ?? hookTracks.length > 1;
654
+ }
655
+ else if (typeof effectiveRequest.query === "string") {
656
+ const searchResult = await this.search(effectiveRequest.query, effectiveRequest.requestedBy || "Unknown");
657
+ tracksToAdd = searchResult.tracks;
658
+ if (searchResult.playlist) {
659
+ isPlaylist = true;
660
+ this.debug(`[Player] Added playlist: ${searchResult.playlist.name} (${tracksToAdd.length} tracks)`);
661
+ }
662
+ }
663
+ else if (effectiveRequest.query) {
664
+ tracksToAdd = [effectiveRequest.query];
665
+ }
620
666
  }
621
667
  if (tracksToAdd.length === 0) {
622
668
  this.debug(`[Player] No tracks found for play`);
@@ -690,49 +736,39 @@ class Player extends events_1.EventEmitter {
690
736
  * await player.interruptWithTTSTrack(track);
691
737
  */
692
738
  async interruptWithTTSTrack(track) {
693
- this.ttsQueue.push(track);
694
- if (!this.ttsActive) {
695
- void this.playNextTTS();
696
- }
697
- }
698
- /**
699
- * Play queued TTS items sequentially
700
- *
701
- * @returns {Promise<void>}
702
- * @example
703
- * await player.playNextTTS();
704
- */
705
- async playNextTTS() {
706
- const next = this.ttsQueue.shift();
707
- if (!next)
708
- return;
709
- this.ttsActive = true;
739
+ const wasPlaying = this.audioPlayer.state.status === voice_1.AudioPlayerStatus.Playing ||
740
+ this.audioPlayer.state.status === voice_1.AudioPlayerStatus.Buffering;
710
741
  try {
711
742
  if (!this.connection)
712
743
  throw new Error("No voice connection for TTS");
713
744
  const ttsPlayer = this.ensureTTSPlayer();
714
745
  // Build resource from plugin stream
715
- const resource = await this.resourceFromTrack(next);
746
+ const streamInfo = await this.getStreamFromPlugin(track);
747
+ if (!streamInfo) {
748
+ throw new Error("No stream available for track: ${track.title}");
749
+ }
750
+ const resource = await this.Audioresource(streamInfo, track);
751
+ if (!resource) {
752
+ throw new Error("No resource available for track: ${track.title}");
753
+ }
716
754
  if (resource.volume) {
717
755
  resource.volume.setVolume((this.options?.tts?.volume ?? this?.volume ?? 100) / 100);
718
756
  }
719
- const wasPlaying = this.audioPlayer.state.status === voice_1.AudioPlayerStatus.Playing ||
720
- this.audioPlayer.state.status === voice_1.AudioPlayerStatus.Buffering;
721
757
  // Pause current music if any
722
758
  try {
723
- this.audioPlayer.pause(true);
759
+ this.pause();
724
760
  }
725
761
  catch { }
726
762
  // Swap subscription and play TTS
727
763
  this.connection.subscribe(ttsPlayer);
728
- this.emit("ttsStart", { track: next });
764
+ this.emit("ttsStart", { track });
729
765
  ttsPlayer.play(resource);
730
766
  // Wait until TTS starts then finishes
731
767
  await (0, voice_1.entersState)(ttsPlayer, voice_1.AudioPlayerStatus.Playing, 5000).catch(() => null);
732
- // Derive timeout from resource/track duration when available, with a sensible cap
768
+ // Derive timeoutMs from resource/track duration when available, with a sensible cap
733
769
  const md = resource?.metadata ?? {};
734
770
  const declared = typeof md.duration === "number" ? md.duration
735
- : typeof next?.duration === "number" ? next.duration
771
+ : typeof track?.duration === "number" ? track.duration
736
772
  : undefined;
737
773
  const declaredMs = declared ?
738
774
  declared > 1000 ?
@@ -744,97 +780,20 @@ class Player extends events_1.EventEmitter {
744
780
  await (0, voice_1.entersState)(ttsPlayer, voice_1.AudioPlayerStatus.Idle, idleTimeout).catch(() => null);
745
781
  // Swap back and resume if needed
746
782
  this.connection.subscribe(this.audioPlayer);
747
- if (wasPlaying) {
748
- try {
749
- this.audioPlayer.unpause();
750
- }
751
- catch { }
752
- }
753
- this.emit("ttsEnd");
754
783
  }
755
784
  catch (err) {
756
785
  this.debug("[TTS] error while playing:", err);
757
786
  this.emit("playerError", err);
758
787
  }
759
788
  finally {
760
- this.ttsActive = false;
761
- if (this.ttsQueue.length > 0) {
762
- await this.playNextTTS();
763
- }
764
- }
765
- }
766
- /**
767
- * Get cached plugin or find and cache a new one
768
- * @param track The track to find plugin for
769
- * @returns The matching plugin or null if not found
770
- */
771
- getCachedPlugin(track) {
772
- const cacheKey = `${track.source}:${track.url}`;
773
- const now = Date.now();
774
- // Check if cache is still valid
775
- const cachedTimestamp = this.pluginCacheTimestamps.get(cacheKey);
776
- if (cachedTimestamp && now - cachedTimestamp < this.PLUGIN_CACHE_TTL) {
777
- const cachedPlugin = this.pluginCache.get(cacheKey);
778
- if (cachedPlugin) {
779
- this.debug(`[PluginCache] Using cached plugin for ${track.source}: ${cachedPlugin.name}`);
780
- return cachedPlugin;
781
- }
782
- }
783
- // Find new plugin and cache it
784
- this.debug(`[PluginCache] Finding plugin for track: ${track.title} (${track.source})`);
785
- const plugin = this.pluginManager.findPlugin(track.url) || this.pluginManager.get(track.source);
786
- if (plugin) {
787
- this.pluginCache.set(cacheKey, plugin);
788
- this.pluginCacheTimestamps.set(cacheKey, now);
789
- this.debug(`[PluginCache] Cached plugin: ${plugin.name} for ${track.source}`);
790
- return plugin;
791
- }
792
- return null;
793
- }
794
- /**
795
- * Clear expired cache entries
796
- */
797
- clearExpiredCache() {
798
- const now = Date.now();
799
- for (const [key, timestamp] of this.pluginCacheTimestamps.entries()) {
800
- if (now - timestamp >= this.PLUGIN_CACHE_TTL) {
801
- this.pluginCache.delete(key);
802
- this.pluginCacheTimestamps.delete(key);
803
- this.debug(`[PluginCache] Cleared expired cache entry: ${key}`);
804
- }
805
- }
806
- }
807
- /**
808
- * Clear all plugin cache entries
809
- * @example
810
- * player.clearPluginCache();
811
- */
812
- clearPluginCache() {
813
- const cacheSize = this.pluginCache.size;
814
- this.pluginCache.clear();
815
- this.pluginCacheTimestamps.clear();
816
- this.debug(`[PluginCache] Cleared all ${cacheSize} cache entries`);
817
- }
818
- /**
819
- * Get plugin cache statistics
820
- * @returns Cache statistics
821
- * @example
822
- * const stats = player.getPluginCacheStats();
823
- * console.log(`Cache size: ${stats.size}, Hit rate: ${stats.hitRate}%`);
824
- */
825
- getPluginCacheStats() {
826
- const now = Date.now();
827
- let expiredEntries = 0;
828
- for (const timestamp of this.pluginCacheTimestamps.values()) {
829
- if (now - timestamp >= this.PLUGIN_CACHE_TTL) {
830
- expiredEntries++;
789
+ if (wasPlaying) {
790
+ try {
791
+ this.resume();
792
+ }
793
+ catch { }
831
794
  }
795
+ this.emit("ttsEnd");
832
796
  }
833
- return {
834
- size: this.pluginCache.size,
835
- hitRate: 0, // Would need to track hits/misses to calculate this
836
- expiredEntries,
837
- };
838
797
  }
839
798
  /**
840
799
  * Get cached search result or null if not found/expired
@@ -890,31 +849,6 @@ class Player extends events_1.EventEmitter {
890
849
  this.searchCacheTimestamps.clear();
891
850
  this.debug(`[SearchCache] Cleared all ${cacheSize} search cache entries`);
892
851
  }
893
- /**
894
- * Get search cache statistics
895
- * @returns Search cache statistics
896
- * @example
897
- * const stats = player.getSearchCacheStats();
898
- * console.log(`Search cache size: ${stats.size}, Expired: ${stats.expiredEntries}`);
899
- */
900
- getSearchCacheStats() {
901
- const now = Date.now();
902
- let expiredEntries = 0;
903
- const queries = [];
904
- for (const [key, timestamp] of this.searchCacheTimestamps.entries()) {
905
- if (now - timestamp >= this.SEARCH_CACHE_TTL) {
906
- expiredEntries++;
907
- }
908
- else {
909
- queries.push(key);
910
- }
911
- }
912
- return {
913
- size: this.searchCache.size,
914
- expiredEntries,
915
- queries,
916
- };
917
- }
918
852
  /**
919
853
  * Debug method to check for duplicate search calls
920
854
  * @param query The search query to check
@@ -939,114 +873,6 @@ class Player extends events_1.EventEmitter {
939
873
  ttsFiltered: allPlugins.length > plugins.length,
940
874
  };
941
875
  }
942
- /** Build AudioResource for a given track using the plugin pipeline */
943
- async resourceFromTrack(track) {
944
- this.debug(`[ResourceFromTrack] Starting resource creation for track: ${track.title} (${track.source})`);
945
- // Clear expired cache entries periodically
946
- if (Math.random() < 0.1) {
947
- // 10% chance to clean cache
948
- this.clearExpiredCache();
949
- }
950
- // Resolve plugin using cache
951
- const plugin = this.getCachedPlugin(track);
952
- if (!plugin) {
953
- this.debug(`[ResourceFromTrack] No plugin found for track: ${track.title} (${track.source})`);
954
- throw new Error(`No plugin found for track: ${track.title}`);
955
- }
956
- this.debug(`[ResourceFromTrack] Using plugin: ${plugin.name} for track: ${track.title}`);
957
- let streamInfo = null;
958
- const timeoutMs = this.options.extractorTimeout ?? 15000;
959
- try {
960
- this.debug(`[ResourceFromTrack] Attempting getStream with ${plugin.name}, timeout: ${timeoutMs}ms`);
961
- const startTime = Date.now();
962
- streamInfo = await (0, timeout_1.withTimeout)(plugin.getStream(track), timeoutMs, "getStream timed out");
963
- const duration = Date.now() - startTime;
964
- this.debug(`[ResourceFromTrack] getStream successful with ${plugin.name} in ${duration}ms`);
965
- if (!streamInfo?.stream) {
966
- this.debug(`[ResourceFromTrack] getStream returned no stream from ${plugin.name}`);
967
- throw new Error(`No stream returned from ${plugin.name}`);
968
- }
969
- }
970
- catch (streamError) {
971
- const errorMessage = streamError instanceof Error ? streamError.message : String(streamError);
972
- this.debug(`[ResourceFromTrack] getStream failed with ${plugin.name}: ${errorMessage}`);
973
- // Log more details for debugging
974
- if (streamError instanceof Error && streamError.stack) {
975
- this.debug(`[ResourceFromTrack] getStream error stack:`, streamError.stack);
976
- }
977
- // try fallbacks
978
- this.debug(`[ResourceFromTrack] Attempting fallback plugins for track: ${track.title}`);
979
- const allplugs = this.pluginManager.getAll();
980
- let fallbackAttempts = 0;
981
- for (const p of allplugs) {
982
- if (typeof p.getFallback !== "function" && typeof p.getStream !== "function") {
983
- this.debug(`[ResourceFromTrack] Skipping plugin ${p.name} - no getFallback or getStream method`);
984
- continue;
985
- }
986
- fallbackAttempts++;
987
- this.debug(`[ResourceFromTrack] Trying fallback plugin ${p.name} (attempt ${fallbackAttempts})`);
988
- try {
989
- // Try getStream first
990
- const startTime = Date.now();
991
- streamInfo = await (0, timeout_1.withTimeout)(p.getStream(track), timeoutMs, "getStream timed out");
992
- const duration = Date.now() - startTime;
993
- if (streamInfo?.stream) {
994
- this.debug(`[ResourceFromTrack] Fallback getStream successful with ${p.name} in ${duration}ms`);
995
- break;
996
- }
997
- // Try getFallback if getStream didn't work
998
- this.debug(`[ResourceFromTrack] Trying getFallback with ${p.name}`);
999
- const fallbackStartTime = Date.now();
1000
- streamInfo = await (0, timeout_1.withTimeout)(p.getFallback(track), timeoutMs, `getFallback timed out for plugin ${p.name}`);
1001
- const fallbackDuration = Date.now() - fallbackStartTime;
1002
- if (streamInfo?.stream) {
1003
- this.debug(`[ResourceFromTrack] Fallback getFallback successful with ${p.name} in ${fallbackDuration}ms`);
1004
- break;
1005
- }
1006
- this.debug(`[ResourceFromTrack] Fallback plugin ${p.name} returned no stream`);
1007
- }
1008
- catch (fallbackError) {
1009
- const errorMessage = fallbackError instanceof Error ? fallbackError.message : String(fallbackError);
1010
- this.debug(`[ResourceFromTrack] Fallback plugin ${p.name} failed: ${errorMessage}`);
1011
- // Log more details for debugging
1012
- if (fallbackError instanceof Error && fallbackError.stack) {
1013
- this.debug(`[ResourceFromTrack] Fallback error stack:`, fallbackError.stack);
1014
- }
1015
- }
1016
- }
1017
- if (!streamInfo?.stream) {
1018
- this.debug(`[ResourceFromTrack] All ${fallbackAttempts} fallback attempts failed for track: ${track.title}`);
1019
- throw new Error(`All getFallback attempts failed for track: ${track.title}`);
1020
- }
1021
- }
1022
- this.debug(`[ResourceFromTrack] Stream obtained, type: ${streamInfo.type}, metadata keys: ${Object.keys(streamInfo.metadata || {}).join(", ")}`);
1023
- const mapToStreamType = (type) => {
1024
- switch (type) {
1025
- case "webm/opus":
1026
- return voice_1.StreamType.WebmOpus;
1027
- case "ogg/opus":
1028
- return voice_1.StreamType.OggOpus;
1029
- case "arbitrary":
1030
- default:
1031
- return voice_1.StreamType.Arbitrary;
1032
- }
1033
- };
1034
- const inputType = mapToStreamType(streamInfo.type);
1035
- this.debug(`[ResourceFromTrack] Creating AudioResource with inputType: ${inputType}`);
1036
- // Merge metadata safely
1037
- const mergedMetadata = {
1038
- ...track,
1039
- ...(streamInfo.metadata || {}),
1040
- };
1041
- const audioResource = (0, voice_1.createAudioResource)(streamInfo.stream, {
1042
- // Prefer plugin-provided metadata (e.g., precise duration), fallback to track fields
1043
- metadata: mergedMetadata,
1044
- inputType,
1045
- inlineVolume: true,
1046
- });
1047
- this.debug(`[ResourceFromTrack] AudioResource created successfully for track: ${track.title}`);
1048
- return audioResource;
1049
- }
1050
876
  async generateWillNext() {
1051
877
  const lastTrack = this.queue.previousTracks[this.queue.previousTracks.length - 1] ?? this.queue.currentTrack;
1052
878
  if (!lastTrack)
@@ -1168,20 +994,46 @@ class Player extends events_1.EventEmitter {
1168
994
  return result;
1169
995
  }
1170
996
  /**
1171
- * Skip to the next track
997
+ * Skip to the next track or skip to a specific index
1172
998
  *
999
+ * @param {number} index - Optional index to skip to (0 = next track)
1173
1000
  * @returns {boolean} True if skipped successfully
1174
1001
  * @example
1175
- * const skipped = player.skip();
1002
+ * const skipped = player.skip(); // Skip to next track
1003
+ * const skippedToIndex = player.skip(2); // Skip to track at index 2
1176
1004
  * console.log(`Skipped: ${skipped}`);
1177
1005
  */
1178
- skip() {
1179
- this.debug(`[Player] skip called`);
1180
- if (this.isPlaying || this.isPaused) {
1181
- this.skipLoop = true;
1182
- return this.audioPlayer.stop();
1006
+ skip(index) {
1007
+ this.debug(`[Player] skip called with index: ${index}`);
1008
+ try {
1009
+ if (typeof index === "number" && index >= 0) {
1010
+ // Skip to specific index
1011
+ const targetTrack = this.queue.getTrack(index);
1012
+ if (!targetTrack) {
1013
+ this.debug(`[Player] No track found at index ${index}`);
1014
+ return false;
1015
+ }
1016
+ // Remove tracks from 0 to index-1
1017
+ for (let i = 0; i < index; i++) {
1018
+ this.queue.remove(0);
1019
+ }
1020
+ this.debug(`[Player] Skipped to track at index ${index}: ${targetTrack.title}`);
1021
+ if (this.isPlaying || this.isPaused) {
1022
+ this.skipLoop = true;
1023
+ return this.audioPlayer.stop();
1024
+ }
1025
+ return true;
1026
+ }
1027
+ if (this.isPlaying || this.isPaused) {
1028
+ this.skipLoop = true;
1029
+ return this.audioPlayer.stop();
1030
+ }
1031
+ return true;
1032
+ }
1033
+ catch (error) {
1034
+ this.debug(`[Player] skip error:`, error);
1035
+ return false;
1183
1036
  }
1184
- return !!this.playNext();
1185
1037
  }
1186
1038
  /**
1187
1039
  * Go back to the previous track in history and play it.
@@ -1202,15 +1054,35 @@ class Player extends events_1.EventEmitter {
1202
1054
  return this.startTrack(track);
1203
1055
  }
1204
1056
  /**
1205
- * Loop the current track
1057
+ * Loop the current track or queue
1206
1058
  *
1207
- * @param {LoopMode} mode - The loop mode to set
1059
+ * @param {LoopMode | number} mode - The loop mode to set ("off", "track", "queue") or number (0=off, 1=track, 2=queue)
1208
1060
  * @returns {LoopMode} The loop mode
1209
1061
  * @example
1210
- * const loopMode = player.loop("track");
1062
+ * const loopMode = player.loop("track"); // Loop current track
1063
+ * const loopQueue = player.loop("queue"); // Loop entire queue
1064
+ * const loopTrack = player.loop(1); // Loop current track (same as "track")
1065
+ * const loopQueueNum = player.loop(2); // Loop entire queue (same as "queue")
1066
+ * const noLoop = player.loop("off"); // No loop
1067
+ * const noLoopNum = player.loop(0); // No loop (same as "off")
1211
1068
  * console.log(`Loop mode: ${loopMode}`);
1212
1069
  */
1213
1070
  loop(mode) {
1071
+ this.debug(`[Player] loop called with mode: ${mode}`);
1072
+ if (typeof mode === "number") {
1073
+ // Number mode: convert to text mode
1074
+ switch (mode) {
1075
+ case 0:
1076
+ return this.queue.loop("off");
1077
+ case 1:
1078
+ return this.queue.loop("track");
1079
+ case 2:
1080
+ return this.queue.loop("queue");
1081
+ default:
1082
+ this.debug(`[Player] Invalid loop number: ${mode}, using "off"`);
1083
+ return this.queue.loop("off");
1084
+ }
1085
+ }
1214
1086
  return this.queue.loop(mode);
1215
1087
  }
1216
1088
  /**
@@ -1429,7 +1301,7 @@ class Player extends events_1.EventEmitter {
1429
1301
  }
1430
1302
  if (this.options.leaveOnEmpty && this.options.leaveTimeout) {
1431
1303
  this.leaveTimeout = setTimeout(() => {
1432
- this.debug(`[Player] Leaving voice channel after timeout`);
1304
+ this.debug(`[Player] Leaving voice channel after timeoutMs`);
1433
1305
  this.destroy();
1434
1306
  }, this.options.leaveTimeout);
1435
1307
  }
@@ -1550,6 +1422,111 @@ class Player extends events_1.EventEmitter {
1550
1422
  get relatedTracks() {
1551
1423
  return this.queue.relatedTracks();
1552
1424
  }
1425
+ /**
1426
+ * Save a track's stream to a file and return a Readable stream
1427
+ *
1428
+ * @param {Track} track - The track to save
1429
+ * @param {SaveOptions | string} options - Save options or filename string (for backward compatibility)
1430
+ * @returns {Promise<Readable>} A Readable stream containing the audio data
1431
+ * @example
1432
+ * // Save current track to file
1433
+ * const track = player.currentTrack;
1434
+ * if (track) {
1435
+ * const stream = await player.save(track);
1436
+ *
1437
+ * // Use fs to write the stream to file
1438
+ * const fs = require('fs');
1439
+ * const writeStream = fs.createWriteStream('saved-song.mp3');
1440
+ * stream.pipe(writeStream);
1441
+ *
1442
+ * writeStream.on('finish', () => {
1443
+ * console.log('File saved successfully!');
1444
+ * });
1445
+ * }
1446
+ *
1447
+ * // Save any track by URL
1448
+ * const searchResult = await player.search("Never Gonna Give You Up", userId);
1449
+ * if (searchResult.tracks.length > 0) {
1450
+ * const stream = await player.save(searchResult.tracks[0]);
1451
+ * // Handle the stream...
1452
+ * }
1453
+ *
1454
+ * // Backward compatibility - filename as string
1455
+ * const stream = await player.save(track, "my-song.mp3");
1456
+ */
1457
+ async save(track, options) {
1458
+ this.debug(`[Player] save called for track: ${track.title}`);
1459
+ // Parse options - support both SaveOptions object and filename string (backward compatibility)
1460
+ let saveOptions = {};
1461
+ if (typeof options === "string") {
1462
+ saveOptions = { filename: options };
1463
+ }
1464
+ else if (options) {
1465
+ saveOptions = options;
1466
+ }
1467
+ // Use timeout from options or fallback to player's extractorTimeout
1468
+ const timeout = saveOptions.timeout ?? this.options.extractorTimeout ?? 15000;
1469
+ try {
1470
+ // Try extensions first
1471
+ let streamInfo = await this.extensionsProvideStream(track);
1472
+ let plugin;
1473
+ if (!streamInfo) {
1474
+ plugin = this.pluginManager.findPlugin(track.url) || this.pluginManager.get(track.source);
1475
+ if (!plugin) {
1476
+ this.debug(`[Player] No plugin found for track: ${track.title}`);
1477
+ throw new Error(`No plugin found for track: ${track.title}`);
1478
+ }
1479
+ this.debug(`[Player] Getting save stream for track: ${track.title}`);
1480
+ this.debug(`[Player] Using save plugin: ${plugin.name}`);
1481
+ try {
1482
+ streamInfo = await (0, timeout_1.withTimeout)(plugin.getStream(track), timeout, "getSaveStream timed out");
1483
+ }
1484
+ catch (streamError) {
1485
+ this.debug(`[Player] getSaveStream failed, trying getFallback:`, streamError);
1486
+ const allplugs = this.pluginManager.getAll();
1487
+ for (const p of allplugs) {
1488
+ if (typeof p.getFallback !== "function" && typeof p.getStream !== "function") {
1489
+ continue;
1490
+ }
1491
+ try {
1492
+ streamInfo = await (0, timeout_1.withTimeout)(p.getStream(track), timeout, `getSaveStream timed out for plugin ${p.name}`);
1493
+ if (streamInfo?.stream) {
1494
+ this.debug(`[Player] getSaveStream succeeded with plugin ${p.name} for track: ${track.title}`);
1495
+ break;
1496
+ }
1497
+ streamInfo = await (0, timeout_1.withTimeout)(p.getFallback(track), timeout, `getSaveFallback timed out for plugin ${p.name}`);
1498
+ if (!streamInfo?.stream)
1499
+ continue;
1500
+ break;
1501
+ }
1502
+ catch (fallbackError) {
1503
+ this.debug(`[Player] getSaveFallback failed with plugin ${p.name}:`, fallbackError);
1504
+ }
1505
+ }
1506
+ if (!streamInfo?.stream) {
1507
+ throw new Error(`All getSaveFallback attempts failed for track: ${track.title}`);
1508
+ }
1509
+ }
1510
+ }
1511
+ else {
1512
+ this.debug(`[Player] Using extension-provided save stream for track: ${track.title}`);
1513
+ }
1514
+ if (!streamInfo || !streamInfo.stream) {
1515
+ throw new Error(`No save stream available for track: ${track.title}`);
1516
+ }
1517
+ this.debug(`[Player] Save stream obtained for track: ${track.title}`);
1518
+ if (saveOptions.filename) {
1519
+ this.debug(`[Player] Save options - filename: ${saveOptions.filename}, quality: ${saveOptions.quality || "default"}`);
1520
+ }
1521
+ // Return the stream directly - caller can pipe it to fs.createWriteStream()
1522
+ return streamInfo.stream;
1523
+ }
1524
+ catch (error) {
1525
+ this.debug(`[Player] save error:`, error);
1526
+ this.emit("playerError", error, track);
1527
+ throw error;
1528
+ }
1529
+ }
1553
1530
  }
1554
1531
  exports.Player = Player;
1555
1532
  //# sourceMappingURL=Player.js.map