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.
- package/README.md +212 -212
- package/dist/plugins/SoundCloudPlugin.d.ts +22 -0
- package/dist/plugins/SoundCloudPlugin.d.ts.map +1 -0
- package/dist/plugins/SoundCloudPlugin.js +171 -0
- package/dist/plugins/SoundCloudPlugin.js.map +1 -0
- package/dist/plugins/SpotifyPlugin.d.ts +26 -0
- package/dist/plugins/SpotifyPlugin.d.ts.map +1 -0
- package/dist/plugins/SpotifyPlugin.js +183 -0
- package/dist/plugins/SpotifyPlugin.js.map +1 -0
- package/dist/plugins/YouTubePlugin.d.ts +25 -0
- package/dist/plugins/YouTubePlugin.d.ts.map +1 -0
- package/dist/plugins/YouTubePlugin.js +314 -0
- package/dist/plugins/YouTubePlugin.js.map +1 -0
- package/dist/structures/Player.d.ts +61 -70
- package/dist/structures/Player.d.ts.map +1 -1
- package/dist/structures/Player.js +332 -355
- package/dist/structures/Player.js.map +1 -1
- package/dist/structures/PlayerManager.d.ts +5 -1
- package/dist/structures/PlayerManager.d.ts.map +1 -1
- package/dist/structures/PlayerManager.js.map +1 -1
- package/dist/types/index.d.ts +58 -16
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +45 -45
- package/src/extensions/BaseExtension.ts +35 -35
- package/src/extensions/index.ts +32 -32
- package/src/index.ts +16 -16
- package/src/plugins/BasePlugin.ts +26 -26
- package/src/plugins/index.ts +32 -32
- package/src/structures/Player.ts +1693 -1747
- package/src/structures/PlayerManager.ts +416 -411
- package/src/structures/Queue.ts +354 -354
- package/src/types/index.ts +510 -470
- package/src/utils/timeout.ts +10 -10
- 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
|
-
|
|
213
|
-
if (!
|
|
214
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
581
|
+
* Play a track, search query, search result, or play from queue
|
|
567
582
|
*
|
|
568
|
-
* @param {string | Track} query - Track URL, search query, or
|
|
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
|
-
|
|
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
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
isPlaylist = hookResponse.isPlaylist ?? hookTracks.length > 1;
|
|
613
|
+
if (!this.isPlaying) {
|
|
614
|
+
return await this.playNext();
|
|
615
|
+
}
|
|
616
|
+
return true;
|
|
609
617
|
}
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
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
|
|
619
|
-
|
|
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.
|
|
694
|
-
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
761
|
-
|
|
762
|
-
|
|
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
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
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
|
|
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
|