ziplayer 0.1.4 → 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.
@@ -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,68 +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
- this.debug(streamInfo);
250
290
  }
251
291
  else {
252
292
  this.debug(`[Player] Using extension-provided stream for track: ${track.title}`);
253
293
  }
254
294
  // Kiểm tra nếu có stream thực sự để tạo AudioResource
255
295
  if (streamInfo && streamInfo.stream) {
256
- function mapToStreamType(type) {
257
- switch (type) {
258
- case "webm/opus":
259
- return voice_1.StreamType.WebmOpus;
260
- case "ogg/opus":
261
- return voice_1.StreamType.OggOpus;
262
- case "arbitrary":
263
- default:
264
- return voice_1.StreamType.Arbitrary;
265
- }
266
- }
267
- const stream = streamInfo.stream;
268
- const inputType = mapToStreamType(streamInfo.type);
269
- this.currentResource = (0, voice_1.createAudioResource)(stream, {
270
- metadata: track,
271
- inputType,
272
- inlineVolume: true,
273
- });
296
+ this.currentResource = await this.Audioresource(streamInfo, track);
274
297
  // Apply initial volume using the resource's VolumeTransformer
275
298
  if (this.volumeInterval) {
276
299
  clearInterval(this.volumeInterval);
@@ -304,7 +327,7 @@ class Player extends events_1.EventEmitter {
304
327
  if (this.leaveTimeout) {
305
328
  clearTimeout(this.leaveTimeout);
306
329
  this.leaveTimeout = null;
307
- this.debug(`[Player] Cleared leave timeout`);
330
+ this.debug(`[Player] Cleared leave timeoutMs`);
308
331
  }
309
332
  }
310
333
  debug(message, ...optionalParams) {
@@ -323,18 +346,12 @@ class Player extends events_1.EventEmitter {
323
346
  this.volumeInterval = null;
324
347
  this.skipLoop = false;
325
348
  this.extensions = [];
326
- // Cache for plugin matching to improve performance
327
- this.pluginCache = new Map();
328
- this.PLUGIN_CACHE_TTL = 5 * 60 * 1000; // 5 minutes
329
- this.pluginCacheTimestamps = new Map();
330
349
  // Cache for search results to avoid duplicate calls
331
350
  this.searchCache = new Map();
332
351
  this.SEARCH_CACHE_TTL = 2 * 60 * 1000; // 2 minutes
333
352
  this.searchCacheTimestamps = new Map();
334
353
  // TTS support
335
354
  this.ttsPlayer = null;
336
- this.ttsQueue = [];
337
- this.ttsActive = false;
338
355
  this.debug(`[Player] Constructor called for guildId: ${guildId}`);
339
356
  this.guildId = guildId;
340
357
  this.queue = new Queue_1.Queue();
@@ -719,49 +736,39 @@ class Player extends events_1.EventEmitter {
719
736
  * await player.interruptWithTTSTrack(track);
720
737
  */
721
738
  async interruptWithTTSTrack(track) {
722
- this.ttsQueue.push(track);
723
- if (!this.ttsActive) {
724
- void this.playNextTTS();
725
- }
726
- }
727
- /**
728
- * Play queued TTS items sequentially
729
- *
730
- * @returns {Promise<void>}
731
- * @example
732
- * await player.playNextTTS();
733
- */
734
- async playNextTTS() {
735
- const next = this.ttsQueue.shift();
736
- if (!next)
737
- return;
738
- this.ttsActive = true;
739
+ const wasPlaying = this.audioPlayer.state.status === voice_1.AudioPlayerStatus.Playing ||
740
+ this.audioPlayer.state.status === voice_1.AudioPlayerStatus.Buffering;
739
741
  try {
740
742
  if (!this.connection)
741
743
  throw new Error("No voice connection for TTS");
742
744
  const ttsPlayer = this.ensureTTSPlayer();
743
745
  // Build resource from plugin stream
744
- 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
+ }
745
754
  if (resource.volume) {
746
755
  resource.volume.setVolume((this.options?.tts?.volume ?? this?.volume ?? 100) / 100);
747
756
  }
748
- const wasPlaying = this.audioPlayer.state.status === voice_1.AudioPlayerStatus.Playing ||
749
- this.audioPlayer.state.status === voice_1.AudioPlayerStatus.Buffering;
750
757
  // Pause current music if any
751
758
  try {
752
- this.audioPlayer.pause(true);
759
+ this.pause();
753
760
  }
754
761
  catch { }
755
762
  // Swap subscription and play TTS
756
763
  this.connection.subscribe(ttsPlayer);
757
- this.emit("ttsStart", { track: next });
764
+ this.emit("ttsStart", { track });
758
765
  ttsPlayer.play(resource);
759
766
  // Wait until TTS starts then finishes
760
767
  await (0, voice_1.entersState)(ttsPlayer, voice_1.AudioPlayerStatus.Playing, 5000).catch(() => null);
761
- // 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
762
769
  const md = resource?.metadata ?? {};
763
770
  const declared = typeof md.duration === "number" ? md.duration
764
- : typeof next?.duration === "number" ? next.duration
771
+ : typeof track?.duration === "number" ? track.duration
765
772
  : undefined;
766
773
  const declaredMs = declared ?
767
774
  declared > 1000 ?
@@ -773,97 +780,20 @@ class Player extends events_1.EventEmitter {
773
780
  await (0, voice_1.entersState)(ttsPlayer, voice_1.AudioPlayerStatus.Idle, idleTimeout).catch(() => null);
774
781
  // Swap back and resume if needed
775
782
  this.connection.subscribe(this.audioPlayer);
776
- if (wasPlaying) {
777
- try {
778
- this.audioPlayer.unpause();
779
- }
780
- catch { }
781
- }
782
- this.emit("ttsEnd");
783
783
  }
784
784
  catch (err) {
785
785
  this.debug("[TTS] error while playing:", err);
786
786
  this.emit("playerError", err);
787
787
  }
788
788
  finally {
789
- this.ttsActive = false;
790
- if (this.ttsQueue.length > 0) {
791
- await this.playNextTTS();
792
- }
793
- }
794
- }
795
- /**
796
- * Get cached plugin or find and cache a new one
797
- * @param track The track to find plugin for
798
- * @returns The matching plugin or null if not found
799
- */
800
- getCachedPlugin(track) {
801
- const cacheKey = `${track.source}:${track.url}`;
802
- const now = Date.now();
803
- // Check if cache is still valid
804
- const cachedTimestamp = this.pluginCacheTimestamps.get(cacheKey);
805
- if (cachedTimestamp && now - cachedTimestamp < this.PLUGIN_CACHE_TTL) {
806
- const cachedPlugin = this.pluginCache.get(cacheKey);
807
- if (cachedPlugin) {
808
- this.debug(`[PluginCache] Using cached plugin for ${track.source}: ${cachedPlugin.name}`);
809
- return cachedPlugin;
810
- }
811
- }
812
- // Find new plugin and cache it
813
- this.debug(`[PluginCache] Finding plugin for track: ${track.title} (${track.source})`);
814
- const plugin = this.pluginManager.findPlugin(track.url) || this.pluginManager.get(track.source);
815
- if (plugin) {
816
- this.pluginCache.set(cacheKey, plugin);
817
- this.pluginCacheTimestamps.set(cacheKey, now);
818
- this.debug(`[PluginCache] Cached plugin: ${plugin.name} for ${track.source}`);
819
- return plugin;
820
- }
821
- return null;
822
- }
823
- /**
824
- * Clear expired cache entries
825
- */
826
- clearExpiredCache() {
827
- const now = Date.now();
828
- for (const [key, timestamp] of this.pluginCacheTimestamps.entries()) {
829
- if (now - timestamp >= this.PLUGIN_CACHE_TTL) {
830
- this.pluginCache.delete(key);
831
- this.pluginCacheTimestamps.delete(key);
832
- this.debug(`[PluginCache] Cleared expired cache entry: ${key}`);
833
- }
834
- }
835
- }
836
- /**
837
- * Clear all plugin cache entries
838
- * @example
839
- * player.clearPluginCache();
840
- */
841
- clearPluginCache() {
842
- const cacheSize = this.pluginCache.size;
843
- this.pluginCache.clear();
844
- this.pluginCacheTimestamps.clear();
845
- this.debug(`[PluginCache] Cleared all ${cacheSize} cache entries`);
846
- }
847
- /**
848
- * Get plugin cache statistics
849
- * @returns Cache statistics
850
- * @example
851
- * const stats = player.getPluginCacheStats();
852
- * console.log(`Cache size: ${stats.size}, Hit rate: ${stats.hitRate}%`);
853
- */
854
- getPluginCacheStats() {
855
- const now = Date.now();
856
- let expiredEntries = 0;
857
- for (const timestamp of this.pluginCacheTimestamps.values()) {
858
- if (now - timestamp >= this.PLUGIN_CACHE_TTL) {
859
- expiredEntries++;
789
+ if (wasPlaying) {
790
+ try {
791
+ this.resume();
792
+ }
793
+ catch { }
860
794
  }
795
+ this.emit("ttsEnd");
861
796
  }
862
- return {
863
- size: this.pluginCache.size,
864
- hitRate: 0, // Would need to track hits/misses to calculate this
865
- expiredEntries,
866
- };
867
797
  }
868
798
  /**
869
799
  * Get cached search result or null if not found/expired
@@ -919,31 +849,6 @@ class Player extends events_1.EventEmitter {
919
849
  this.searchCacheTimestamps.clear();
920
850
  this.debug(`[SearchCache] Cleared all ${cacheSize} search cache entries`);
921
851
  }
922
- /**
923
- * Get search cache statistics
924
- * @returns Search cache statistics
925
- * @example
926
- * const stats = player.getSearchCacheStats();
927
- * console.log(`Search cache size: ${stats.size}, Expired: ${stats.expiredEntries}`);
928
- */
929
- getSearchCacheStats() {
930
- const now = Date.now();
931
- let expiredEntries = 0;
932
- const queries = [];
933
- for (const [key, timestamp] of this.searchCacheTimestamps.entries()) {
934
- if (now - timestamp >= this.SEARCH_CACHE_TTL) {
935
- expiredEntries++;
936
- }
937
- else {
938
- queries.push(key);
939
- }
940
- }
941
- return {
942
- size: this.searchCache.size,
943
- expiredEntries,
944
- queries,
945
- };
946
- }
947
852
  /**
948
853
  * Debug method to check for duplicate search calls
949
854
  * @param query The search query to check
@@ -968,114 +873,6 @@ class Player extends events_1.EventEmitter {
968
873
  ttsFiltered: allPlugins.length > plugins.length,
969
874
  };
970
875
  }
971
- /** Build AudioResource for a given track using the plugin pipeline */
972
- async resourceFromTrack(track) {
973
- this.debug(`[ResourceFromTrack] Starting resource creation for track: ${track.title} (${track.source})`);
974
- // Clear expired cache entries periodically
975
- if (Math.random() < 0.1) {
976
- // 10% chance to clean cache
977
- this.clearExpiredCache();
978
- }
979
- // Resolve plugin using cache
980
- const plugin = this.getCachedPlugin(track);
981
- if (!plugin) {
982
- this.debug(`[ResourceFromTrack] No plugin found for track: ${track.title} (${track.source})`);
983
- throw new Error(`No plugin found for track: ${track.title}`);
984
- }
985
- this.debug(`[ResourceFromTrack] Using plugin: ${plugin.name} for track: ${track.title}`);
986
- let streamInfo = null;
987
- const timeoutMs = this.options.extractorTimeout ?? 15000;
988
- try {
989
- this.debug(`[ResourceFromTrack] Attempting getStream with ${plugin.name}, timeout: ${timeoutMs}ms`);
990
- const startTime = Date.now();
991
- streamInfo = await (0, timeout_1.withTimeout)(plugin.getStream(track), timeoutMs, "getStream timed out");
992
- const duration = Date.now() - startTime;
993
- this.debug(`[ResourceFromTrack] getStream successful with ${plugin.name} in ${duration}ms`);
994
- if (!streamInfo?.stream) {
995
- this.debug(`[ResourceFromTrack] getStream returned no stream from ${plugin.name}`);
996
- throw new Error(`No stream returned from ${plugin.name}`);
997
- }
998
- }
999
- catch (streamError) {
1000
- const errorMessage = streamError instanceof Error ? streamError.message : String(streamError);
1001
- this.debug(`[ResourceFromTrack] getStream failed with ${plugin.name}: ${errorMessage}`);
1002
- // Log more details for debugging
1003
- if (streamError instanceof Error && streamError.stack) {
1004
- this.debug(`[ResourceFromTrack] getStream error stack:`, streamError.stack);
1005
- }
1006
- // try fallbacks
1007
- this.debug(`[ResourceFromTrack] Attempting fallback plugins for track: ${track.title}`);
1008
- const allplugs = this.pluginManager.getAll();
1009
- let fallbackAttempts = 0;
1010
- for (const p of allplugs) {
1011
- if (typeof p.getFallback !== "function" && typeof p.getStream !== "function") {
1012
- this.debug(`[ResourceFromTrack] Skipping plugin ${p.name} - no getFallback or getStream method`);
1013
- continue;
1014
- }
1015
- fallbackAttempts++;
1016
- this.debug(`[ResourceFromTrack] Trying fallback plugin ${p.name} (attempt ${fallbackAttempts})`);
1017
- try {
1018
- // Try getStream first
1019
- const startTime = Date.now();
1020
- streamInfo = await (0, timeout_1.withTimeout)(p.getStream(track), timeoutMs, "getStream timed out");
1021
- const duration = Date.now() - startTime;
1022
- if (streamInfo?.stream) {
1023
- this.debug(`[ResourceFromTrack] Fallback getStream successful with ${p.name} in ${duration}ms`);
1024
- break;
1025
- }
1026
- // Try getFallback if getStream didn't work
1027
- this.debug(`[ResourceFromTrack] Trying getFallback with ${p.name}`);
1028
- const fallbackStartTime = Date.now();
1029
- streamInfo = await (0, timeout_1.withTimeout)(p.getFallback(track), timeoutMs, `getFallback timed out for plugin ${p.name}`);
1030
- const fallbackDuration = Date.now() - fallbackStartTime;
1031
- if (streamInfo?.stream) {
1032
- this.debug(`[ResourceFromTrack] Fallback getFallback successful with ${p.name} in ${fallbackDuration}ms`);
1033
- break;
1034
- }
1035
- this.debug(`[ResourceFromTrack] Fallback plugin ${p.name} returned no stream`);
1036
- }
1037
- catch (fallbackError) {
1038
- const errorMessage = fallbackError instanceof Error ? fallbackError.message : String(fallbackError);
1039
- this.debug(`[ResourceFromTrack] Fallback plugin ${p.name} failed: ${errorMessage}`);
1040
- // Log more details for debugging
1041
- if (fallbackError instanceof Error && fallbackError.stack) {
1042
- this.debug(`[ResourceFromTrack] Fallback error stack:`, fallbackError.stack);
1043
- }
1044
- }
1045
- }
1046
- if (!streamInfo?.stream) {
1047
- this.debug(`[ResourceFromTrack] All ${fallbackAttempts} fallback attempts failed for track: ${track.title}`);
1048
- throw new Error(`All getFallback attempts failed for track: ${track.title}`);
1049
- }
1050
- }
1051
- this.debug(`[ResourceFromTrack] Stream obtained, type: ${streamInfo.type}, metadata keys: ${Object.keys(streamInfo.metadata || {}).join(", ")}`);
1052
- const mapToStreamType = (type) => {
1053
- switch (type) {
1054
- case "webm/opus":
1055
- return voice_1.StreamType.WebmOpus;
1056
- case "ogg/opus":
1057
- return voice_1.StreamType.OggOpus;
1058
- case "arbitrary":
1059
- default:
1060
- return voice_1.StreamType.Arbitrary;
1061
- }
1062
- };
1063
- const inputType = mapToStreamType(streamInfo.type);
1064
- this.debug(`[ResourceFromTrack] Creating AudioResource with inputType: ${inputType}`);
1065
- // Merge metadata safely
1066
- const mergedMetadata = {
1067
- ...track,
1068
- ...(streamInfo.metadata || {}),
1069
- };
1070
- const audioResource = (0, voice_1.createAudioResource)(streamInfo.stream, {
1071
- // Prefer plugin-provided metadata (e.g., precise duration), fallback to track fields
1072
- metadata: mergedMetadata,
1073
- inputType,
1074
- inlineVolume: true,
1075
- });
1076
- this.debug(`[ResourceFromTrack] AudioResource created successfully for track: ${track.title}`);
1077
- return audioResource;
1078
- }
1079
876
  async generateWillNext() {
1080
877
  const lastTrack = this.queue.previousTracks[this.queue.previousTracks.length - 1] ?? this.queue.currentTrack;
1081
878
  if (!lastTrack)
@@ -1504,7 +1301,7 @@ class Player extends events_1.EventEmitter {
1504
1301
  }
1505
1302
  if (this.options.leaveOnEmpty && this.options.leaveTimeout) {
1506
1303
  this.leaveTimeout = setTimeout(() => {
1507
- this.debug(`[Player] Leaving voice channel after timeout`);
1304
+ this.debug(`[Player] Leaving voice channel after timeoutMs`);
1508
1305
  this.destroy();
1509
1306
  }, this.options.leaveTimeout);
1510
1307
  }
@@ -1625,6 +1422,111 @@ class Player extends events_1.EventEmitter {
1625
1422
  get relatedTracks() {
1626
1423
  return this.queue.relatedTracks();
1627
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
+ }
1628
1530
  }
1629
1531
  exports.Player = Player;
1630
1532
  //# sourceMappingURL=Player.js.map