flux-dl 1.0.0
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/LICENSE +21 -0
- package/README.md +454 -0
- package/package.json +50 -0
- package/src/VideoDownloader.js +197 -0
- package/src/index.js +11 -0
- package/src/platforms/dailymotion.js +71 -0
- package/src/platforms/examplePlatform.js +45 -0
- package/src/platforms/index.js +13 -0
- package/src/platforms/vimeo.js +66 -0
- package/src/platforms/youtube-deno.js +209 -0
- package/src/platforms/youtube.js +449 -0
- package/src/utils/browserEmulation.js +241 -0
- package/src/utils/cookieManager.js +110 -0
- package/src/utils/encryption.js +102 -0
- package/src/utils/requestSpoofing.js +141 -0
- package/src/utils/signatureDecoder.js +164 -0
- package/src/utils/youtubeInnerTube.js +201 -0
- package/src/utils/youtubeParser.js +283 -0
- package/src/utils/youtubeSearch.js +94 -0
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
const YouTubeInnerTube = require('../utils/youtubeInnerTube');
|
|
3
|
+
const BrowserEmulation = require('../utils/browserEmulation');
|
|
4
|
+
|
|
5
|
+
module.exports = {
|
|
6
|
+
name: 'YouTube',
|
|
7
|
+
innerTube: new YouTubeInnerTube(),
|
|
8
|
+
browser: new BrowserEmulation(),
|
|
9
|
+
|
|
10
|
+
canHandle(url) {
|
|
11
|
+
return url.includes('youtube.com') || url.includes('youtu.be');
|
|
12
|
+
},
|
|
13
|
+
|
|
14
|
+
async extractInfo(url, options) {
|
|
15
|
+
try {
|
|
16
|
+
const videoId = this.extractVideoId(url);
|
|
17
|
+
if (!videoId) {
|
|
18
|
+
throw new Error('Invalid YouTube URL');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
console.log(`\n=== Extracting YouTube Video: ${videoId} ===`);
|
|
22
|
+
|
|
23
|
+
// Starte Browser-Session (wie echter User)
|
|
24
|
+
await this.browser.startSession();
|
|
25
|
+
|
|
26
|
+
// Methode 1: InnerTube API mit verschiedenen Clients
|
|
27
|
+
const clientPriority = ['android', 'ios', 'tvEmbedded', 'web'];
|
|
28
|
+
|
|
29
|
+
for (const clientType of clientPriority) {
|
|
30
|
+
try {
|
|
31
|
+
console.log(`[InnerTube] Trying ${clientType} client...`);
|
|
32
|
+
const data = await this.innerTube.getVideoInfo(videoId, clientType);
|
|
33
|
+
|
|
34
|
+
if (data.videoDetails && data.streamingData) {
|
|
35
|
+
const formats = data.streamingData.formats || [];
|
|
36
|
+
const adaptiveFormats = data.streamingData.adaptiveFormats || [];
|
|
37
|
+
const allFormats = [...formats, ...adaptiveFormats];
|
|
38
|
+
|
|
39
|
+
const formatsWithUrl = allFormats.filter(f => f.url);
|
|
40
|
+
|
|
41
|
+
if (formatsWithUrl.length > 0) {
|
|
42
|
+
console.log(`✓ ${clientType} client success! ${formatsWithUrl.length} formats available`);
|
|
43
|
+
|
|
44
|
+
const bestFormat = this.selectBestFormat(formatsWithUrl);
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
title: data.videoDetails.title,
|
|
48
|
+
videoId: videoId,
|
|
49
|
+
duration: parseInt(data.videoDetails.lengthSeconds),
|
|
50
|
+
thumbnail: data.videoDetails.thumbnail.thumbnails[0].url,
|
|
51
|
+
author: data.videoDetails.author,
|
|
52
|
+
viewCount: parseInt(data.videoDetails.viewCount || 0),
|
|
53
|
+
videoUrl: bestFormat.url,
|
|
54
|
+
quality: bestFormat.qualityLabel || bestFormat.quality || 'unknown',
|
|
55
|
+
format: bestFormat,
|
|
56
|
+
allFormats: formatsWithUrl,
|
|
57
|
+
platform: this.name,
|
|
58
|
+
clientUsed: clientType
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
} catch (innerTubeError) {
|
|
63
|
+
console.log(`✗ ${clientType} client failed: ${innerTubeError.message}`);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Methode 2: Web-Scraping (Fallback)
|
|
69
|
+
console.log('[Web Scraping] Trying web scraping method...');
|
|
70
|
+
|
|
71
|
+
const watchUrl = `https://www.youtube.com/watch?v=${videoId}`;
|
|
72
|
+
|
|
73
|
+
await this.browser.humanDelay(800, 1500);
|
|
74
|
+
|
|
75
|
+
const response = await this.browser.makeRequest(watchUrl, {
|
|
76
|
+
referer: 'https://www.youtube.com/'
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const html = response.data;
|
|
80
|
+
const playerResponse = this.extractPlayerResponse(html);
|
|
81
|
+
|
|
82
|
+
if (!playerResponse || !playerResponse.videoDetails) {
|
|
83
|
+
throw new Error('Could not extract video details');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const videoDetails = playerResponse.videoDetails;
|
|
87
|
+
const streamingData = playerResponse.streamingData;
|
|
88
|
+
|
|
89
|
+
if (!streamingData) {
|
|
90
|
+
throw new Error('No streaming data available');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let allFormats = [
|
|
94
|
+
...(streamingData.formats || []),
|
|
95
|
+
...(streamingData.adaptiveFormats || [])
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
console.log(`Found ${allFormats.length} total formats`);
|
|
99
|
+
|
|
100
|
+
// Entschlüsselung falls nötig
|
|
101
|
+
const needsDecipher = allFormats.some(f => !f.url);
|
|
102
|
+
|
|
103
|
+
if (needsDecipher) {
|
|
104
|
+
console.log('[Decryption] Decrypting signatures...');
|
|
105
|
+
|
|
106
|
+
const playerUrl = this.extractPlayerUrl(html);
|
|
107
|
+
if (!playerUrl) {
|
|
108
|
+
throw new Error('Could not find player URL');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
await this.browser.humanDelay(300, 700);
|
|
112
|
+
|
|
113
|
+
const playerCode = await this.loadPlayerCode(playerUrl);
|
|
114
|
+
allFormats = this.decipherAllFormats(allFormats, playerCode);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const validFormats = allFormats.filter(f => f.url);
|
|
118
|
+
console.log(`✓ ${validFormats.length} formats ready for download`);
|
|
119
|
+
|
|
120
|
+
if (validFormats.length === 0) {
|
|
121
|
+
throw new Error('No valid formats found');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const bestFormat = this.selectBestFormat(validFormats);
|
|
125
|
+
console.log(`Selected: ${bestFormat.qualityLabel || 'unknown'} quality\n`);
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
title: videoDetails.title,
|
|
129
|
+
videoId: videoId,
|
|
130
|
+
duration: parseInt(videoDetails.lengthSeconds),
|
|
131
|
+
thumbnail: videoDetails.thumbnail.thumbnails[0].url,
|
|
132
|
+
author: videoDetails.author,
|
|
133
|
+
viewCount: parseInt(videoDetails.viewCount || 0),
|
|
134
|
+
videoUrl: bestFormat.url,
|
|
135
|
+
quality: bestFormat.qualityLabel || 'unknown',
|
|
136
|
+
format: bestFormat,
|
|
137
|
+
allFormats: validFormats,
|
|
138
|
+
platform: this.name,
|
|
139
|
+
clientUsed: 'web-scraping'
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
} catch (error) {
|
|
143
|
+
throw new Error(`YouTube extraction failed: ${error.message}`);
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
decipherAllFormats(formats, playerCode) {
|
|
148
|
+
const vm = require('vm');
|
|
149
|
+
|
|
150
|
+
console.log('Extracting decipher and N-transform functions...');
|
|
151
|
+
|
|
152
|
+
// Extrahiere beide Funktionen
|
|
153
|
+
const decipherFunc = this.extractDecipherFunc(playerCode);
|
|
154
|
+
const nFunc = this.extractNFunc(playerCode);
|
|
155
|
+
|
|
156
|
+
if (decipherFunc) console.log('✓ Decipher function found');
|
|
157
|
+
if (nFunc) console.log('✓ N-transform function found');
|
|
158
|
+
|
|
159
|
+
let successCount = 0;
|
|
160
|
+
|
|
161
|
+
for (const format of formats) {
|
|
162
|
+
// Schritt 1: URL aus signatureCipher extrahieren
|
|
163
|
+
if (!format.url && (format.signatureCipher || format.cipher)) {
|
|
164
|
+
const params = new URLSearchParams(format.signatureCipher || format.cipher);
|
|
165
|
+
let url = params.get('url');
|
|
166
|
+
const s = params.get('s');
|
|
167
|
+
const sp = params.get('sp') || 'signature';
|
|
168
|
+
|
|
169
|
+
if (s && decipherFunc) {
|
|
170
|
+
try {
|
|
171
|
+
const sig = decipherFunc(s);
|
|
172
|
+
url = `${url}&${sp}=${encodeURIComponent(sig)}`;
|
|
173
|
+
} catch (e) {
|
|
174
|
+
console.warn('Signature decipher failed:', e.message);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
format.url = url;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Schritt 2: N-Parameter transformieren (KRITISCH!)
|
|
182
|
+
if (format.url) {
|
|
183
|
+
const nMatch = format.url.match(/[&?]n=([^&]+)/);
|
|
184
|
+
if (nMatch) {
|
|
185
|
+
if (nFunc) {
|
|
186
|
+
try {
|
|
187
|
+
const n = decodeURIComponent(nMatch[1]);
|
|
188
|
+
const newN = nFunc(n);
|
|
189
|
+
|
|
190
|
+
if (newN && newN !== n) {
|
|
191
|
+
format.url = format.url.replace(
|
|
192
|
+
/([&?])n=[^&]+/,
|
|
193
|
+
`$1n=${encodeURIComponent(newN)}`
|
|
194
|
+
);
|
|
195
|
+
format.nTransformed = true;
|
|
196
|
+
successCount++;
|
|
197
|
+
} else {
|
|
198
|
+
// Transformation gab gleichen Wert zurück - behalte Original
|
|
199
|
+
format.nTransformed = false;
|
|
200
|
+
}
|
|
201
|
+
} catch (e) {
|
|
202
|
+
console.warn('N-transform failed:', e.message);
|
|
203
|
+
// WICHTIG: Behalte den originalen N-Parameter!
|
|
204
|
+
format.nTransformed = false;
|
|
205
|
+
}
|
|
206
|
+
} else {
|
|
207
|
+
// Kein N-Transform gefunden - behalte Original-URL
|
|
208
|
+
console.warn('No N-transform function, keeping original N parameter');
|
|
209
|
+
format.nTransformed = false;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
console.log(`N-parameter transformed for ${successCount} formats`);
|
|
216
|
+
|
|
217
|
+
return formats;
|
|
218
|
+
},
|
|
219
|
+
|
|
220
|
+
extractDecipherFunc(code) {
|
|
221
|
+
try {
|
|
222
|
+
const vm = require('vm');
|
|
223
|
+
|
|
224
|
+
// Finde Funktion
|
|
225
|
+
const patterns = [
|
|
226
|
+
/([a-zA-Z0-9$]+)=function\([a-zA-Z]\)\{[a-zA-Z]=\1\.split\(""\)/,
|
|
227
|
+
/\b([a-zA-Z0-9$]{2,})\s*=\s*function\(\s*a\s*\)\s*\{\s*a\s*=\s*a\.split\(\s*""\s*\)/
|
|
228
|
+
];
|
|
229
|
+
|
|
230
|
+
let funcName = null;
|
|
231
|
+
for (const p of patterns) {
|
|
232
|
+
const m = code.match(p);
|
|
233
|
+
if (m) {
|
|
234
|
+
funcName = m[1];
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (!funcName) return null;
|
|
240
|
+
|
|
241
|
+
// Extrahiere Funktion und Helper
|
|
242
|
+
const funcRe = new RegExp(`${funcName.replace(/\$/g, '\\$')}=function\\([a-zA-Z]\\)\\{[^}]+\\}`);
|
|
243
|
+
const funcMatch = code.match(funcRe);
|
|
244
|
+
if (!funcMatch) return null;
|
|
245
|
+
|
|
246
|
+
const funcBody = funcMatch[0];
|
|
247
|
+
const helperMatch = funcBody.match(/;([a-zA-Z0-9$]+)\./);
|
|
248
|
+
if (!helperMatch) return null;
|
|
249
|
+
|
|
250
|
+
const helperName = helperMatch[1];
|
|
251
|
+
const helperRe = new RegExp(`var ${helperName.replace(/\$/g, '\\$')}=\\{[\\s\\S]+?\\}\\};`);
|
|
252
|
+
const helperMatch2 = code.match(helperRe);
|
|
253
|
+
if (!helperMatch2) return null;
|
|
254
|
+
|
|
255
|
+
const fullCode = `${helperMatch2[0]}\n${funcBody}\n${funcName};`;
|
|
256
|
+
|
|
257
|
+
const ctx = {};
|
|
258
|
+
vm.createContext(ctx);
|
|
259
|
+
return vm.runInContext(fullCode, ctx);
|
|
260
|
+
} catch (e) {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
|
|
265
|
+
extractNFunc(code) {
|
|
266
|
+
try {
|
|
267
|
+
const vm = require('vm');
|
|
268
|
+
|
|
269
|
+
// Erweiterte Patterns für N-Funktion (2024 Update)
|
|
270
|
+
const patterns = [
|
|
271
|
+
// Pattern 1: Enhanced throttling function
|
|
272
|
+
/\.get\("n"\)\)&&\(b=([a-zA-Z0-9$]+)(?:\[(\d+)\])?\(b\)/,
|
|
273
|
+
|
|
274
|
+
// Pattern 2: Direct assignment
|
|
275
|
+
/&&\(b=([a-zA-Z0-9$]+)\(decodeURIComponent\(b\)\)\)/,
|
|
276
|
+
|
|
277
|
+
// Pattern 3: Array access
|
|
278
|
+
/([a-zA-Z0-9$]+)\[(\d+)\]\(b\)/,
|
|
279
|
+
|
|
280
|
+
// Pattern 4: Simple function call
|
|
281
|
+
/\b([a-zA-Z0-9$]{2,})\s*=\s*function\([a-zA-Z]\)\{[^}]*\.split\(""\)[^}]*\.join\(""\)\}/,
|
|
282
|
+
|
|
283
|
+
// Pattern 5: With var declaration
|
|
284
|
+
/var\s+([a-zA-Z0-9$]+)=\{[^}]*\bfunction\([a-zA-Z]\)\{[^}]*\.split\(""\)/
|
|
285
|
+
];
|
|
286
|
+
|
|
287
|
+
let funcName = null;
|
|
288
|
+
let arrayIndex = null;
|
|
289
|
+
|
|
290
|
+
for (let i = 0; i < patterns.length; i++) {
|
|
291
|
+
const m = code.match(patterns[i]);
|
|
292
|
+
if (m) {
|
|
293
|
+
funcName = m[1];
|
|
294
|
+
arrayIndex = m[2] || null;
|
|
295
|
+
console.log(`✓ Found N-function pattern ${i + 1}: ${funcName}${arrayIndex ? `[${arrayIndex}]` : ''}`);
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (!funcName) {
|
|
301
|
+
console.warn('Could not find N-transform function');
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Versuche die Funktion zu extrahieren
|
|
306
|
+
let funcCode = null;
|
|
307
|
+
|
|
308
|
+
// Methode 1: Array-Zugriff (z.B. yt.a[42])
|
|
309
|
+
if (arrayIndex) {
|
|
310
|
+
const arrayPattern = new RegExp(`var ${funcName.replace(/\$/g, '\\$')}=\\[([\\s\\S]*?)\\];`, 'g');
|
|
311
|
+
const arrayMatch = code.match(arrayPattern);
|
|
312
|
+
|
|
313
|
+
if (arrayMatch) {
|
|
314
|
+
try {
|
|
315
|
+
const arrayContent = arrayMatch[0];
|
|
316
|
+
const ctx = {};
|
|
317
|
+
vm.createContext(ctx);
|
|
318
|
+
vm.runInContext(arrayContent, ctx);
|
|
319
|
+
|
|
320
|
+
if (ctx[funcName] && ctx[funcName][arrayIndex]) {
|
|
321
|
+
return ctx[funcName][arrayIndex];
|
|
322
|
+
}
|
|
323
|
+
} catch (e) {
|
|
324
|
+
console.warn('Array extraction failed:', e.message);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Methode 2: Direkte Funktionsextraktion
|
|
330
|
+
const funcPatterns = [
|
|
331
|
+
new RegExp(`${funcName.replace(/\$/g, '\\$')}\\s*=\\s*function\\([^)]+\\)\\{[\\s\\S]{1,2000}?\\}`, 'g'),
|
|
332
|
+
new RegExp(`var ${funcName.replace(/\$/g, '\\$')}\\s*=\\s*function\\([^)]+\\)\\{[\\s\\S]{1,2000}?\\}`, 'g'),
|
|
333
|
+
new RegExp(`function ${funcName.replace(/\$/g, '\\$')}\\([^)]+\\)\\{[\\s\\S]{1,2000}?\\}`, 'g')
|
|
334
|
+
];
|
|
335
|
+
|
|
336
|
+
for (const pattern of funcPatterns) {
|
|
337
|
+
const matches = code.match(pattern);
|
|
338
|
+
if (matches && matches.length > 0) {
|
|
339
|
+
funcCode = matches[0];
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (!funcCode) {
|
|
345
|
+
console.warn('Could not extract N-function body');
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Versuche die Funktion auszuführen
|
|
350
|
+
try {
|
|
351
|
+
const fullCode = `${funcCode}\n${funcName};`;
|
|
352
|
+
|
|
353
|
+
const ctx = {};
|
|
354
|
+
vm.createContext(ctx);
|
|
355
|
+
const func = vm.runInContext(fullCode, ctx);
|
|
356
|
+
|
|
357
|
+
// Teste die Funktion
|
|
358
|
+
try {
|
|
359
|
+
const testResult = func('test_string_123');
|
|
360
|
+
if (testResult && typeof testResult === 'string') {
|
|
361
|
+
console.log('✓ N-transform function validated successfully');
|
|
362
|
+
return func;
|
|
363
|
+
}
|
|
364
|
+
} catch (testError) {
|
|
365
|
+
console.warn('N-function test failed:', testError.message);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return func;
|
|
369
|
+
} catch (vmError) {
|
|
370
|
+
console.warn('Failed to execute N-function:', vmError.message);
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
} catch (e) {
|
|
375
|
+
console.warn('N-function extraction error:', e.message);
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
},
|
|
379
|
+
|
|
380
|
+
extractPlayerResponse(html) {
|
|
381
|
+
const patterns = [
|
|
382
|
+
/var ytInitialPlayerResponse\s*=\s*({.+?});/,
|
|
383
|
+
/ytInitialPlayerResponse\s*=\s*({.+?});/
|
|
384
|
+
];
|
|
385
|
+
|
|
386
|
+
for (const p of patterns) {
|
|
387
|
+
const m = html.match(p);
|
|
388
|
+
if (m) {
|
|
389
|
+
try {
|
|
390
|
+
return JSON.parse(m[1]);
|
|
391
|
+
} catch (e) {
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return null;
|
|
397
|
+
},
|
|
398
|
+
|
|
399
|
+
extractPlayerUrl(html) {
|
|
400
|
+
const m = html.match(/"jsUrl":"([^"]+)"/);
|
|
401
|
+
if (!m) return null;
|
|
402
|
+
|
|
403
|
+
let url = m[1].replace(/\\\//g, '/');
|
|
404
|
+
if (url.startsWith('//')) url = 'https:' + url;
|
|
405
|
+
else if (url.startsWith('/')) url = 'https://www.youtube.com' + url;
|
|
406
|
+
|
|
407
|
+
return url;
|
|
408
|
+
},
|
|
409
|
+
|
|
410
|
+
async loadPlayerCode(url) {
|
|
411
|
+
const res = await axios.get(url, {
|
|
412
|
+
headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' },
|
|
413
|
+
httpsAgent: new (require('https').Agent)({ rejectUnauthorized: false })
|
|
414
|
+
});
|
|
415
|
+
return res.data;
|
|
416
|
+
},
|
|
417
|
+
|
|
418
|
+
selectBestFormat(formats) {
|
|
419
|
+
// Mit Audio + Video
|
|
420
|
+
const withBoth = formats.filter(f =>
|
|
421
|
+
f.url && f.mimeType && f.mimeType.includes('video') && f.audioQuality
|
|
422
|
+
);
|
|
423
|
+
if (withBoth.length > 0) {
|
|
424
|
+
withBoth.sort((a, b) => (b.height || 0) - (a.height || 0));
|
|
425
|
+
return withBoth[0];
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Nur Video
|
|
429
|
+
const videoOnly = formats.filter(f =>
|
|
430
|
+
f.url && f.mimeType && f.mimeType.includes('video')
|
|
431
|
+
);
|
|
432
|
+
if (videoOnly.length > 0) {
|
|
433
|
+
videoOnly.sort((a, b) => (b.height || 0) - (a.height || 0));
|
|
434
|
+
return videoOnly[0];
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return formats[0];
|
|
438
|
+
},
|
|
439
|
+
|
|
440
|
+
extractVideoId(url) {
|
|
441
|
+
let m = url.match(/[?&]v=([^&]+)/);
|
|
442
|
+
if (m) return m[1];
|
|
443
|
+
m = url.match(/youtu\.be\/([^?]+)/);
|
|
444
|
+
if (m) return m[1];
|
|
445
|
+
m = url.match(/\/embed\/([^?]+)/);
|
|
446
|
+
if (m) return m[1];
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
};
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
const CookieManager = require('./cookieManager');
|
|
3
|
+
|
|
4
|
+
class BrowserEmulation {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.cookies = {};
|
|
7
|
+
this.sessionStarted = false;
|
|
8
|
+
this.lastRequestTime = 0;
|
|
9
|
+
this.cookieManager = new CookieManager();
|
|
10
|
+
|
|
11
|
+
// Versuche Cookies aus Datei zu laden
|
|
12
|
+
this.cookieManager.loadFromFile('cookies.txt');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Simuliere echten Browser-Start
|
|
16
|
+
async startSession() {
|
|
17
|
+
if (this.sessionStarted) return;
|
|
18
|
+
|
|
19
|
+
console.log('Starting browser session...');
|
|
20
|
+
|
|
21
|
+
// Wenn wir Cookies aus Datei haben, überspringe Homepage-Laden
|
|
22
|
+
if (this.cookieManager.hasCookies()) {
|
|
23
|
+
console.log('✓ Using cookies from cookies.txt (authenticated session)');
|
|
24
|
+
this.sessionStarted = true;
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Schritt 1: Lade YouTube Homepage (wie echter User)
|
|
29
|
+
try {
|
|
30
|
+
const response = await axios.get('https://www.youtube.com', {
|
|
31
|
+
headers: this.getInitialHeaders(),
|
|
32
|
+
httpsAgent: new (require('https').Agent)({
|
|
33
|
+
rejectUnauthorized: false
|
|
34
|
+
})
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Extrahiere Cookies
|
|
38
|
+
if (response.headers['set-cookie']) {
|
|
39
|
+
this.parseCookies(response.headers['set-cookie']);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Warte wie echter User
|
|
43
|
+
await this.humanDelay(500, 1500);
|
|
44
|
+
|
|
45
|
+
this.sessionStarted = true;
|
|
46
|
+
console.log('✓ Browser session established');
|
|
47
|
+
} catch (error) {
|
|
48
|
+
console.warn('Session start failed, continuing anyway...');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
parseCookies(setCookieHeaders) {
|
|
53
|
+
for (const cookie of setCookieHeaders) {
|
|
54
|
+
const parts = cookie.split(';')[0].split('=');
|
|
55
|
+
if (parts.length === 2) {
|
|
56
|
+
this.cookies[parts[0]] = parts[1];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
getCookieString() {
|
|
62
|
+
// Kombiniere Session-Cookies und geladene Cookies
|
|
63
|
+
const sessionCookies = Object.entries(this.cookies)
|
|
64
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
65
|
+
.join('; ');
|
|
66
|
+
|
|
67
|
+
const fileCookies = this.cookieManager.getCookieString('youtube.com');
|
|
68
|
+
|
|
69
|
+
if (sessionCookies && fileCookies) {
|
|
70
|
+
return `${fileCookies}; ${sessionCookies}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return fileCookies || sessionCookies;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
getInitialHeaders() {
|
|
77
|
+
return {
|
|
78
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
79
|
+
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
|
|
80
|
+
'Accept-Language': 'en-US,en;q=0.9',
|
|
81
|
+
'Accept-Encoding': 'gzip, deflate, br',
|
|
82
|
+
'Connection': 'keep-alive',
|
|
83
|
+
'Upgrade-Insecure-Requests': '1',
|
|
84
|
+
'Sec-Fetch-Dest': 'document',
|
|
85
|
+
'Sec-Fetch-Mode': 'navigate',
|
|
86
|
+
'Sec-Fetch-Site': 'none',
|
|
87
|
+
'Sec-Fetch-User': '?1',
|
|
88
|
+
'sec-ch-ua': '"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"',
|
|
89
|
+
'sec-ch-ua-mobile': '?0',
|
|
90
|
+
'sec-ch-ua-platform': '"Windows"',
|
|
91
|
+
'Cache-Control': 'max-age=0'
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
getVideoPageHeaders(referer = null) {
|
|
96
|
+
const headers = {
|
|
97
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
98
|
+
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
|
|
99
|
+
'Accept-Language': 'en-US,en;q=0.9',
|
|
100
|
+
'Accept-Encoding': 'gzip, deflate, br',
|
|
101
|
+
'Connection': 'keep-alive',
|
|
102
|
+
'Upgrade-Insecure-Requests': '1',
|
|
103
|
+
'Sec-Fetch-Dest': 'document',
|
|
104
|
+
'Sec-Fetch-Mode': 'navigate',
|
|
105
|
+
'Sec-Fetch-Site': referer ? 'same-origin' : 'none',
|
|
106
|
+
'Sec-Fetch-User': '?1',
|
|
107
|
+
'sec-ch-ua': '"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"',
|
|
108
|
+
'sec-ch-ua-mobile': '?0',
|
|
109
|
+
'sec-ch-ua-platform': '"Windows"',
|
|
110
|
+
'Cache-Control': 'max-age=0'
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
if (referer) {
|
|
114
|
+
headers['Referer'] = referer;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const cookieStr = this.getCookieString();
|
|
118
|
+
if (cookieStr) {
|
|
119
|
+
headers['Cookie'] = cookieStr;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return headers;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
getVideoDownloadHeaders(videoUrl, referer) {
|
|
126
|
+
// KRITISCH: Range-Header für YouTube Downloads
|
|
127
|
+
const headers = {
|
|
128
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
129
|
+
'Accept': '*/*',
|
|
130
|
+
'Accept-Language': 'en-US,en;q=0.9',
|
|
131
|
+
'Connection': 'keep-alive',
|
|
132
|
+
'Range': 'bytes=0-',
|
|
133
|
+
'Referer': referer,
|
|
134
|
+
'Sec-Fetch-Dest': 'video',
|
|
135
|
+
'Sec-Fetch-Mode': 'no-cors',
|
|
136
|
+
'Sec-Fetch-Site': 'cross-site',
|
|
137
|
+
'sec-ch-ua': '"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"',
|
|
138
|
+
'sec-ch-ua-mobile': '?0',
|
|
139
|
+
'sec-ch-ua-platform': '"Windows"'
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const cookieStr = this.getCookieString();
|
|
143
|
+
if (cookieStr) {
|
|
144
|
+
headers['Cookie'] = cookieStr;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return headers;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Simuliere menschliche Verzögerungen
|
|
151
|
+
async humanDelay(min = 300, max = 800) {
|
|
152
|
+
const delay = Math.floor(Math.random() * (max - min + 1)) + min;
|
|
153
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Rate-Limiting wie echter User
|
|
157
|
+
async respectRateLimit() {
|
|
158
|
+
const now = Date.now();
|
|
159
|
+
const timeSinceLastRequest = now - this.lastRequestTime;
|
|
160
|
+
|
|
161
|
+
// Mindestens 200ms zwischen Requests
|
|
162
|
+
if (timeSinceLastRequest < 200) {
|
|
163
|
+
await new Promise(resolve => setTimeout(resolve, 200 - timeSinceLastRequest));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
this.lastRequestTime = Date.now();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async makeRequest(url, options = {}) {
|
|
170
|
+
await this.respectRateLimit();
|
|
171
|
+
|
|
172
|
+
const config = {
|
|
173
|
+
url: url,
|
|
174
|
+
method: options.method || 'GET',
|
|
175
|
+
headers: options.headers || this.getVideoPageHeaders(options.referer),
|
|
176
|
+
timeout: options.timeout || 30000,
|
|
177
|
+
httpsAgent: new (require('https').Agent)({
|
|
178
|
+
rejectUnauthorized: false
|
|
179
|
+
}),
|
|
180
|
+
maxRedirects: 5,
|
|
181
|
+
validateStatus: (status) => status < 500
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
if (options.responseType) {
|
|
185
|
+
config.responseType = options.responseType;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const response = await axios(config);
|
|
189
|
+
|
|
190
|
+
// Speichere neue Cookies
|
|
191
|
+
if (response.headers['set-cookie']) {
|
|
192
|
+
this.parseCookies(response.headers['set-cookie']);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return response;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async downloadStream(url, referer, onProgress) {
|
|
199
|
+
await this.respectRateLimit();
|
|
200
|
+
|
|
201
|
+
const headers = this.getVideoDownloadHeaders(url, referer);
|
|
202
|
+
|
|
203
|
+
const config = {
|
|
204
|
+
url: url,
|
|
205
|
+
method: 'GET',
|
|
206
|
+
headers: headers,
|
|
207
|
+
responseType: 'stream',
|
|
208
|
+
httpsAgent: new (require('https').Agent)({
|
|
209
|
+
rejectUnauthorized: false
|
|
210
|
+
}),
|
|
211
|
+
maxRedirects: 10,
|
|
212
|
+
timeout: 0,
|
|
213
|
+
decompress: false, // WICHTIG: Keine automatische Dekompression
|
|
214
|
+
validateStatus: (status) => status >= 200 && status < 400
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const response = await axios(config);
|
|
218
|
+
|
|
219
|
+
if (response.status === 403) {
|
|
220
|
+
// Nur loggen, nicht werfen - manchmal funktioniert es trotzdem
|
|
221
|
+
console.warn('Warning: Received 403 status, but continuing...');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (onProgress) {
|
|
225
|
+
const totalLength = response.headers['content-length'];
|
|
226
|
+
let downloadedLength = 0;
|
|
227
|
+
|
|
228
|
+
response.data.on('data', (chunk) => {
|
|
229
|
+
downloadedLength += chunk.length;
|
|
230
|
+
if (totalLength) {
|
|
231
|
+
const percent = Math.round((downloadedLength / totalLength) * 100);
|
|
232
|
+
onProgress(percent, downloadedLength, totalLength);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return response.data;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
module.exports = BrowserEmulation;
|