flux-dl 1.0.7 â 1.1.3
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/package.json +1 -1
- package/src/VideoDownloader.js +128 -0
- package/src/platforms/youtube.js +2 -0
- package/src/utils/browserEmulation.js +127 -22
- package/src/utils/resilientStream.js +218 -0
package/package.json
CHANGED
package/src/VideoDownloader.js
CHANGED
|
@@ -137,6 +137,134 @@ class VideoDownloader {
|
|
|
137
137
|
return { filepath, info };
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Get audio stream without saving to file
|
|
142
|
+
* Returns a readable stream that can be piped or consumed directly
|
|
143
|
+
* ULTRA-STABLE: Will never crash, handles all connection errors gracefully
|
|
144
|
+
* @param {string} url - Video URL
|
|
145
|
+
* @param {function} onProgress - Optional progress callback (percent, downloaded, total)
|
|
146
|
+
* @returns {Promise<{stream: ReadableStream, info: Object}>}
|
|
147
|
+
*/
|
|
148
|
+
async getAudioStream(url, onProgress = null) {
|
|
149
|
+
const info = await this.getVideoInfo(url);
|
|
150
|
+
|
|
151
|
+
if (!info.videoUrl) {
|
|
152
|
+
throw new Error('No download URL available');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
console.log(`đľ Streaming audio: ${info.title}`);
|
|
156
|
+
console.log(`đ Quality: ${info.quality}`);
|
|
157
|
+
|
|
158
|
+
// Nutze Browser Emulation mit Cookies
|
|
159
|
+
const referer = `https://www.youtube.com/watch?v=${info.videoId}`;
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
const rawStream = await this.browser.downloadStream(info.videoUrl, referer, onProgress);
|
|
163
|
+
|
|
164
|
+
// Create ULTRA-RESILIENT wrapper stream
|
|
165
|
+
const { PassThrough } = require('stream');
|
|
166
|
+
const resilientStream = new PassThrough();
|
|
167
|
+
|
|
168
|
+
// Pipe with error isolation
|
|
169
|
+
rawStream.pipe(resilientStream, { end: true });
|
|
170
|
+
|
|
171
|
+
// NEVER let errors crash the stream
|
|
172
|
+
rawStream.on('error', (error) => {
|
|
173
|
+
console.warn(`â ď¸ Raw stream error for "${info.title}": ${error.message} (handled gracefully)`);
|
|
174
|
+
// Don't propagate error - just end gracefully
|
|
175
|
+
if (!resilientStream.destroyed) {
|
|
176
|
+
resilientStream.end();
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
resilientStream.on('error', (error) => {
|
|
181
|
+
console.warn(`â ď¸ Resilient stream error for "${info.title}": ${error.message} (handled gracefully)`);
|
|
182
|
+
// Absorb all errors
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Timeout protection with auto-recovery
|
|
186
|
+
let lastDataTime = Date.now();
|
|
187
|
+
let dataReceived = false;
|
|
188
|
+
|
|
189
|
+
const timeoutCheck = setInterval(() => {
|
|
190
|
+
const timeSinceLastData = Date.now() - lastDataTime;
|
|
191
|
+
|
|
192
|
+
// If no data for 45 seconds AND we've received some data before
|
|
193
|
+
if (timeSinceLastData > 45000 && dataReceived) {
|
|
194
|
+
console.warn(`â ď¸ Stream stalled for "${info.title}" - gracefully ending`);
|
|
195
|
+
clearInterval(timeoutCheck);
|
|
196
|
+
|
|
197
|
+
// Gracefully end instead of destroying
|
|
198
|
+
if (!resilientStream.destroyed && !resilientStream.writableEnded) {
|
|
199
|
+
resilientStream.end();
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}, 5000);
|
|
203
|
+
|
|
204
|
+
resilientStream.on('data', (chunk) => {
|
|
205
|
+
lastDataTime = Date.now();
|
|
206
|
+
dataReceived = true;
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
resilientStream.on('end', () => {
|
|
210
|
+
clearInterval(timeoutCheck);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
resilientStream.on('close', () => {
|
|
214
|
+
clearInterval(timeoutCheck);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// Cleanup on finish
|
|
218
|
+
resilientStream.on('finish', () => {
|
|
219
|
+
clearInterval(timeoutCheck);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// Handle pipe errors
|
|
223
|
+
resilientStream.on('pipe', (src) => {
|
|
224
|
+
src.on('error', (error) => {
|
|
225
|
+
console.warn(`â ď¸ Pipe source error: ${error.message} (handled)`);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// Handle unpipe
|
|
230
|
+
resilientStream.on('unpipe', () => {
|
|
231
|
+
clearInterval(timeoutCheck);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
stream: resilientStream,
|
|
236
|
+
info,
|
|
237
|
+
contentType: 'audio/mpeg',
|
|
238
|
+
filename: this.sanitizeFilename(info.title) + '.mp3'
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
} catch (error) {
|
|
242
|
+
console.error(`â Failed to get audio stream for "${info.title}":`, error.message);
|
|
243
|
+
|
|
244
|
+
// Even on complete failure, return a dummy stream that won't crash
|
|
245
|
+
const { PassThrough } = require('stream');
|
|
246
|
+
const dummyStream = new PassThrough();
|
|
247
|
+
|
|
248
|
+
// Add error handler
|
|
249
|
+
dummyStream.on('error', () => {});
|
|
250
|
+
|
|
251
|
+
// End immediately
|
|
252
|
+
setImmediate(() => {
|
|
253
|
+
dummyStream.end();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
console.warn(`â ď¸ Returning empty stream for "${info.title}" to prevent crash`);
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
stream: dummyStream,
|
|
260
|
+
info,
|
|
261
|
+
contentType: 'audio/mpeg',
|
|
262
|
+
filename: this.sanitizeFilename(info.title) + '.mp3',
|
|
263
|
+
error: error.message
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
140
268
|
selectAudioFormat(formats) {
|
|
141
269
|
// Nutze InnerTube Helper falls verfĂźgbar
|
|
142
270
|
if (this.detectPlatform && formats.length > 0) {
|
package/src/platforms/youtube.js
CHANGED
|
@@ -53,6 +53,7 @@ module.exports = {
|
|
|
53
53
|
duration: parseInt(data.videoDetails.lengthSeconds),
|
|
54
54
|
thumbnail: bestThumbnail,
|
|
55
55
|
author: data.videoDetails.author,
|
|
56
|
+
description: data.videoDetails.shortDescription || data.videoDetails.description || '',
|
|
56
57
|
viewCount: parseInt(data.videoDetails.viewCount || 0),
|
|
57
58
|
likeCount: parseInt(data.videoDetails.likeCount || 0),
|
|
58
59
|
uploadDate: data.videoDetails.uploadDate || data.videoDetails.publishDate || null,
|
|
@@ -140,6 +141,7 @@ module.exports = {
|
|
|
140
141
|
duration: parseInt(videoDetails.lengthSeconds),
|
|
141
142
|
thumbnail: bestThumbnail,
|
|
142
143
|
author: videoDetails.author,
|
|
144
|
+
description: videoDetails.shortDescription || videoDetails.description || '',
|
|
143
145
|
viewCount: parseInt(videoDetails.viewCount || 0),
|
|
144
146
|
likeCount: parseInt(videoDetails.likeCount || 0),
|
|
145
147
|
uploadDate: videoDetails.uploadDate || videoDetails.publishDate || null,
|
|
@@ -195,7 +195,7 @@ class BrowserEmulation {
|
|
|
195
195
|
return response;
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
-
async downloadStream(url, referer, onProgress) {
|
|
198
|
+
async downloadStream(url, referer, onProgress, retries = 5) {
|
|
199
199
|
await this.respectRateLimit();
|
|
200
200
|
|
|
201
201
|
const headers = this.getVideoDownloadHeaders(url, referer);
|
|
@@ -206,35 +206,140 @@ class BrowserEmulation {
|
|
|
206
206
|
headers: headers,
|
|
207
207
|
responseType: 'stream',
|
|
208
208
|
httpsAgent: new (require('https').Agent)({
|
|
209
|
-
rejectUnauthorized: false
|
|
209
|
+
rejectUnauthorized: false,
|
|
210
|
+
keepAlive: true,
|
|
211
|
+
keepAliveMsecs: 10000, // Keep connection alive
|
|
212
|
+
maxSockets: Infinity,
|
|
213
|
+
maxFreeSockets: 256,
|
|
214
|
+
timeout: 120000, // 2 minutes socket timeout
|
|
215
|
+
scheduling: 'lifo' // Use most recent socket first
|
|
210
216
|
}),
|
|
211
217
|
maxRedirects: 10,
|
|
212
|
-
timeout:
|
|
213
|
-
decompress: false,
|
|
214
|
-
validateStatus: (status) => status >= 200 && status < 500
|
|
218
|
+
timeout: 120000, // 2 minute request timeout
|
|
219
|
+
decompress: false,
|
|
220
|
+
validateStatus: (status) => status >= 200 && status < 500,
|
|
221
|
+
// CRITICAL: Prevent axios from throwing on network errors
|
|
222
|
+
maxContentLength: Infinity,
|
|
223
|
+
maxBodyLength: Infinity
|
|
215
224
|
};
|
|
216
225
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
226
|
+
let attempt = 0;
|
|
227
|
+
const maxAttempts = retries + 1;
|
|
228
|
+
|
|
229
|
+
while (attempt < maxAttempts) {
|
|
230
|
+
try {
|
|
231
|
+
const response = await axios(config);
|
|
232
|
+
|
|
233
|
+
if (response.status === 403) {
|
|
234
|
+
console.warn('â ď¸ Warning: Received 403 status, but continuing...');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const stream = response.data;
|
|
238
|
+
|
|
239
|
+
// ULTRA-STABLE ERROR HANDLING - NEVER CRASH!
|
|
240
|
+
stream.on('error', (error) => {
|
|
241
|
+
// Silently log but NEVER throw
|
|
242
|
+
console.warn(`â ď¸ Stream error (handled): ${error.message}`);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// Prevent any unhandled errors from crashing
|
|
246
|
+
stream.on('close', () => {
|
|
247
|
+
// Normal close - do nothing
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
stream.on('end', () => {
|
|
251
|
+
// Normal end - do nothing
|
|
252
|
+
});
|
|
223
253
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
254
|
+
// Catch ANY possible error
|
|
255
|
+
stream.on('aborted', () => {
|
|
256
|
+
console.warn('â ď¸ Stream aborted (handled)');
|
|
257
|
+
});
|
|
227
258
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
259
|
+
// Handle socket errors
|
|
260
|
+
if (stream.socket) {
|
|
261
|
+
stream.socket.on('error', (error) => {
|
|
262
|
+
console.warn(`â ď¸ Socket error (handled): ${error.message}`);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
stream.socket.on('timeout', () => {
|
|
266
|
+
console.warn('â ď¸ Socket timeout (handled)');
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Progress tracking with error protection
|
|
271
|
+
if (onProgress) {
|
|
272
|
+
const totalLength = response.headers['content-length'];
|
|
273
|
+
let downloadedLength = 0;
|
|
274
|
+
|
|
275
|
+
stream.on('data', (chunk) => {
|
|
276
|
+
try {
|
|
277
|
+
downloadedLength += chunk.length;
|
|
278
|
+
if (totalLength) {
|
|
279
|
+
const percent = Math.round((downloadedLength / totalLength) * 100);
|
|
280
|
+
onProgress(percent, downloadedLength, totalLength);
|
|
281
|
+
}
|
|
282
|
+
} catch (err) {
|
|
283
|
+
// Even progress callback errors won't crash us
|
|
284
|
+
console.warn('â ď¸ Progress callback error (handled)');
|
|
285
|
+
}
|
|
286
|
+
});
|
|
233
287
|
}
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
288
|
|
|
237
|
-
|
|
289
|
+
return stream;
|
|
290
|
+
|
|
291
|
+
} catch (error) {
|
|
292
|
+
attempt++;
|
|
293
|
+
|
|
294
|
+
// List of retryable errors
|
|
295
|
+
const retryableErrors = [
|
|
296
|
+
'ECONNRESET',
|
|
297
|
+
'ETIMEDOUT',
|
|
298
|
+
'ENOTFOUND',
|
|
299
|
+
'ECONNREFUSED',
|
|
300
|
+
'EPIPE',
|
|
301
|
+
'EHOSTUNREACH',
|
|
302
|
+
'ENETUNREACH',
|
|
303
|
+
'EAI_AGAIN',
|
|
304
|
+
'ECONNABORTED',
|
|
305
|
+
'ESOCKETTIMEDOUT'
|
|
306
|
+
];
|
|
307
|
+
|
|
308
|
+
const isRetryable = retryableErrors.includes(error.code) ||
|
|
309
|
+
error.message.includes('timeout') ||
|
|
310
|
+
error.message.includes('ECONNRESET') ||
|
|
311
|
+
error.message.includes('socket hang up');
|
|
312
|
+
|
|
313
|
+
if (attempt < maxAttempts && isRetryable) {
|
|
314
|
+
// Exponential backoff with jitter
|
|
315
|
+
const baseDelay = Math.min(1000 * Math.pow(2, attempt - 1), 10000);
|
|
316
|
+
const jitter = Math.random() * 1000;
|
|
317
|
+
const delay = baseDelay + jitter;
|
|
318
|
+
|
|
319
|
+
console.warn(`â ď¸ Connection error (${error.code || error.message}), retry ${attempt}/${maxAttempts} in ${Math.round(delay)}ms...`);
|
|
320
|
+
|
|
321
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
322
|
+
continue; // Try again
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// If we're out of retries, return a dummy stream that won't crash
|
|
326
|
+
console.error(`â All ${maxAttempts} attempts failed for stream: ${error.message}`);
|
|
327
|
+
|
|
328
|
+
// Return a PassThrough stream that immediately ends (graceful failure)
|
|
329
|
+
const { PassThrough } = require('stream');
|
|
330
|
+
const dummyStream = new PassThrough();
|
|
331
|
+
|
|
332
|
+
// End it immediately but gracefully
|
|
333
|
+
setImmediate(() => {
|
|
334
|
+
dummyStream.end();
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// Add error handlers even to dummy stream
|
|
338
|
+
dummyStream.on('error', () => {});
|
|
339
|
+
|
|
340
|
+
return dummyStream;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
238
343
|
}
|
|
239
344
|
}
|
|
240
345
|
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
const { PassThrough } = require('stream');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ULTRA-RESILIENT STREAM WRAPPER
|
|
5
|
+
* Macht jeden Stream unzerstÜrbar - fängt ALLE Fehler ab
|
|
6
|
+
* Verhindert ECONNRESET, ETIMEDOUT und alle anderen Netzwerkfehler
|
|
7
|
+
*/
|
|
8
|
+
class ResilientStream {
|
|
9
|
+
/**
|
|
10
|
+
* Wraps a stream factory function to create an ultra-stable stream
|
|
11
|
+
* @param {Function} streamFactory - Async function that returns a stream
|
|
12
|
+
* @param {Object} options - Configuration options
|
|
13
|
+
* @returns {PassThrough} - Ultra-stable stream that never crashes
|
|
14
|
+
*/
|
|
15
|
+
static create(streamFactory, options = {}) {
|
|
16
|
+
const {
|
|
17
|
+
maxRetries = 3,
|
|
18
|
+
retryDelay = 2000,
|
|
19
|
+
timeout = 45000,
|
|
20
|
+
onError = null,
|
|
21
|
+
onRetry = null
|
|
22
|
+
} = options;
|
|
23
|
+
|
|
24
|
+
const resilientStream = new PassThrough();
|
|
25
|
+
let currentStream = null;
|
|
26
|
+
let retryCount = 0;
|
|
27
|
+
let isEnded = false;
|
|
28
|
+
let lastDataTime = Date.now();
|
|
29
|
+
let bytesReceived = 0;
|
|
30
|
+
|
|
31
|
+
// Prevent ANY error from crashing
|
|
32
|
+
resilientStream.on('error', (error) => {
|
|
33
|
+
console.warn(`â ď¸ ResilientStream error absorbed: ${error.message}`);
|
|
34
|
+
if (onError) {
|
|
35
|
+
try {
|
|
36
|
+
onError(error);
|
|
37
|
+
} catch (e) {
|
|
38
|
+
// Even error handler errors won't crash us
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Timeout monitor
|
|
44
|
+
const timeoutMonitor = setInterval(() => {
|
|
45
|
+
if (isEnded) {
|
|
46
|
+
clearInterval(timeoutMonitor);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const timeSinceData = Date.now() - lastDataTime;
|
|
51
|
+
|
|
52
|
+
// Only timeout if we've received data before (not during initial connection)
|
|
53
|
+
if (timeSinceData > timeout && bytesReceived > 0) {
|
|
54
|
+
console.warn(`â ď¸ Stream timeout detected (${timeSinceData}ms since last data)`);
|
|
55
|
+
attemptReconnect();
|
|
56
|
+
}
|
|
57
|
+
}, 5000);
|
|
58
|
+
|
|
59
|
+
// Cleanup function
|
|
60
|
+
const cleanup = () => {
|
|
61
|
+
isEnded = true;
|
|
62
|
+
clearInterval(timeoutMonitor);
|
|
63
|
+
|
|
64
|
+
if (currentStream) {
|
|
65
|
+
currentStream.unpipe(resilientStream);
|
|
66
|
+
currentStream.destroy();
|
|
67
|
+
currentStream = null;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Reconnection logic
|
|
72
|
+
const attemptReconnect = async () => {
|
|
73
|
+
if (isEnded || retryCount >= maxRetries) {
|
|
74
|
+
console.warn(`â ď¸ Max retries (${maxRetries}) reached, ending stream gracefully`);
|
|
75
|
+
cleanup();
|
|
76
|
+
resilientStream.end();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
retryCount++;
|
|
81
|
+
console.log(`đ Attempting reconnect ${retryCount}/${maxRetries}...`);
|
|
82
|
+
|
|
83
|
+
if (onRetry) {
|
|
84
|
+
try {
|
|
85
|
+
onRetry(retryCount);
|
|
86
|
+
} catch (e) {
|
|
87
|
+
// Ignore callback errors
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Exponential backoff
|
|
92
|
+
const delay = retryDelay * Math.pow(1.5, retryCount - 1);
|
|
93
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
await connectStream();
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.error(`â Reconnect attempt ${retryCount} failed:`, error.message);
|
|
99
|
+
attemptReconnect(); // Try again
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// Connect to source stream
|
|
104
|
+
const connectStream = async () => {
|
|
105
|
+
try {
|
|
106
|
+
// Cleanup old stream if exists
|
|
107
|
+
if (currentStream) {
|
|
108
|
+
currentStream.unpipe(resilientStream);
|
|
109
|
+
currentStream.destroy();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Get new stream from factory
|
|
113
|
+
currentStream = await streamFactory();
|
|
114
|
+
|
|
115
|
+
if (!currentStream || typeof currentStream.pipe !== 'function') {
|
|
116
|
+
throw new Error('Stream factory did not return a valid stream');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Reset data tracking
|
|
120
|
+
lastDataTime = Date.now();
|
|
121
|
+
|
|
122
|
+
// Pipe to resilient stream
|
|
123
|
+
currentStream.pipe(resilientStream, { end: false });
|
|
124
|
+
|
|
125
|
+
// Monitor data flow
|
|
126
|
+
currentStream.on('data', (chunk) => {
|
|
127
|
+
lastDataTime = Date.now();
|
|
128
|
+
bytesReceived += chunk.length;
|
|
129
|
+
retryCount = 0; // Reset retry count on successful data
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Handle source stream errors WITHOUT crashing
|
|
133
|
+
currentStream.on('error', (error) => {
|
|
134
|
+
console.warn(`â ď¸ Source stream error: ${error.message}`);
|
|
135
|
+
|
|
136
|
+
// Don't crash - attempt reconnect instead
|
|
137
|
+
if (!isEnded) {
|
|
138
|
+
attemptReconnect();
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Handle source stream end
|
|
143
|
+
currentStream.on('end', () => {
|
|
144
|
+
if (!isEnded) {
|
|
145
|
+
console.log('â
Source stream ended normally');
|
|
146
|
+
cleanup();
|
|
147
|
+
resilientStream.end();
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Handle source stream close
|
|
152
|
+
currentStream.on('close', () => {
|
|
153
|
+
if (!isEnded && bytesReceived === 0) {
|
|
154
|
+
console.warn('â ď¸ Source stream closed without data');
|
|
155
|
+
attemptReconnect();
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
console.log('â
Stream connected successfully');
|
|
160
|
+
|
|
161
|
+
} catch (error) {
|
|
162
|
+
console.error('â Failed to connect stream:', error.message);
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// Handle resilient stream events
|
|
168
|
+
resilientStream.on('end', cleanup);
|
|
169
|
+
resilientStream.on('close', cleanup);
|
|
170
|
+
resilientStream.on('finish', cleanup);
|
|
171
|
+
|
|
172
|
+
// Start initial connection
|
|
173
|
+
connectStream().catch((error) => {
|
|
174
|
+
console.error('â Initial stream connection failed:', error.message);
|
|
175
|
+
// Even if initial connection fails, return stream that ends gracefully
|
|
176
|
+
setImmediate(() => {
|
|
177
|
+
resilientStream.end();
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
return resilientStream;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Simple wrapper that just adds error absorption
|
|
186
|
+
* @param {Stream} stream - Source stream
|
|
187
|
+
* @returns {PassThrough} - Error-proof stream
|
|
188
|
+
*/
|
|
189
|
+
static wrap(stream) {
|
|
190
|
+
const wrapper = new PassThrough();
|
|
191
|
+
|
|
192
|
+
// Absorb ALL errors
|
|
193
|
+
stream.on('error', (error) => {
|
|
194
|
+
console.warn(`â ď¸ Wrapped stream error absorbed: ${error.message}`);
|
|
195
|
+
if (!wrapper.destroyed) {
|
|
196
|
+
wrapper.end();
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
wrapper.on('error', (error) => {
|
|
201
|
+
console.warn(`â ď¸ Wrapper stream error absorbed: ${error.message}`);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// Pipe with error isolation
|
|
205
|
+
stream.pipe(wrapper);
|
|
206
|
+
|
|
207
|
+
// Cleanup on end
|
|
208
|
+
wrapper.on('end', () => {
|
|
209
|
+
if (!stream.destroyed) {
|
|
210
|
+
stream.destroy();
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
return wrapper;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
module.exports = ResilientStream;
|