flux-dl 1.1.9 → 1.1.10

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flux-dl",
3
- "version": "1.1.9",
3
+ "version": "1.1.10",
4
4
  "description": "Leistungsstarke Video-Downloader Library für YouTube, Vimeo und Dailymotion - komplett in JavaScript, keine externen Binaries",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -8,6 +8,7 @@
8
8
  "test-encryption": "node examples/encryption-test.js",
9
9
  "test-platforms": "node examples/test-platforms.js"
10
10
  },
11
+
11
12
  "keywords": [
12
13
  "video",
13
14
  "download",
@@ -38,8 +39,8 @@
38
39
  "node": ">=14.0.0"
39
40
  },
40
41
  "dependencies": {
41
- "axios": "^1.6.0",
42
- "cheerio": "^1.0.0-rc.12",
42
+ "axios": "^1.13.5",
43
+ "cheerio": "^1.2.0",
43
44
  "m3u8-parser": "^7.1.0"
44
45
  },
45
46
  "files": [
@@ -48,3 +49,5 @@
48
49
  "LICENSE"
49
50
  ]
50
51
  }
52
+
53
+
@@ -149,11 +149,14 @@ class VideoDownloader {
149
149
  * Get audio stream without saving to file
150
150
  * Returns a readable stream that can be piped or consumed directly
151
151
  * ULTRA-STABLE: Will never crash, handles all connection errors gracefully
152
+ * Uses InnerTube API (Android client) to avoid 403 errors
152
153
  * @param {string} url - Video URL
153
154
  * @param {function} onProgress - Optional progress callback (percent, downloaded, total)
154
155
  * @returns {Promise<{stream: ReadableStream, info: Object}>}
155
156
  */
156
157
  async getAudioStream(url, onProgress = null) {
158
+ // WICHTIG: Nutze getVideoInfo um InnerTube API zu verwenden (Android Client)
159
+ // Dies vermeidet 403 Fehler komplett!
157
160
  const info = await this.getVideoInfo(url);
158
161
 
159
162
  if (!info.videoUrl) {
@@ -162,9 +165,10 @@ class VideoDownloader {
162
165
 
163
166
  console.log(`🎵 Streaming audio: ${info.title}`);
164
167
  console.log(`📊 Quality: ${info.quality}`);
168
+ console.log(`🤖 Client: ${info.clientUsed || 'unknown'}`);
165
169
 
166
- // Nutze Browser Emulation mit Cookies
167
- const referer = `https://www.youtube.com/watch?v=${info.videoId}`;
170
+ // Nutze Browser Emulation mit Cookies für bessere Kompatibilität
171
+ const referer = info.videoId ? `https://www.youtube.com/watch?v=${info.videoId}` : null;
168
172
 
169
173
  // Wrap onProgress to catch errors
170
174
  const safeOnProgress = onProgress ? (percent, downloaded, total) => {
@@ -202,6 +206,7 @@ class VideoDownloader {
202
206
  // Timeout protection with auto-recovery
203
207
  let lastDataTime = Date.now();
204
208
  let dataReceived = false;
209
+ let bytesReceived = 0;
205
210
 
206
211
  const timeoutCheck = setInterval(() => {
207
212
  const timeSinceLastData = Date.now() - lastDataTime;
@@ -221,10 +226,14 @@ class VideoDownloader {
221
226
  resilientStream.on('data', (chunk) => {
222
227
  lastDataTime = Date.now();
223
228
  dataReceived = true;
229
+ bytesReceived += chunk.length;
224
230
  });
225
231
 
226
232
  resilientStream.on('end', () => {
227
233
  clearInterval(timeoutCheck);
234
+ if (bytesReceived === 0) {
235
+ console.warn(`⚠️ Stream ended with 0 bytes for "${info.title}"`);
236
+ }
228
237
  });
229
238
 
230
239
  resilientStream.on('close', () => {
@@ -252,7 +261,8 @@ class VideoDownloader {
252
261
  stream: resilientStream,
253
262
  info,
254
263
  contentType: 'audio/mpeg',
255
- filename: this.sanitizeFilename(info.title) + '.mp3'
264
+ filename: this.sanitizeFilename(info.title) + '.mp3',
265
+ bytesReceived: () => bytesReceived // Function to check bytes received
256
266
  };
257
267
 
258
268
  } catch (error) {
@@ -277,7 +287,8 @@ class VideoDownloader {
277
287
  info,
278
288
  contentType: 'audio/mpeg',
279
289
  filename: this.sanitizeFilename(info.title) + '.mp3',
280
- error: error.message
290
+ error: error.message,
291
+ bytesReceived: () => 0
281
292
  };
282
293
  }
283
294
  }
@@ -467,6 +478,7 @@ class VideoDownloader {
467
478
 
468
479
  /**
469
480
  * Stream mit User-spezifischer Qualität
481
+ * Uses InnerTube API (Android client) to avoid 403 errors
470
482
  * @param {string} url - Video URL
471
483
  * @param {string} userId - User ID
472
484
  * @param {boolean} isPremium - Ist Premium User?
@@ -478,6 +490,7 @@ class VideoDownloader {
478
490
 
479
491
  console.log(`🎵 Streaming for user ${userId}`);
480
492
  console.log(`📊 Quality: ${this.qualityManager.getQualityLabel(userQuality)}`);
493
+ console.log(`👤 Premium: ${isPremium ? 'Yes' : 'No'}`);
481
494
 
482
495
  // Temporär Qualität setzen
483
496
  const originalQuality = this.options.quality;
@@ -488,7 +501,9 @@ class VideoDownloader {
488
501
  return {
489
502
  ...result,
490
503
  quality: userQuality,
491
- qualityLabel: this.qualityManager.getQualityLabel(userQuality)
504
+ qualityLabel: this.qualityManager.getQualityLabel(userQuality),
505
+ userId: userId,
506
+ isPremium: isPremium
492
507
  };
493
508
  } finally {
494
509
  // Qualität zurücksetzen
@@ -24,7 +24,7 @@ module.exports = {
24
24
  await this.browser.startSession();
25
25
 
26
26
  // Methode 1: InnerTube API mit verschiedenen Clients
27
- const clientPriority = ['android', 'ios', 'tvEmbedded', 'web'];
27
+ const clientPriority = ['android', 'androidMusic', 'ios', 'webEmbedded', 'tvEmbedded', 'androidTv', 'web'];
28
28
 
29
29
  // Get target quality from options
30
30
  const targetQuality = options?.quality || null;
@@ -187,8 +187,10 @@ module.exports = {
187
187
 
188
188
  if (decipherFunc) console.log('✓ Decipher function found');
189
189
  if (nFunc) console.log('✓ N-transform function found');
190
+ else console.log('⚠ No N-transform function found - URLs may have throttling issues');
190
191
 
191
192
  let successCount = 0;
193
+ let nTransformAttempts = 0;
192
194
 
193
195
  for (const format of formats) {
194
196
  // Schritt 1: URL aus signatureCipher extrahieren
@@ -214,12 +216,14 @@ module.exports = {
214
216
  if (format.url) {
215
217
  const nMatch = format.url.match(/[&?]n=([^&]+)/);
216
218
  if (nMatch) {
219
+ nTransformAttempts++;
220
+
217
221
  if (nFunc) {
218
222
  try {
219
223
  const n = decodeURIComponent(nMatch[1]);
220
224
  const newN = nFunc(n);
221
225
 
222
- if (newN && newN !== n) {
226
+ if (newN && newN !== n && typeof newN === 'string') {
223
227
  format.url = format.url.replace(
224
228
  /([&?])n=[^&]+/,
225
229
  `$1n=${encodeURIComponent(newN)}`
@@ -231,20 +235,21 @@ module.exports = {
231
235
  format.nTransformed = false;
232
236
  }
233
237
  } catch (e) {
234
- console.warn('N-transform failed:', e.message);
235
- // WICHTIG: Behalte den originalen N-Parameter!
238
+ // N-Transform fehlgeschlagen - behalte Original-URL
239
+ // Dies ist besser als gar keine URL zu haben
236
240
  format.nTransformed = false;
237
241
  }
238
242
  } else {
239
243
  // Kein N-Transform gefunden - behalte Original-URL
240
- console.warn('No N-transform function, keeping original N parameter');
241
244
  format.nTransformed = false;
242
245
  }
243
246
  }
244
247
  }
245
248
  }
246
249
 
247
- console.log(`N-parameter transformed for ${successCount} formats`);
250
+ if (nTransformAttempts > 0) {
251
+ console.log(`N-parameter transformed for ${successCount}/${nTransformAttempts} formats`);
252
+ }
248
253
 
249
254
  return formats;
250
255
  },
@@ -298,9 +303,9 @@ module.exports = {
298
303
  try {
299
304
  const vm = require('vm');
300
305
 
301
- // Erweiterte Patterns für N-Funktion (2024 Update)
306
+ // Erweiterte Patterns für N-Funktion (2026 Update - mehr Patterns)
302
307
  const patterns = [
303
- // Pattern 1: Enhanced throttling function
308
+ // Pattern 1: Enhanced throttling function (klassisch)
304
309
  /\.get\("n"\)\)&&\(b=([a-zA-Z0-9$]+)(?:\[(\d+)\])?\(b\)/,
305
310
 
306
311
  // Pattern 2: Direct assignment
@@ -309,11 +314,19 @@ module.exports = {
309
314
  // Pattern 3: Array access
310
315
  /([a-zA-Z0-9$]+)\[(\d+)\]\(b\)/,
311
316
 
312
- // Pattern 4: Simple function call
313
- /\b([a-zA-Z0-9$]{2,})\s*=\s*function\([a-zA-Z]\)\{[^}]*\.split\(""\)[^}]*\.join\(""\)\}/,
317
+ // Pattern 4: Neue YouTube Patterns (2025/2026)
318
+ /\.get\("n"\)\)&&\(b=([a-zA-Z0-9$]+)(?:\[(\d+)\]|\(b\))/,
319
+ /b=([a-zA-Z0-9$]+)\(b\).*?\.get\("n"\)/,
320
+ /\.set\("n",([a-zA-Z0-9$]+)(?:\[(\d+)\])?\(/,
321
+
322
+ // Pattern 5: Function in object
323
+ /\{[^}]*n:function\([a-zA-Z]\)\{[^}]*\}/,
314
324
 
315
- // Pattern 5: With var declaration
316
- /var\s+([a-zA-Z0-9$]+)=\{[^}]*\bfunction\([a-zA-Z]\)\{[^}]*\.split\(""\)/
325
+ // Pattern 6: Throttling parameter
326
+ /&&\(b=([a-zA-Z0-9$]+)\[(\d+)\]\(b\)\)/,
327
+
328
+ // Pattern 7: Direct n parameter handling
329
+ /[,;]([a-zA-Z0-9$]+)=function\([a-zA-Z]\)\{[^}]{50,500}?\.split\(""\)[^}]{50,500}?\.join\(""\)\}/
317
330
  ];
318
331
 
319
332
  let funcName = null;
@@ -330,8 +343,31 @@ module.exports = {
330
343
  }
331
344
 
332
345
  if (!funcName) {
333
- console.warn('Could not find N-transform function');
334
- return null;
346
+ // Fallback: Suche nach alternativen Patterns im gesamten Code
347
+ console.warn('Primary N-function patterns failed, trying fallback search...');
348
+
349
+ // Suche nach typischen N-Transform Charakteristiken
350
+ const fallbackPatterns = [
351
+ // Suche nach Funktionen die mit split/join arbeiten (typisch für N-Transform)
352
+ /([a-zA-Z0-9$_]+)\s*=\s*function\([a-zA-Z]\)\{[^}]*\.split\(""\)[^}]*\.reverse\(\)[^}]*\.join\(""\)/,
353
+ /function\s+([a-zA-Z0-9$_]+)\([a-zA-Z]\)\{[^}]*\.split\(""\)[^}]*\.splice\([^}]*\.join\(""\)/,
354
+ // Suche nach throttle/n-parameter bezogenen Funktionen
355
+ /([a-zA-Z0-9$_]+)\s*=\s*function\([a-zA-Z]\)\{[^}]{100,800}?return[^}]*\.join\(""\)\}/
356
+ ];
357
+
358
+ for (let i = 0; i < fallbackPatterns.length; i++) {
359
+ const m = code.match(fallbackPatterns[i]);
360
+ if (m) {
361
+ funcName = m[1];
362
+ console.log(`✓ Found N-function using fallback pattern ${i + 1}: ${funcName}`);
363
+ break;
364
+ }
365
+ }
366
+
367
+ if (!funcName) {
368
+ console.warn('Could not find N-transform function with any pattern');
369
+ return null;
370
+ }
335
371
  }
336
372
 
337
373
  // Versuche die Funktion zu extrahieren
@@ -350,6 +386,7 @@ module.exports = {
350
386
  vm.runInContext(arrayContent, ctx);
351
387
 
352
388
  if (ctx[funcName] && ctx[funcName][arrayIndex]) {
389
+ console.log('✓ N-function extracted from array');
353
390
  return ctx[funcName][arrayIndex];
354
391
  }
355
392
  } catch (e) {
@@ -358,17 +395,25 @@ module.exports = {
358
395
  }
359
396
  }
360
397
 
361
- // Methode 2: Direkte Funktionsextraktion
398
+ // Methode 2: Direkte Funktionsextraktion (erweitert)
362
399
  const funcPatterns = [
363
- new RegExp(`${funcName.replace(/\$/g, '\\$')}\\s*=\\s*function\\([^)]+\\)\\{[\\s\\S]{1,2000}?\\}`, 'g'),
364
- new RegExp(`var ${funcName.replace(/\$/g, '\\$')}\\s*=\\s*function\\([^)]+\\)\\{[\\s\\S]{1,2000}?\\}`, 'g'),
365
- new RegExp(`function ${funcName.replace(/\$/g, '\\$')}\\([^)]+\\)\\{[\\s\\S]{1,2000}?\\}`, 'g')
400
+ // Standard function assignment
401
+ new RegExp(`${funcName.replace(/\$/g, '\\$')}\\s*=\\s*function\\([^)]+\\)\\{[\\s\\S]{1,3000}?\\}`, 'g'),
402
+ new RegExp(`var ${funcName.replace(/\$/g, '\\$')}\\s*=\\s*function\\([^)]+\\)\\{[\\s\\S]{1,3000}?\\}`, 'g'),
403
+ new RegExp(`function ${funcName.replace(/\$/g, '\\$')}\\([^)]+\\)\\{[\\s\\S]{1,3000}?\\}`, 'g'),
404
+
405
+ // Arrow function
406
+ new RegExp(`${funcName.replace(/\$/g, '\\$')}\\s*=\\s*\\([^)]+\\)\\s*=>\\s*\\{[\\s\\S]{1,3000}?\\}`, 'g'),
407
+
408
+ // Const/let declaration
409
+ new RegExp(`(?:const|let)\\s+${funcName.replace(/\$/g, '\\$')}\\s*=\\s*function\\([^)]+\\)\\{[\\s\\S]{1,3000}?\\}`, 'g')
366
410
  ];
367
411
 
368
412
  for (const pattern of funcPatterns) {
369
413
  const matches = code.match(pattern);
370
414
  if (matches && matches.length > 0) {
371
415
  funcCode = matches[0];
416
+ console.log('✓ N-function code extracted');
372
417
  break;
373
418
  }
374
419
  }
@@ -380,7 +425,29 @@ module.exports = {
380
425
 
381
426
  // Versuche die Funktion auszuführen
382
427
  try {
383
- const fullCode = `${funcCode}\n${funcName};`;
428
+ // Extrahiere auch mögliche Abhängigkeiten
429
+ let fullCode = funcCode;
430
+
431
+ // Suche nach referenzierten Variablen/Funktionen im Code
432
+ const references = funcCode.match(/\b([a-zA-Z0-9$_]{1,3})\b(?=\(|\[|\.)/g);
433
+ if (references) {
434
+ const uniqueRefs = [...new Set(references)].filter(ref =>
435
+ ref.length <= 3 && // Typische obfuscated Namen
436
+ !['if', 'for', 'var', 'let', 'new', 'try'].includes(ref)
437
+ );
438
+
439
+ // Versuche diese Referenzen zu finden
440
+ for (const ref of uniqueRefs) {
441
+ const refPattern = new RegExp(`(?:var|const|let|,)\\s*${ref.replace(/\$/g, '\\$')}\\s*=\\s*(?:function|\\{)[\\s\\S]{1,500}?[};]`, 'g');
442
+ const refMatch = code.match(refPattern);
443
+ if (refMatch && refMatch[0]) {
444
+ fullCode = refMatch[0] + '\n' + fullCode;
445
+ console.log(`✓ Found dependency: ${ref}`);
446
+ }
447
+ }
448
+ }
449
+
450
+ fullCode = `${fullCode}\n${funcName};`;
384
451
 
385
452
  const ctx = {};
386
453
  vm.createContext(ctx);
@@ -395,6 +462,7 @@ module.exports = {
395
462
  }
396
463
  } catch (testError) {
397
464
  console.warn('N-function test failed:', testError.message);
465
+ // Versuche trotzdem zurückzugeben - könnte bei echten Werten funktionieren
398
466
  }
399
467
 
400
468
  return func;
@@ -500,12 +568,29 @@ module.exports = {
500
568
  },
501
569
 
502
570
  extractVideoId(url) {
503
- let m = url.match(/[?&]v=([^&]+)/);
571
+ // Clean URL first (remove whitespace, etc.)
572
+ url = url.trim();
573
+
574
+ // Standard watch URL: youtube.com/watch?v=VIDEO_ID
575
+ let m = url.match(/[?&]v=([a-zA-Z0-9_-]{11})/);
576
+ if (m) return m[1];
577
+
578
+ // Short URL: youtu.be/VIDEO_ID
579
+ m = url.match(/youtu\.be\/([a-zA-Z0-9_-]{11})/);
580
+ if (m) return m[1];
581
+
582
+ // Embed URL: youtube.com/embed/VIDEO_ID
583
+ m = url.match(/\/embed\/([a-zA-Z0-9_-]{11})/);
504
584
  if (m) return m[1];
505
- m = url.match(/youtu\.be\/([^?]+)/);
585
+
586
+ // Shorts URL: youtube.com/shorts/VIDEO_ID
587
+ m = url.match(/\/shorts\/([a-zA-Z0-9_-]{11})/);
506
588
  if (m) return m[1];
507
- m = url.match(/\/embed\/([^?]+)/);
589
+
590
+ // Fallback: Try to extract any 11-character video ID pattern
591
+ m = url.match(/([a-zA-Z0-9_-]{11})/);
508
592
  if (m) return m[1];
593
+
509
594
  return null;
510
595
  }
511
596
  };
@@ -50,7 +50,6 @@ class CookieManager {
50
50
  sum + Object.keys(domain).length, 0
51
51
  );
52
52
 
53
- console.log(`✓ Loaded ${cookieCount} cookies from ${filepath}`);
54
53
  return true;
55
54
 
56
55
  } catch (error) {
@@ -2,26 +2,33 @@ const axios = require('axios');
2
2
 
3
3
  class YouTubeInnerTube {
4
4
  constructor() {
5
- // Verschiedene Client-Konfigurationen zum Testen
5
+ // Verschiedene Client-Konfigurationen zum Testen (2026 Update)
6
6
  this.clients = {
7
7
  // Android Client - BESTE Option, keine N-Parameter!
8
8
  android: {
9
9
  clientName: 'ANDROID',
10
- clientVersion: '19.09.37',
10
+ clientVersion: '19.29.37',
11
11
  androidSdkVersion: 34,
12
12
  apiKey: 'AIzaSyA8eiZmM1FaDVjRy-df2KTyQ_vz_yYM39w'
13
13
  },
14
+ // Android Music - Alternative
15
+ androidMusic: {
16
+ clientName: 'ANDROID_MUSIC',
17
+ clientVersion: '7.11.50',
18
+ androidSdkVersion: 34,
19
+ apiKey: 'AIzaSyAOghZGza2MQSZkY_zfZ370N-PUdXEo8AI'
20
+ },
14
21
  // iOS Client
15
22
  ios: {
16
23
  clientName: 'IOS',
17
- clientVersion: '19.09.3',
24
+ clientVersion: '19.29.1',
18
25
  deviceModel: 'iPhone16,2',
19
26
  apiKey: 'AIzaSyB-63vPrdThhKuerbB2N_l7Kwwcxj6yUAc'
20
27
  },
21
28
  // Web Client
22
29
  web: {
23
30
  clientName: 'WEB',
24
- clientVersion: '2.20240304.00.00',
31
+ clientVersion: '2.20260227.01.00',
25
32
  apiKey: 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8'
26
33
  },
27
34
  // TV Embedded - oft weniger geschützt
@@ -29,6 +36,19 @@ class YouTubeInnerTube {
29
36
  clientName: 'TVHTML5_SIMPLY_EMBEDDED_PLAYER',
30
37
  clientVersion: '2.0',
31
38
  apiKey: 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8'
39
+ },
40
+ // Web Embedded - Alternative
41
+ webEmbedded: {
42
+ clientName: 'WEB_EMBEDDED_PLAYER',
43
+ clientVersion: '1.20260227.01.00',
44
+ apiKey: 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8'
45
+ },
46
+ // Android TV
47
+ androidTv: {
48
+ clientName: 'ANDROID_TESTSUITE',
49
+ clientVersion: '1.9',
50
+ androidSdkVersion: 34,
51
+ apiKey: 'AIzaSyA8eiZmM1FaDVjRy-df2KTyQ_vz_yYM39w'
32
52
  }
33
53
  };
34
54
 
@@ -132,10 +152,13 @@ class YouTubeInnerTube {
132
152
 
133
153
  getUserAgent(clientType) {
134
154
  const agents = {
135
- web: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
136
- android: 'com.google.android.youtube/19.09.37 (Linux; U; Android 14; en_US) gzip',
137
- ios: 'com.google.ios.youtube/19.09.3 (iPhone16,2; U; CPU iOS 17_4 like Mac OS X; en_US)',
138
- tvEmbedded: 'Mozilla/5.0 (ChromiumStylePlatform) Cobalt/Version'
155
+ web: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
156
+ android: 'com.google.android.youtube/19.29.37 (Linux; U; Android 14; en_US) gzip',
157
+ androidMusic: 'com.google.android.apps.youtube.music/7.11.50 (Linux; U; Android 14; en_US) gzip',
158
+ ios: 'com.google.ios.youtube/19.29.1 (iPhone16,2; U; CPU iOS 18_0 like Mac OS X; en_US)',
159
+ tvEmbedded: 'Mozilla/5.0 (ChromiumStylePlatform) Cobalt/Version',
160
+ webEmbedded: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
161
+ androidTv: 'com.google.android.youtube/1.9 (Linux; U; Android 14; en_US) gzip'
139
162
  };
140
163
 
141
164
  return agents[clientType] || agents.android;