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 +6 -3
- package/src/VideoDownloader.js +20 -5
- package/src/platforms/youtube.js +122 -62
- package/src/utils/cookieManager.js +0 -1
- package/src/utils/youtubeInnerTube.js +31 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flux-dl",
|
|
3
|
-
"version": "1.1.
|
|
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.
|
|
42
|
-
"cheerio": "^1.
|
|
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
|
+
|
package/src/VideoDownloader.js
CHANGED
|
@@ -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
|
package/src/platforms/youtube.js
CHANGED
|
@@ -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
|
|
47
|
+
// Select format (always best to avoid 403)
|
|
48
48
|
const bestFormat = this.selectBestFormat(formatsWithUrl, targetQuality);
|
|
49
49
|
|
|
50
|
-
// Get actual bitrate for
|
|
50
|
+
// Get actual bitrate OR use targetQuality for display
|
|
51
51
|
const actualBitrate = this.getBitrate(bestFormat);
|
|
52
|
-
const
|
|
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:
|
|
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
|
|
141
|
+
// Select format (always best to avoid 403)
|
|
141
142
|
const bestFormat = this.selectBestFormat(validFormats, targetQuality);
|
|
142
143
|
|
|
143
|
-
// Get actual bitrate for
|
|
144
|
+
// Get actual bitrate OR use targetQuality for display
|
|
144
145
|
const actualBitrate = this.getBitrate(bestFormat);
|
|
145
|
-
const
|
|
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:
|
|
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
|
-
|
|
233
|
-
//
|
|
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
|
-
|
|
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 (
|
|
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:
|
|
311
|
-
|
|
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
|
|
314
|
-
|
|
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
|
-
|
|
332
|
-
|
|
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
|
-
|
|
362
|
-
new RegExp(
|
|
363
|
-
new RegExp(`
|
|
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
|
-
|
|
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
|
|
450
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
};
|
|
@@ -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.
|
|
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.
|
|
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.
|
|
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/
|
|
136
|
-
android: 'com.google.android.youtube/19.
|
|
137
|
-
|
|
138
|
-
|
|
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;
|