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.
@@ -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;