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.
- package/dist/structures/Player.d.ts +38 -57
- package/dist/structures/Player.d.ts.map +1 -1
- package/dist/structures/Player.js +205 -303
- 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 +1 -1
- package/src/structures/Player.ts +225 -360
- package/src/structures/PlayerManager.ts +16 -11
- package/src/types/index.ts +56 -16
|
@@ -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
|
-
|
|
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
|
-
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
|
-
|
|
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
|
|
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.
|
|
723
|
-
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
790
|
-
|
|
791
|
-
|
|
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
|
|
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
|