flux-dl 1.0.8 â 1.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/package.json +1 -1
- package/src/VideoDownloader.js +279 -10
- package/src/index.js +2 -0
- package/src/utils/browserEmulation.js +127 -22
- package/src/utils/qualityManager.js +270 -0
- package/src/utils/resilientStream.js +218 -0
package/package.json
CHANGED
package/src/VideoDownloader.js
CHANGED
|
@@ -5,6 +5,7 @@ const platforms = require('./platforms');
|
|
|
5
5
|
const VideoEncryption = require('./utils/encryption');
|
|
6
6
|
const RequestSpoofing = require('./utils/requestSpoofing');
|
|
7
7
|
const BrowserEmulation = require('./utils/browserEmulation');
|
|
8
|
+
const QualityManager = require('./utils/qualityManager');
|
|
8
9
|
|
|
9
10
|
class VideoDownloader {
|
|
10
11
|
constructor(options = {}) {
|
|
@@ -13,17 +14,24 @@ class VideoDownloader {
|
|
|
13
14
|
timeout: 30000,
|
|
14
15
|
encryptionKey: options.encryptionKey || 'default-key',
|
|
15
16
|
cookiesFile: options.cookiesFile,
|
|
17
|
+
quality: options.quality || 192, // Standard: Medium (192 kbps)
|
|
16
18
|
...options
|
|
17
19
|
};
|
|
18
20
|
|
|
19
21
|
this.encryption = new VideoEncryption(this.options.encryptionKey);
|
|
20
22
|
this.spoofing = new RequestSpoofing();
|
|
21
23
|
this.browser = new BrowserEmulation();
|
|
24
|
+
this.qualityManager = new QualityManager();
|
|
22
25
|
|
|
23
26
|
// Load cookies if provided
|
|
24
27
|
if (this.options.cookiesFile) {
|
|
25
28
|
this.browser.cookieManager.loadFromFile(this.options.cookiesFile);
|
|
26
29
|
}
|
|
30
|
+
|
|
31
|
+
// Set default quality if provided
|
|
32
|
+
if (options.quality) {
|
|
33
|
+
this.qualityManager.setDefaultQuality(options.quality);
|
|
34
|
+
}
|
|
27
35
|
}
|
|
28
36
|
|
|
29
37
|
async getVideoInfo(url) {
|
|
@@ -140,6 +148,7 @@ class VideoDownloader {
|
|
|
140
148
|
/**
|
|
141
149
|
* Get audio stream without saving to file
|
|
142
150
|
* Returns a readable stream that can be piped or consumed directly
|
|
151
|
+
* ULTRA-STABLE: Will never crash, handles all connection errors gracefully
|
|
143
152
|
* @param {string} url - Video URL
|
|
144
153
|
* @param {function} onProgress - Optional progress callback (percent, downloaded, total)
|
|
145
154
|
* @returns {Promise<{stream: ReadableStream, info: Object}>}
|
|
@@ -151,20 +160,117 @@ class VideoDownloader {
|
|
|
151
160
|
throw new Error('No download URL available');
|
|
152
161
|
}
|
|
153
162
|
|
|
154
|
-
console.log(
|
|
155
|
-
console.log(
|
|
163
|
+
console.log(`đľ Streaming audio: ${info.title}`);
|
|
164
|
+
console.log(`đ Quality: ${info.quality}`);
|
|
156
165
|
|
|
157
166
|
// Nutze Browser Emulation mit Cookies
|
|
158
167
|
const referer = `https://www.youtube.com/watch?v=${info.videoId}`;
|
|
159
168
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
stream
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
169
|
+
try {
|
|
170
|
+
const rawStream = await this.browser.downloadStream(info.videoUrl, referer, onProgress);
|
|
171
|
+
|
|
172
|
+
// Create ULTRA-RESILIENT wrapper stream
|
|
173
|
+
const { PassThrough } = require('stream');
|
|
174
|
+
const resilientStream = new PassThrough();
|
|
175
|
+
|
|
176
|
+
// Pipe with error isolation
|
|
177
|
+
rawStream.pipe(resilientStream, { end: true });
|
|
178
|
+
|
|
179
|
+
// NEVER let errors crash the stream
|
|
180
|
+
rawStream.on('error', (error) => {
|
|
181
|
+
console.warn(`â ď¸ Raw stream error for "${info.title}": ${error.message} (handled gracefully)`);
|
|
182
|
+
// Don't propagate error - just end gracefully
|
|
183
|
+
if (!resilientStream.destroyed) {
|
|
184
|
+
resilientStream.end();
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
resilientStream.on('error', (error) => {
|
|
189
|
+
console.warn(`â ď¸ Resilient stream error for "${info.title}": ${error.message} (handled gracefully)`);
|
|
190
|
+
// Absorb all errors
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// Timeout protection with auto-recovery
|
|
194
|
+
let lastDataTime = Date.now();
|
|
195
|
+
let dataReceived = false;
|
|
196
|
+
|
|
197
|
+
const timeoutCheck = setInterval(() => {
|
|
198
|
+
const timeSinceLastData = Date.now() - lastDataTime;
|
|
199
|
+
|
|
200
|
+
// If no data for 45 seconds AND we've received some data before
|
|
201
|
+
if (timeSinceLastData > 45000 && dataReceived) {
|
|
202
|
+
console.warn(`â ď¸ Stream stalled for "${info.title}" - gracefully ending`);
|
|
203
|
+
clearInterval(timeoutCheck);
|
|
204
|
+
|
|
205
|
+
// Gracefully end instead of destroying
|
|
206
|
+
if (!resilientStream.destroyed && !resilientStream.writableEnded) {
|
|
207
|
+
resilientStream.end();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}, 5000);
|
|
211
|
+
|
|
212
|
+
resilientStream.on('data', (chunk) => {
|
|
213
|
+
lastDataTime = Date.now();
|
|
214
|
+
dataReceived = true;
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
resilientStream.on('end', () => {
|
|
218
|
+
clearInterval(timeoutCheck);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
resilientStream.on('close', () => {
|
|
222
|
+
clearInterval(timeoutCheck);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// Cleanup on finish
|
|
226
|
+
resilientStream.on('finish', () => {
|
|
227
|
+
clearInterval(timeoutCheck);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// Handle pipe errors
|
|
231
|
+
resilientStream.on('pipe', (src) => {
|
|
232
|
+
src.on('error', (error) => {
|
|
233
|
+
console.warn(`â ď¸ Pipe source error: ${error.message} (handled)`);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// Handle unpipe
|
|
238
|
+
resilientStream.on('unpipe', () => {
|
|
239
|
+
clearInterval(timeoutCheck);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
stream: resilientStream,
|
|
244
|
+
info,
|
|
245
|
+
contentType: 'audio/mpeg',
|
|
246
|
+
filename: this.sanitizeFilename(info.title) + '.mp3'
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
} catch (error) {
|
|
250
|
+
console.error(`â Failed to get audio stream for "${info.title}":`, error.message);
|
|
251
|
+
|
|
252
|
+
// Even on complete failure, return a dummy stream that won't crash
|
|
253
|
+
const { PassThrough } = require('stream');
|
|
254
|
+
const dummyStream = new PassThrough();
|
|
255
|
+
|
|
256
|
+
// Add error handler
|
|
257
|
+
dummyStream.on('error', () => {});
|
|
258
|
+
|
|
259
|
+
// End immediately
|
|
260
|
+
setImmediate(() => {
|
|
261
|
+
dummyStream.end();
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
console.warn(`â ď¸ Returning empty stream for "${info.title}" to prevent crash`);
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
stream: dummyStream,
|
|
268
|
+
info,
|
|
269
|
+
contentType: 'audio/mpeg',
|
|
270
|
+
filename: this.sanitizeFilename(info.title) + '.mp3',
|
|
271
|
+
error: error.message
|
|
272
|
+
};
|
|
273
|
+
}
|
|
168
274
|
}
|
|
169
275
|
|
|
170
276
|
selectAudioFormat(formats) {
|
|
@@ -249,6 +355,169 @@ class VideoDownloader {
|
|
|
249
355
|
signUrl(url, expiresIn = 3600) {
|
|
250
356
|
return this.encryption.signUrl(url, expiresIn);
|
|
251
357
|
}
|
|
358
|
+
|
|
359
|
+
// ========== QUALITY MANAGEMENT API ==========
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Setzt die Qualität fßr einen bestimmten User
|
|
363
|
+
* @param {string} userId - User ID
|
|
364
|
+
* @param {number} quality - Bitrate (120-360 kbps)
|
|
365
|
+
* @returns {number} Gesetzte Qualität
|
|
366
|
+
*/
|
|
367
|
+
setUserQuality(userId, quality) {
|
|
368
|
+
return this.qualityManager.setUserQuality(userId, quality);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Gibt die erlaubte Qualität fßr einen User zurßck
|
|
373
|
+
* @param {string} userId - User ID
|
|
374
|
+
* @param {boolean} isPremium - Ist Premium User?
|
|
375
|
+
* @returns {number} Erlaubte Bitrate
|
|
376
|
+
*/
|
|
377
|
+
getUserQuality(userId, isPremium = false) {
|
|
378
|
+
return this.qualityManager.getUserQuality(userId, isPremium);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Setzt globale Standard-Qualität
|
|
383
|
+
* @param {number} quality - Bitrate (120-360 kbps)
|
|
384
|
+
*/
|
|
385
|
+
setDefaultQuality(quality) {
|
|
386
|
+
this.qualityManager.setDefaultQuality(quality);
|
|
387
|
+
this.options.quality = quality;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Setzt maximale Qualität fßr Free Users
|
|
392
|
+
* @param {number} quality - Bitrate (120-360 kbps)
|
|
393
|
+
*/
|
|
394
|
+
setMaxQualityForFreeUsers(quality) {
|
|
395
|
+
this.qualityManager.setMaxQualityForFreeUsers(quality);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Setzt maximale Qualität fßr Premium Users
|
|
400
|
+
* @param {number} quality - Bitrate (120-360 kbps)
|
|
401
|
+
*/
|
|
402
|
+
setMaxQualityForPremiumUsers(quality) {
|
|
403
|
+
this.qualityManager.setMaxQualityForPremiumUsers(quality);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Gibt verfßgbare Qualitätsstufen fßr User zurßck
|
|
408
|
+
* @param {string} userId - User ID
|
|
409
|
+
* @param {boolean} isPremium - Ist Premium User?
|
|
410
|
+
* @returns {Array} VerfĂźgbare Stufen
|
|
411
|
+
*/
|
|
412
|
+
getAvailableQualities(userId, isPremium = false) {
|
|
413
|
+
return this.qualityManager.getAvailableQualities(userId, isPremium);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Prßft ob User bestimmte Qualität nutzen darf
|
|
418
|
+
* @param {string} userId - User ID
|
|
419
|
+
* @param {number} requestedQuality - Gewßnschte Qualität
|
|
420
|
+
* @param {boolean} isPremium - Ist Premium User?
|
|
421
|
+
* @returns {boolean} Erlaubt?
|
|
422
|
+
*/
|
|
423
|
+
canUserUseQuality(userId, requestedQuality, isPremium = false) {
|
|
424
|
+
return this.qualityManager.canUserUseQuality(userId, requestedQuality, isPremium);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Download mit User-spezifischer Qualität
|
|
429
|
+
* @param {string} url - Video URL
|
|
430
|
+
* @param {string} outputPath - Output Pfad
|
|
431
|
+
* @param {string} userId - User ID
|
|
432
|
+
* @param {boolean} isPremium - Ist Premium User?
|
|
433
|
+
* @param {function} onProgress - Progress Callback
|
|
434
|
+
* @returns {Promise<Object>} Download Result
|
|
435
|
+
*/
|
|
436
|
+
async downloadAudioWithQuality(url, outputPath, userId, isPremium = false, onProgress = null) {
|
|
437
|
+
const userQuality = this.getUserQuality(userId, isPremium);
|
|
438
|
+
|
|
439
|
+
console.log(`đľ Downloading for user ${userId}`);
|
|
440
|
+
console.log(`đ Quality: ${this.qualityManager.getQualityLabel(userQuality)}`);
|
|
441
|
+
|
|
442
|
+
// Temporär Qualität setzen
|
|
443
|
+
const originalQuality = this.options.quality;
|
|
444
|
+
this.options.quality = userQuality;
|
|
445
|
+
|
|
446
|
+
try {
|
|
447
|
+
const result = await this.downloadAudio(url, outputPath, onProgress);
|
|
448
|
+
return {
|
|
449
|
+
...result,
|
|
450
|
+
quality: userQuality,
|
|
451
|
+
qualityLabel: this.qualityManager.getQualityLabel(userQuality)
|
|
452
|
+
};
|
|
453
|
+
} finally {
|
|
454
|
+
// Qualität zurßcksetzen
|
|
455
|
+
this.options.quality = originalQuality;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Stream mit User-spezifischer Qualität
|
|
461
|
+
* @param {string} url - Video URL
|
|
462
|
+
* @param {string} userId - User ID
|
|
463
|
+
* @param {boolean} isPremium - Ist Premium User?
|
|
464
|
+
* @param {function} onProgress - Progress Callback
|
|
465
|
+
* @returns {Promise<Object>} Stream Result
|
|
466
|
+
*/
|
|
467
|
+
async getAudioStreamWithQuality(url, userId, isPremium = false, onProgress = null) {
|
|
468
|
+
const userQuality = this.getUserQuality(userId, isPremium);
|
|
469
|
+
|
|
470
|
+
console.log(`đľ Streaming for user ${userId}`);
|
|
471
|
+
console.log(`đ Quality: ${this.qualityManager.getQualityLabel(userQuality)}`);
|
|
472
|
+
|
|
473
|
+
// Temporär Qualität setzen
|
|
474
|
+
const originalQuality = this.options.quality;
|
|
475
|
+
this.options.quality = userQuality;
|
|
476
|
+
|
|
477
|
+
try {
|
|
478
|
+
const result = await this.getAudioStream(url, onProgress);
|
|
479
|
+
return {
|
|
480
|
+
...result,
|
|
481
|
+
quality: userQuality,
|
|
482
|
+
qualityLabel: this.qualityManager.getQualityLabel(userQuality)
|
|
483
|
+
};
|
|
484
|
+
} finally {
|
|
485
|
+
// Qualität zurßcksetzen
|
|
486
|
+
this.options.quality = originalQuality;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Entfernt User-Permission
|
|
492
|
+
* @param {string} userId - User ID
|
|
493
|
+
*/
|
|
494
|
+
removeUserQuality(userId) {
|
|
495
|
+
this.qualityManager.removeUserQuality(userId);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Gibt alle User-Permissions zurĂźck
|
|
500
|
+
* @returns {Object} User-Permissions
|
|
501
|
+
*/
|
|
502
|
+
getAllUserPermissions() {
|
|
503
|
+
return this.qualityManager.getAllUserPermissions();
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Exportiert Quality-Konfiguration
|
|
508
|
+
* @returns {Object} Konfiguration
|
|
509
|
+
*/
|
|
510
|
+
exportQualityConfig() {
|
|
511
|
+
return this.qualityManager.exportConfig();
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Importiert Quality-Konfiguration
|
|
516
|
+
* @param {Object} config - Konfiguration
|
|
517
|
+
*/
|
|
518
|
+
importQualityConfig(config) {
|
|
519
|
+
this.qualityManager.importConfig(config);
|
|
520
|
+
}
|
|
252
521
|
}
|
|
253
522
|
|
|
254
523
|
module.exports = VideoDownloader;
|
package/src/index.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
const VideoDownloader = require('./VideoDownloader');
|
|
2
2
|
const VideoEncryption = require('./utils/encryption');
|
|
3
3
|
const YouTubeSearch = require('./utils/youtubeSearch');
|
|
4
|
+
const QualityManager = require('./utils/qualityManager');
|
|
4
5
|
const { supportedPlatforms } = require('./platforms');
|
|
5
6
|
|
|
6
7
|
module.exports = {
|
|
7
8
|
VideoDownloader,
|
|
8
9
|
VideoEncryption,
|
|
9
10
|
YouTubeSearch,
|
|
11
|
+
QualityManager,
|
|
10
12
|
supportedPlatforms
|
|
11
13
|
};
|
|
@@ -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,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QUALITY MANAGER - Audio Bitrate Control System
|
|
3
|
+
* Verwaltet Audio-Qualität mit User-Permissions (120-360 kbps)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class QualityManager {
|
|
7
|
+
constructor() {
|
|
8
|
+
// Qualitätsstufen (in kbps fßr Audio)
|
|
9
|
+
this.QUALITY_LEVELS = {
|
|
10
|
+
MINIMUM: 120, // Niedrigste Qualität
|
|
11
|
+
LOW: 128, // Niedrig
|
|
12
|
+
MEDIUM: 192, // Mittel (Standard)
|
|
13
|
+
HIGH: 256, // Hoch
|
|
14
|
+
MAXIMUM: 360 // HÜchste Qualität
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// User-Permissions (wer darf welche Qualität nutzen)
|
|
18
|
+
this.userPermissions = new Map();
|
|
19
|
+
|
|
20
|
+
// Globale Einstellungen
|
|
21
|
+
this.defaultQuality = this.QUALITY_LEVELS.MEDIUM;
|
|
22
|
+
this.maxQualityForFreeUsers = this.QUALITY_LEVELS.MEDIUM;
|
|
23
|
+
this.maxQualityForPremiumUsers = this.QUALITY_LEVELS.MAXIMUM;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Setzt die erlaubte Qualität fßr einen User
|
|
28
|
+
* @param {string} userId - User ID
|
|
29
|
+
* @param {number} maxQuality - Maximale Bitrate (120-360)
|
|
30
|
+
*/
|
|
31
|
+
setUserQuality(userId, maxQuality) {
|
|
32
|
+
const quality = this.validateQuality(maxQuality);
|
|
33
|
+
this.userPermissions.set(userId, quality);
|
|
34
|
+
return quality;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Gibt die erlaubte Qualität fßr einen User zurßck
|
|
39
|
+
* @param {string} userId - User ID
|
|
40
|
+
* @param {boolean} isPremium - Ist der User Premium?
|
|
41
|
+
* @returns {number} Erlaubte Bitrate
|
|
42
|
+
*/
|
|
43
|
+
getUserQuality(userId, isPremium = false) {
|
|
44
|
+
// Wenn User spezifische Permission hat
|
|
45
|
+
if (this.userPermissions.has(userId)) {
|
|
46
|
+
return this.userPermissions.get(userId);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Sonst nach Premium-Status
|
|
50
|
+
return isPremium ? this.maxQualityForPremiumUsers : this.maxQualityForFreeUsers;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Validiert und begrenzt Qualitätswert
|
|
55
|
+
* @param {number} quality - GewĂźnschte Bitrate
|
|
56
|
+
* @returns {number} Validierte Bitrate (120-360)
|
|
57
|
+
*/
|
|
58
|
+
validateQuality(quality) {
|
|
59
|
+
const q = parseInt(quality);
|
|
60
|
+
|
|
61
|
+
if (isNaN(q)) {
|
|
62
|
+
return this.defaultQuality;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Begrenze auf Min/Max
|
|
66
|
+
if (q < this.QUALITY_LEVELS.MINIMUM) {
|
|
67
|
+
return this.QUALITY_LEVELS.MINIMUM;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (q > this.QUALITY_LEVELS.MAXIMUM) {
|
|
71
|
+
return this.QUALITY_LEVELS.MAXIMUM;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return q;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Wählt das beste Audio-Format basierend auf Qualitätseinstellung
|
|
79
|
+
* @param {Array} formats - VerfĂźgbare Formate
|
|
80
|
+
* @param {number} targetQuality - Ziel-Bitrate
|
|
81
|
+
* @returns {Object} Bestes Format
|
|
82
|
+
*/
|
|
83
|
+
selectBestAudioFormat(formats, targetQuality) {
|
|
84
|
+
// Nur Audio-Formate
|
|
85
|
+
const audioFormats = formats.filter(f =>
|
|
86
|
+
f.mimeType && f.mimeType.includes('audio') && f.url
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
if (audioFormats.length === 0) return null;
|
|
90
|
+
|
|
91
|
+
// Sortiere nach Bitrate
|
|
92
|
+
audioFormats.sort((a, b) => {
|
|
93
|
+
const bitrateA = this.getBitrate(a);
|
|
94
|
+
const bitrateB = this.getBitrate(b);
|
|
95
|
+
return Math.abs(bitrateA - targetQuality) - Math.abs(bitrateB - targetQuality);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Finde Format das am nächsten zur Ziel-Qualität ist
|
|
99
|
+
let bestFormat = audioFormats[0];
|
|
100
|
+
|
|
101
|
+
for (const format of audioFormats) {
|
|
102
|
+
const bitrate = this.getBitrate(format);
|
|
103
|
+
|
|
104
|
+
// Wenn Format unter Ziel-Qualität liegt, nehme es
|
|
105
|
+
if (bitrate <= targetQuality && bitrate > this.getBitrate(bestFormat)) {
|
|
106
|
+
bestFormat = format;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return bestFormat;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Extrahiert Bitrate aus Format
|
|
115
|
+
* @param {Object} format - Format-Objekt
|
|
116
|
+
* @returns {number} Bitrate in kbps
|
|
117
|
+
*/
|
|
118
|
+
getBitrate(format) {
|
|
119
|
+
if (format.bitrate) {
|
|
120
|
+
return Math.round(format.bitrate / 1000); // Bits zu kbps
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (format.audioBitrate) {
|
|
124
|
+
return format.audioBitrate;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Fallback: Schätze aus Quality-Label
|
|
128
|
+
if (format.qualityLabel) {
|
|
129
|
+
const match = format.qualityLabel.match(/(\d+)kbps/i);
|
|
130
|
+
if (match) {
|
|
131
|
+
return parseInt(match[1]);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Default: Mittel
|
|
136
|
+
return this.QUALITY_LEVELS.MEDIUM;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Gibt Qualitäts-Label zurßck
|
|
141
|
+
* @param {number} bitrate - Bitrate in kbps
|
|
142
|
+
* @returns {string} Label
|
|
143
|
+
*/
|
|
144
|
+
getQualityLabel(bitrate) {
|
|
145
|
+
if (bitrate >= this.QUALITY_LEVELS.MAXIMUM) return 'đĽ Maximum (360 kbps)';
|
|
146
|
+
if (bitrate >= this.QUALITY_LEVELS.HIGH) return 'â High (256 kbps)';
|
|
147
|
+
if (bitrate >= this.QUALITY_LEVELS.MEDIUM) return 'â
Medium (192 kbps)';
|
|
148
|
+
if (bitrate >= this.QUALITY_LEVELS.LOW) return 'đ Low (128 kbps)';
|
|
149
|
+
return 'đž Minimum (120 kbps)';
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Setzt globale Standard-Qualität
|
|
154
|
+
* @param {number} quality - Bitrate (120-360)
|
|
155
|
+
*/
|
|
156
|
+
setDefaultQuality(quality) {
|
|
157
|
+
this.defaultQuality = this.validateQuality(quality);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Setzt maximale Qualität fßr Free Users
|
|
162
|
+
* @param {number} quality - Bitrate (120-360)
|
|
163
|
+
*/
|
|
164
|
+
setMaxQualityForFreeUsers(quality) {
|
|
165
|
+
this.maxQualityForFreeUsers = this.validateQuality(quality);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Setzt maximale Qualität fßr Premium Users
|
|
170
|
+
* @param {number} quality - Bitrate (120-360)
|
|
171
|
+
*/
|
|
172
|
+
setMaxQualityForPremiumUsers(quality) {
|
|
173
|
+
this.maxQualityForPremiumUsers = this.validateQuality(quality);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Entfernt User-Permission
|
|
178
|
+
* @param {string} userId - User ID
|
|
179
|
+
*/
|
|
180
|
+
removeUserQuality(userId) {
|
|
181
|
+
this.userPermissions.delete(userId);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Gibt alle User-Permissions zurĂźck
|
|
186
|
+
* @returns {Object} User-Permissions
|
|
187
|
+
*/
|
|
188
|
+
getAllUserPermissions() {
|
|
189
|
+
const permissions = {};
|
|
190
|
+
for (const [userId, quality] of this.userPermissions.entries()) {
|
|
191
|
+
permissions[userId] = {
|
|
192
|
+
quality,
|
|
193
|
+
label: this.getQualityLabel(quality)
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
return permissions;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Prßft ob User bestimmte Qualität nutzen darf
|
|
201
|
+
* @param {string} userId - User ID
|
|
202
|
+
* @param {number} requestedQuality - Gewßnschte Qualität
|
|
203
|
+
* @param {boolean} isPremium - Ist Premium User?
|
|
204
|
+
* @returns {boolean} Erlaubt?
|
|
205
|
+
*/
|
|
206
|
+
canUserUseQuality(userId, requestedQuality, isPremium = false) {
|
|
207
|
+
const maxAllowed = this.getUserQuality(userId, isPremium);
|
|
208
|
+
return requestedQuality <= maxAllowed;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Gibt verfßgbare Qualitätsstufen fßr User zurßck
|
|
213
|
+
* @param {string} userId - User ID
|
|
214
|
+
* @param {boolean} isPremium - Ist Premium User?
|
|
215
|
+
* @returns {Array} VerfĂźgbare Stufen
|
|
216
|
+
*/
|
|
217
|
+
getAvailableQualities(userId, isPremium = false) {
|
|
218
|
+
const maxQuality = this.getUserQuality(userId, isPremium);
|
|
219
|
+
const available = [];
|
|
220
|
+
|
|
221
|
+
for (const [name, value] of Object.entries(this.QUALITY_LEVELS)) {
|
|
222
|
+
if (value <= maxQuality) {
|
|
223
|
+
available.push({
|
|
224
|
+
name,
|
|
225
|
+
value,
|
|
226
|
+
label: this.getQualityLabel(value)
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return available;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Exportiert Konfiguration
|
|
236
|
+
* @returns {Object} Konfiguration
|
|
237
|
+
*/
|
|
238
|
+
exportConfig() {
|
|
239
|
+
return {
|
|
240
|
+
defaultQuality: this.defaultQuality,
|
|
241
|
+
maxQualityForFreeUsers: this.maxQualityForFreeUsers,
|
|
242
|
+
maxQualityForPremiumUsers: this.maxQualityForPremiumUsers,
|
|
243
|
+
userPermissions: Object.fromEntries(this.userPermissions)
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Importiert Konfiguration
|
|
249
|
+
* @param {Object} config - Konfiguration
|
|
250
|
+
*/
|
|
251
|
+
importConfig(config) {
|
|
252
|
+
if (config.defaultQuality) {
|
|
253
|
+
this.defaultQuality = this.validateQuality(config.defaultQuality);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (config.maxQualityForFreeUsers) {
|
|
257
|
+
this.maxQualityForFreeUsers = this.validateQuality(config.maxQualityForFreeUsers);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (config.maxQualityForPremiumUsers) {
|
|
261
|
+
this.maxQualityForPremiumUsers = this.validateQuality(config.maxQualityForPremiumUsers);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (config.userPermissions) {
|
|
265
|
+
this.userPermissions = new Map(Object.entries(config.userPermissions));
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
module.exports = QualityManager;
|
|
@@ -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;
|