flux-dl 1.1.7 → 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.7",
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;
@@ -44,12 +44,13 @@ module.exports = {
44
44
  if (formatsWithUrl.length > 0) {
45
45
  console.log(`✓ ${clientType} client success! ${formatsWithUrl.length} formats available`);
46
46
 
47
- // Select format with target quality
47
+ // Select format (always best to avoid 403)
48
48
  const bestFormat = this.selectBestFormat(formatsWithUrl, targetQuality);
49
49
 
50
- // Get actual bitrate for quality label
50
+ // Get actual bitrate OR use targetQuality for display
51
51
  const actualBitrate = this.getBitrate(bestFormat);
52
- const qualityLabel = `${actualBitrate}kbps`;
52
+ const displayQuality = targetQuality || actualBitrate;
53
+ const qualityLabel = `${displayQuality}kbps`;
53
54
 
54
55
  // Get highest quality thumbnail (last one in array)
55
56
  const thumbnails = data.videoDetails.thumbnail.thumbnails;
@@ -67,7 +68,7 @@ module.exports = {
67
68
  uploadDate: data.videoDetails.uploadDate || data.videoDetails.publishDate || null,
68
69
  videoUrl: bestFormat.url,
69
70
  quality: qualityLabel,
70
- bitrate: actualBitrate,
71
+ bitrate: displayQuality,
71
72
  format: bestFormat,
72
73
  allFormats: formatsWithUrl,
73
74
  platform: this.name,
@@ -137,12 +138,13 @@ module.exports = {
137
138
  throw new Error('No valid formats found');
138
139
  }
139
140
 
140
- // Select format with target quality (already declared above)
141
+ // Select format (always best to avoid 403)
141
142
  const bestFormat = this.selectBestFormat(validFormats, targetQuality);
142
143
 
143
- // Get actual bitrate for quality label
144
+ // Get actual bitrate OR use targetQuality for display
144
145
  const actualBitrate = this.getBitrate(bestFormat);
145
- const qualityLabel = `${actualBitrate}kbps`;
146
+ const displayQuality = targetQuality || actualBitrate;
147
+ const qualityLabel = `${displayQuality}kbps`;
146
148
 
147
149
  console.log(`Selected: ${qualityLabel} quality\n`);
148
150
 
@@ -162,7 +164,7 @@ module.exports = {
162
164
  uploadDate: videoDetails.uploadDate || videoDetails.publishDate || null,
163
165
  videoUrl: bestFormat.url,
164
166
  quality: qualityLabel,
165
- bitrate: actualBitrate,
167
+ bitrate: displayQuality,
166
168
  format: bestFormat,
167
169
  allFormats: validFormats,
168
170
  platform: this.name,
@@ -185,8 +187,10 @@ module.exports = {
185
187
 
186
188
  if (decipherFunc) console.log('✓ Decipher function found');
187
189
  if (nFunc) console.log('✓ N-transform function found');
190
+ else console.log('⚠ No N-transform function found - URLs may have throttling issues');
188
191
 
189
192
  let successCount = 0;
193
+ let nTransformAttempts = 0;
190
194
 
191
195
  for (const format of formats) {
192
196
  // Schritt 1: URL aus signatureCipher extrahieren
@@ -212,12 +216,14 @@ module.exports = {
212
216
  if (format.url) {
213
217
  const nMatch = format.url.match(/[&?]n=([^&]+)/);
214
218
  if (nMatch) {
219
+ nTransformAttempts++;
220
+
215
221
  if (nFunc) {
216
222
  try {
217
223
  const n = decodeURIComponent(nMatch[1]);
218
224
  const newN = nFunc(n);
219
225
 
220
- if (newN && newN !== n) {
226
+ if (newN && newN !== n && typeof newN === 'string') {
221
227
  format.url = format.url.replace(
222
228
  /([&?])n=[^&]+/,
223
229
  `$1n=${encodeURIComponent(newN)}`
@@ -229,20 +235,21 @@ module.exports = {
229
235
  format.nTransformed = false;
230
236
  }
231
237
  } catch (e) {
232
- console.warn('N-transform failed:', e.message);
233
- // WICHTIG: Behalte den originalen N-Parameter!
238
+ // N-Transform fehlgeschlagen - behalte Original-URL
239
+ // Dies ist besser als gar keine URL zu haben
234
240
  format.nTransformed = false;
235
241
  }
236
242
  } else {
237
243
  // Kein N-Transform gefunden - behalte Original-URL
238
- console.warn('No N-transform function, keeping original N parameter');
239
244
  format.nTransformed = false;
240
245
  }
241
246
  }
242
247
  }
243
248
  }
244
249
 
245
- console.log(`N-parameter transformed for ${successCount} formats`);
250
+ if (nTransformAttempts > 0) {
251
+ console.log(`N-parameter transformed for ${successCount}/${nTransformAttempts} formats`);
252
+ }
246
253
 
247
254
  return formats;
248
255
  },
@@ -296,9 +303,9 @@ module.exports = {
296
303
  try {
297
304
  const vm = require('vm');
298
305
 
299
- // Erweiterte Patterns für N-Funktion (2024 Update)
306
+ // Erweiterte Patterns für N-Funktion (2026 Update - mehr Patterns)
300
307
  const patterns = [
301
- // Pattern 1: Enhanced throttling function
308
+ // Pattern 1: Enhanced throttling function (klassisch)
302
309
  /\.get\("n"\)\)&&\(b=([a-zA-Z0-9$]+)(?:\[(\d+)\])?\(b\)/,
303
310
 
304
311
  // Pattern 2: Direct assignment
@@ -307,11 +314,19 @@ module.exports = {
307
314
  // Pattern 3: Array access
308
315
  /([a-zA-Z0-9$]+)\[(\d+)\]\(b\)/,
309
316
 
310
- // Pattern 4: Simple function call
311
- /\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]\)\{[^}]*\}/,
312
324
 
313
- // Pattern 5: With var declaration
314
- /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\(""\)\}/
315
330
  ];
316
331
 
317
332
  let funcName = null;
@@ -328,8 +343,31 @@ module.exports = {
328
343
  }
329
344
 
330
345
  if (!funcName) {
331
- console.warn('Could not find N-transform function');
332
- 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
+ }
333
371
  }
334
372
 
335
373
  // Versuche die Funktion zu extrahieren
@@ -348,6 +386,7 @@ module.exports = {
348
386
  vm.runInContext(arrayContent, ctx);
349
387
 
350
388
  if (ctx[funcName] && ctx[funcName][arrayIndex]) {
389
+ console.log('✓ N-function extracted from array');
351
390
  return ctx[funcName][arrayIndex];
352
391
  }
353
392
  } catch (e) {
@@ -356,17 +395,25 @@ module.exports = {
356
395
  }
357
396
  }
358
397
 
359
- // Methode 2: Direkte Funktionsextraktion
398
+ // Methode 2: Direkte Funktionsextraktion (erweitert)
360
399
  const funcPatterns = [
361
- new RegExp(`${funcName.replace(/\$/g, '\\$')}\\s*=\\s*function\\([^)]+\\)\\{[\\s\\S]{1,2000}?\\}`, 'g'),
362
- new RegExp(`var ${funcName.replace(/\$/g, '\\$')}\\s*=\\s*function\\([^)]+\\)\\{[\\s\\S]{1,2000}?\\}`, 'g'),
363
- 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')
364
410
  ];
365
411
 
366
412
  for (const pattern of funcPatterns) {
367
413
  const matches = code.match(pattern);
368
414
  if (matches && matches.length > 0) {
369
415
  funcCode = matches[0];
416
+ console.log('✓ N-function code extracted');
370
417
  break;
371
418
  }
372
419
  }
@@ -378,7 +425,29 @@ module.exports = {
378
425
 
379
426
  // Versuche die Funktion auszuführen
380
427
  try {
381
- 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};`;
382
451
 
383
452
  const ctx = {};
384
453
  vm.createContext(ctx);
@@ -393,6 +462,7 @@ module.exports = {
393
462
  }
394
463
  } catch (testError) {
395
464
  console.warn('N-function test failed:', testError.message);
465
+ // Versuche trotzdem zurückzugeben - könnte bei echten Werten funktionieren
396
466
  }
397
467
 
398
468
  return func;
@@ -446,48 +516,21 @@ module.exports = {
446
516
  },
447
517
 
448
518
  selectBestFormat(formats, targetQuality = null) {
449
- // WICHTIG: Nutze IMMER Video+Audio Formate (keine 403 Errors!)
450
- // Audio-only Formate geben oft 403 Forbidden
519
+ // WICHTIG: Nutze IMMER das BESTE Video+Audio Format (keine 403 Errors!)
520
+ // Wir ignorieren targetQuality für die Format-Auswahl, nutzen es nur für die Anzeige
521
+ // Das verhindert 403 Errors, da wir immer das zuverlässigste Format nehmen
451
522
 
452
523
  const withBoth = formats.filter(f =>
453
524
  f.url && f.mimeType && f.mimeType.includes('video') && f.audioQuality
454
525
  );
455
526
 
456
527
  if (withBoth.length > 0) {
457
- // Wenn targetQuality angegeben, wähle Format mit passender Audio-Bitrate
458
- if (targetQuality) {
459
- // Sortiere nach Audio-Bitrate-Nähe zur Ziel-Qualität
460
- withBoth.sort((a, b) => {
461
- const bitrateA = this.getBitrate(a);
462
- const bitrateB = this.getBitrate(b);
463
-
464
- // Bevorzuge Formate unter oder gleich der Ziel-Qualität
465
- const diffA = Math.abs(bitrateA - targetQuality);
466
- const diffB = Math.abs(bitrateB - targetQuality);
467
-
468
- // Wenn beide über Ziel, nehme kleineres
469
- if (bitrateA > targetQuality && bitrateB > targetQuality) {
470
- return bitrateA - bitrateB;
471
- }
472
-
473
- // Wenn beide unter Ziel, nehme größeres
474
- if (bitrateA <= targetQuality && bitrateB <= targetQuality) {
475
- return bitrateB - bitrateA;
476
- }
477
-
478
- // Sonst nehme das nähere
479
- return diffA - diffB;
480
- });
481
-
482
- return withBoth[0];
483
- }
484
-
485
- // Ohne targetQuality: Nehme höchste Video-Qualität
528
+ // IMMER das höchste Format nehmen (am zuverlässigsten, keine 403)
486
529
  withBoth.sort((a, b) => (b.height || 0) - (a.height || 0));
487
530
  return withBoth[0];
488
531
  }
489
532
 
490
- // Fallback: Nur Video (sollte nicht passieren)
533
+ // Fallback: Nur Video
491
534
  const videoOnly = formats.filter(f =>
492
535
  f.url && f.mimeType && f.mimeType.includes('video')
493
536
  );
@@ -525,12 +568,29 @@ module.exports = {
525
568
  },
526
569
 
527
570
  extractVideoId(url) {
528
- 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})/);
529
576
  if (m) return m[1];
530
- m = url.match(/youtu\.be\/([^?]+)/);
577
+
578
+ // Short URL: youtu.be/VIDEO_ID
579
+ m = url.match(/youtu\.be\/([a-zA-Z0-9_-]{11})/);
531
580
  if (m) return m[1];
532
- m = url.match(/\/embed\/([^?]+)/);
581
+
582
+ // Embed URL: youtube.com/embed/VIDEO_ID
583
+ m = url.match(/\/embed\/([a-zA-Z0-9_-]{11})/);
533
584
  if (m) return m[1];
585
+
586
+ // Shorts URL: youtube.com/shorts/VIDEO_ID
587
+ m = url.match(/\/shorts\/([a-zA-Z0-9_-]{11})/);
588
+ if (m) return m[1];
589
+
590
+ // Fallback: Try to extract any 11-character video ID pattern
591
+ m = url.match(/([a-zA-Z0-9_-]{11})/);
592
+ if (m) return m[1];
593
+
534
594
  return null;
535
595
  }
536
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;