frostpv 1.0.9 → 1.0.11
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/index.js +117 -204
- package/package.json +3 -3
package/index.js
CHANGED
|
@@ -4,7 +4,7 @@ const path = require("path");
|
|
|
4
4
|
const os = require("os");
|
|
5
5
|
const FormData = require("form-data");
|
|
6
6
|
const crypto = require("crypto");
|
|
7
|
-
const { igdl, ttdl, fbdown, mediafire, capcut, gdrive, pinterest
|
|
7
|
+
const { igdl, ttdl, fbdown, mediafire, capcut, gdrive, pinterest } = require("btch-downloader");
|
|
8
8
|
const { TwitterDL } = require("twitter-downloader");
|
|
9
9
|
const btch = require("btch-downloader");
|
|
10
10
|
const btchOld = require("btch-downloader-old");
|
|
@@ -21,10 +21,10 @@ ffmpeg.setFfmpegPath(pathToFfmpeg.ffmpegPath);
|
|
|
21
21
|
|
|
22
22
|
// Output directory is the caller's working directory (keeps files next to the running app)
|
|
23
23
|
const OUTPUT_DIR = process.cwd();
|
|
24
|
-
try { fs.mkdirSync(OUTPUT_DIR, { recursive: true }); } catch (_) {
|
|
24
|
+
try { fs.mkdirSync(OUTPUT_DIR, { recursive: true }); } catch (_) {}
|
|
25
25
|
// Keep an OS temp directory for any future transient needs
|
|
26
26
|
const TEMP_DIR = path.join(os.tmpdir(), "downloader-dl-bot");
|
|
27
|
-
try { fs.mkdirSync(TEMP_DIR, { recursive: true }); } catch (_) {
|
|
27
|
+
try { fs.mkdirSync(TEMP_DIR, { recursive: true }); } catch (_) {}
|
|
28
28
|
// TTL (minutes) for files in OUTPUT_DIR (audio/video) before they are pruned
|
|
29
29
|
const OUTPUT_RETENTION_MIN = Number(process.env.OUTPUT_RETENTION_MIN || 5);
|
|
30
30
|
|
|
@@ -58,13 +58,7 @@ const videoPlatforms = [
|
|
|
58
58
|
"https://twitter.com",
|
|
59
59
|
"https://www.twitter.com",
|
|
60
60
|
"https://vm.tiktok.com/",
|
|
61
|
-
"https://vt.tiktok.com/"
|
|
62
|
-
"https://www.youtube.com",
|
|
63
|
-
"https://youtube.com",
|
|
64
|
-
"https://youtu.be",
|
|
65
|
-
"https://m.youtube.com",
|
|
66
|
-
"https://www.youtube.com/watch?"
|
|
67
|
-
|
|
61
|
+
"https://vt.tiktok.com/"
|
|
68
62
|
];
|
|
69
63
|
|
|
70
64
|
// Blacklist links
|
|
@@ -79,7 +73,7 @@ const isVideoLink = (link) => {
|
|
|
79
73
|
const u = new URL(link);
|
|
80
74
|
const host = (u.hostname || '').toLowerCase();
|
|
81
75
|
if (host.endsWith('pintere21313213st.com')) return true;
|
|
82
|
-
} catch (_) {
|
|
76
|
+
} catch (_) {}
|
|
83
77
|
return videoPlatforms.some(platform => link.startsWith(platform));
|
|
84
78
|
};
|
|
85
79
|
|
|
@@ -152,14 +146,14 @@ async function safeUnlinkWithRetry(filePath, maxRetries = 3) {
|
|
|
152
146
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
153
147
|
try {
|
|
154
148
|
const resolvedPath = path.resolve(filePath);
|
|
155
|
-
|
|
149
|
+
|
|
156
150
|
if (fs.existsSync(resolvedPath)) {
|
|
157
151
|
// Check if file is in use (Windows)
|
|
158
152
|
if (process.platform === 'win32' && isFileInUse(resolvedPath)) {
|
|
159
153
|
await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
|
|
160
154
|
continue;
|
|
161
155
|
}
|
|
162
|
-
|
|
156
|
+
|
|
163
157
|
fs.rmSync(resolvedPath, { force: true });
|
|
164
158
|
return true;
|
|
165
159
|
} else {
|
|
@@ -231,7 +225,7 @@ async function validateVideoUrl(url, platform) {
|
|
|
231
225
|
return status >= 200 && status < 400; // Accept redirects
|
|
232
226
|
}
|
|
233
227
|
});
|
|
234
|
-
|
|
228
|
+
|
|
235
229
|
return true;
|
|
236
230
|
} catch (error) {
|
|
237
231
|
return false;
|
|
@@ -243,18 +237,18 @@ function validateTwitterUrl(url) {
|
|
|
243
237
|
try {
|
|
244
238
|
// Remove query parameters that might cause issues
|
|
245
239
|
const cleanUrl = url.split('?')[0];
|
|
246
|
-
|
|
240
|
+
|
|
247
241
|
// Ensure it's a valid Twitter/X URL format
|
|
248
242
|
if (!cleanUrl.includes('x.com') && !cleanUrl.includes('twitter.com')) {
|
|
249
243
|
throw new Error("Not a valid Twitter/X URL");
|
|
250
244
|
}
|
|
251
|
-
|
|
245
|
+
|
|
252
246
|
// Check if it has the required structure
|
|
253
247
|
const urlParts = cleanUrl.split('/');
|
|
254
248
|
if (urlParts.length < 5) {
|
|
255
249
|
throw new Error("Invalid Twitter/X URL structure");
|
|
256
250
|
}
|
|
257
|
-
|
|
251
|
+
|
|
258
252
|
return cleanUrl;
|
|
259
253
|
} catch (error) {
|
|
260
254
|
throw new Error(`Twitter URL validation failed: ${error.message}`);
|
|
@@ -293,10 +287,10 @@ async function downloadYoutubeAudio(url, outputPath) {
|
|
|
293
287
|
try {
|
|
294
288
|
const ytdlp = new YtDlp({ ffmpegPath });
|
|
295
289
|
const cookiesPath = getYoutubeCookiesPath();
|
|
296
|
-
|
|
290
|
+
|
|
297
291
|
// Garantir que o nome do arquivo tenha a extensão correta
|
|
298
292
|
const baseName = outputPath.replace(/\.[^/.]+$/, ""); // Remove extensão se existir
|
|
299
|
-
|
|
293
|
+
|
|
300
294
|
// Primeiro, tentar com a API de opções
|
|
301
295
|
try {
|
|
302
296
|
const options = {
|
|
@@ -307,11 +301,11 @@ async function downloadYoutubeAudio(url, outputPath) {
|
|
|
307
301
|
},
|
|
308
302
|
output: outputPath
|
|
309
303
|
};
|
|
310
|
-
|
|
304
|
+
|
|
311
305
|
if (cookiesPath) {
|
|
312
306
|
options.cookies = cookiesPath;
|
|
313
307
|
}
|
|
314
|
-
|
|
308
|
+
|
|
315
309
|
// Remover logs detalhados para download de áudio do YouTube
|
|
316
310
|
const result = await ytdlp.downloadAsync(url, options);
|
|
317
311
|
// Verificar se o arquivo foi criado
|
|
@@ -320,7 +314,7 @@ async function downloadYoutubeAudio(url, outputPath) {
|
|
|
320
314
|
resolve(actualFileName);
|
|
321
315
|
return;
|
|
322
316
|
}
|
|
323
|
-
|
|
317
|
+
|
|
324
318
|
// Tentar encontrar o arquivo com extensão diferente
|
|
325
319
|
const files = fs.readdirSync('./');
|
|
326
320
|
const downloadedFile = files.find(file => file.startsWith(baseName.split('/').pop()));
|
|
@@ -331,18 +325,18 @@ async function downloadYoutubeAudio(url, outputPath) {
|
|
|
331
325
|
} catch (apiError) {
|
|
332
326
|
console.log('API method failed, trying direct args method:', apiError.message);
|
|
333
327
|
}
|
|
334
|
-
|
|
328
|
+
|
|
335
329
|
// Fallback: usar argumentos diretos para máxima qualidade de áudio
|
|
336
330
|
const args = [
|
|
337
331
|
url,
|
|
338
332
|
'-f', 'bestaudio[ext=mp3]/bestaudio',
|
|
339
333
|
'-o', outputPath
|
|
340
334
|
];
|
|
341
|
-
|
|
335
|
+
|
|
342
336
|
if (cookiesPath) {
|
|
343
337
|
args.push('--cookies', cookiesPath);
|
|
344
338
|
}
|
|
345
|
-
|
|
339
|
+
|
|
346
340
|
// Remover logs detalhados para download de áudio do YouTube
|
|
347
341
|
const result = await ytdlp.execAsync(args);
|
|
348
342
|
// Verificar se o arquivo foi criado
|
|
@@ -369,53 +363,25 @@ async function downloadYoutubeAudio(url, outputPath) {
|
|
|
369
363
|
// Enhanced function to get platform type from URL
|
|
370
364
|
function getPlatformType(url) {
|
|
371
365
|
const lowerUrl = url.toLowerCase();
|
|
372
|
-
|
|
366
|
+
|
|
373
367
|
if (lowerUrl.includes("instagram.com") || lowerUrl.includes("instagr.am")) return "instagram";
|
|
374
368
|
if (lowerUrl.includes("tiktok.com") || lowerUrl.includes("vm.tiktok.com") || lowerUrl.includes("vt.tiktok.com")) return "tiktok";
|
|
375
369
|
if (lowerUrl.includes("facebook.com") || lowerUrl.includes("fb.watch") || lowerUrl.includes("fb.com")) return "facebook";
|
|
376
370
|
if (lowerUrl.includes("mediafire.com")) return "mediafire";
|
|
377
371
|
if (lowerUrl.includes("x.com") || lowerUrl.includes("twitter.com")) return "twitter";
|
|
378
|
-
if (lowerUrl.includes("youtube.com") || lowerUrl.includes("
|
|
379
|
-
|
|
372
|
+
if (lowerUrl.includes("youtube.com") || lowerUrl.includes("you112t12u.be") || lowerUrl.includes("m.y11outu314be.com")) return "youtu1354be";
|
|
373
|
+
|
|
380
374
|
return "unknown";
|
|
381
375
|
}
|
|
382
376
|
|
|
383
|
-
// Function to get direct video URL from Twitter using yt-dlp
|
|
384
|
-
async function downloadTwitterWithYtDlp(url) {
|
|
385
|
-
return new Promise(async (resolve, reject) => {
|
|
386
|
-
try {
|
|
387
|
-
const ytdlp = new YtDlp({ ffmpegPath });
|
|
388
|
-
// Use -g to get URL, -f "best[ext=mp4]/best" to get best quality single file if possible
|
|
389
|
-
const args = [
|
|
390
|
-
url,
|
|
391
|
-
'-g',
|
|
392
|
-
'-f', 'best[ext=mp4]/best'
|
|
393
|
-
];
|
|
394
|
-
|
|
395
|
-
const result = await ytdlp.execAsync(args);
|
|
396
|
-
// yt-dlp might return multiple lines if it finds separate audio/video streams
|
|
397
|
-
// We take the first one, but for Twitter it is usually a single mp4 file
|
|
398
|
-
const videoUrl = result.trim().split('\n')[0];
|
|
399
|
-
|
|
400
|
-
if (videoUrl && videoUrl.startsWith('http')) {
|
|
401
|
-
resolve(videoUrl);
|
|
402
|
-
} else {
|
|
403
|
-
reject(new Error('yt-dlp did not return a valid URL'));
|
|
404
|
-
}
|
|
405
|
-
} catch (error) {
|
|
406
|
-
reject(error);
|
|
407
|
-
}
|
|
408
|
-
});
|
|
409
|
-
}
|
|
410
|
-
|
|
411
377
|
// Enhanced fallback download function with platform-specific methods
|
|
412
378
|
async function tryFallbackDownload(url, maxRetries = 3) {
|
|
413
379
|
const platform = getPlatformType(url);
|
|
414
|
-
|
|
380
|
+
|
|
415
381
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
416
382
|
try {
|
|
417
383
|
let videoUrl = null;
|
|
418
|
-
|
|
384
|
+
|
|
419
385
|
// Platform-specific fallback methods
|
|
420
386
|
switch (platform) {
|
|
421
387
|
case "instagram": {
|
|
@@ -461,7 +427,7 @@ async function tryFallbackDownload(url, maxRetries = 3) {
|
|
|
461
427
|
}
|
|
462
428
|
}
|
|
463
429
|
];
|
|
464
|
-
|
|
430
|
+
|
|
465
431
|
for (const method of methods) {
|
|
466
432
|
try {
|
|
467
433
|
videoUrl = await method();
|
|
@@ -472,7 +438,7 @@ async function tryFallbackDownload(url, maxRetries = 3) {
|
|
|
472
438
|
}
|
|
473
439
|
break;
|
|
474
440
|
}
|
|
475
|
-
|
|
441
|
+
|
|
476
442
|
case "tiktok": {
|
|
477
443
|
// Try multiple TikTok methods
|
|
478
444
|
const methods = [
|
|
@@ -507,7 +473,7 @@ async function tryFallbackDownload(url, maxRetries = 3) {
|
|
|
507
473
|
throw new Error("No valid video in Tiktok.Downloader v2 response");
|
|
508
474
|
}
|
|
509
475
|
];
|
|
510
|
-
|
|
476
|
+
|
|
511
477
|
for (const method of methods) {
|
|
512
478
|
try {
|
|
513
479
|
videoUrl = await method();
|
|
@@ -518,7 +484,7 @@ async function tryFallbackDownload(url, maxRetries = 3) {
|
|
|
518
484
|
}
|
|
519
485
|
break;
|
|
520
486
|
}
|
|
521
|
-
|
|
487
|
+
|
|
522
488
|
case "facebook": {
|
|
523
489
|
// Try multiple Facebook methods
|
|
524
490
|
const methods = [
|
|
@@ -542,7 +508,7 @@ async function tryFallbackDownload(url, maxRetries = 3) {
|
|
|
542
508
|
throw new Error("No valid video in fbdown response with custom headers");
|
|
543
509
|
}
|
|
544
510
|
];
|
|
545
|
-
|
|
511
|
+
|
|
546
512
|
for (const method of methods) {
|
|
547
513
|
try {
|
|
548
514
|
videoUrl = await method();
|
|
@@ -553,46 +519,42 @@ async function tryFallbackDownload(url, maxRetries = 3) {
|
|
|
553
519
|
}
|
|
554
520
|
break;
|
|
555
521
|
}
|
|
556
|
-
|
|
522
|
+
|
|
557
523
|
case "twitter": {
|
|
558
|
-
// Try
|
|
524
|
+
// Try multiple Twitter methods with better response handling
|
|
559
525
|
const methods = [
|
|
560
|
-
async () => {
|
|
561
|
-
// Priority 1: yt-dlp
|
|
562
|
-
return await downloadTwitterWithYtDlp(url);
|
|
563
|
-
},
|
|
564
526
|
async () => {
|
|
565
527
|
const cleanUrl = validateTwitterUrl(url);
|
|
566
528
|
const data = await TwitterDL(cleanUrl, {});
|
|
567
|
-
|
|
529
|
+
|
|
568
530
|
// Check multiple possible response structures
|
|
569
531
|
if (data && data.result) {
|
|
570
532
|
// Structure 1: data.result.media[0].videos[0].url
|
|
571
|
-
if (data.result.media && data.result.media[0] &&
|
|
572
|
-
|
|
573
|
-
|
|
533
|
+
if (data.result.media && data.result.media[0] &&
|
|
534
|
+
data.result.media[0].videos && data.result.media[0].videos[0] &&
|
|
535
|
+
data.result.media[0].videos[0].url) {
|
|
574
536
|
return data.result.media[0].videos[0].url;
|
|
575
537
|
}
|
|
576
|
-
|
|
538
|
+
|
|
577
539
|
// Structure 2: data.result.video[0].url
|
|
578
540
|
if (data.result.video && data.result.video[0] && data.result.video[0].url) {
|
|
579
541
|
return data.result.video[0].url;
|
|
580
542
|
}
|
|
581
|
-
|
|
543
|
+
|
|
582
544
|
// Structure 3: data.result.url (direct URL)
|
|
583
545
|
if (data.result.url) {
|
|
584
546
|
return data.result.url;
|
|
585
547
|
}
|
|
586
|
-
|
|
548
|
+
|
|
587
549
|
// Structure 4: data.result.media[0].url
|
|
588
550
|
if (data.result.media && data.result.media[0] && data.result.media[0].url) {
|
|
589
551
|
return data.result.media[0].url;
|
|
590
552
|
}
|
|
591
|
-
|
|
553
|
+
|
|
592
554
|
// Structure 5: Check for any video URL in the entire response
|
|
593
555
|
const findVideoUrl = (obj) => {
|
|
594
|
-
if (typeof obj === 'string' && obj.includes('http') &&
|
|
595
|
-
|
|
556
|
+
if (typeof obj === 'string' && obj.includes('http') &&
|
|
557
|
+
(obj.includes('.mp4') || obj.includes('video') || obj.includes('media'))) {
|
|
596
558
|
return obj;
|
|
597
559
|
}
|
|
598
560
|
if (typeof obj === 'object' && obj !== null) {
|
|
@@ -603,13 +565,13 @@ async function tryFallbackDownload(url, maxRetries = 3) {
|
|
|
603
565
|
}
|
|
604
566
|
return null;
|
|
605
567
|
};
|
|
606
|
-
|
|
568
|
+
|
|
607
569
|
const foundUrl = findVideoUrl(data.result);
|
|
608
570
|
if (foundUrl) {
|
|
609
571
|
return foundUrl;
|
|
610
572
|
}
|
|
611
573
|
}
|
|
612
|
-
|
|
574
|
+
|
|
613
575
|
throw new Error("No valid video URL found in TwitterDL response structure");
|
|
614
576
|
},
|
|
615
577
|
async () => {
|
|
@@ -625,30 +587,30 @@ async function tryFallbackDownload(url, maxRetries = 3) {
|
|
|
625
587
|
'Upgrade-Insecure-Requests': '1'
|
|
626
588
|
}
|
|
627
589
|
});
|
|
628
|
-
|
|
590
|
+
|
|
629
591
|
// Same structure checking as above
|
|
630
592
|
if (data && data.result) {
|
|
631
|
-
if (data.result.media && data.result.media[0] &&
|
|
632
|
-
|
|
633
|
-
|
|
593
|
+
if (data.result.media && data.result.media[0] &&
|
|
594
|
+
data.result.media[0].videos && data.result.media[0].videos[0] &&
|
|
595
|
+
data.result.media[0].videos[0].url) {
|
|
634
596
|
return data.result.media[0].videos[0].url;
|
|
635
597
|
}
|
|
636
|
-
|
|
598
|
+
|
|
637
599
|
if (data.result.video && data.result.video[0] && data.result.video[0].url) {
|
|
638
600
|
return data.result.video[0].url;
|
|
639
601
|
}
|
|
640
|
-
|
|
602
|
+
|
|
641
603
|
if (data.result.url) {
|
|
642
604
|
return data.result.url;
|
|
643
605
|
}
|
|
644
|
-
|
|
606
|
+
|
|
645
607
|
if (data.result.media && data.result.media[0] && data.result.media[0].url) {
|
|
646
608
|
return data.result.media[0].url;
|
|
647
609
|
}
|
|
648
|
-
|
|
610
|
+
|
|
649
611
|
const findVideoUrl = (obj) => {
|
|
650
|
-
if (typeof obj === 'string' && obj.includes('http') &&
|
|
651
|
-
|
|
612
|
+
if (typeof obj === 'string' && obj.includes('http') &&
|
|
613
|
+
(obj.includes('.mp4') || obj.includes('video') || obj.includes('media'))) {
|
|
652
614
|
return obj;
|
|
653
615
|
}
|
|
654
616
|
if (typeof obj === 'object' && obj !== null) {
|
|
@@ -659,13 +621,13 @@ async function tryFallbackDownload(url, maxRetries = 3) {
|
|
|
659
621
|
}
|
|
660
622
|
return null;
|
|
661
623
|
};
|
|
662
|
-
|
|
624
|
+
|
|
663
625
|
const foundUrl = findVideoUrl(data.result);
|
|
664
626
|
if (foundUrl) {
|
|
665
627
|
return foundUrl;
|
|
666
628
|
}
|
|
667
629
|
}
|
|
668
|
-
|
|
630
|
+
|
|
669
631
|
throw new Error("No valid video URL found in TwitterDL response with custom headers");
|
|
670
632
|
},
|
|
671
633
|
async () => {
|
|
@@ -701,9 +663,9 @@ async function tryFallbackDownload(url, maxRetries = 3) {
|
|
|
701
663
|
const data = await btchOld[funcName](cleanUrl);
|
|
702
664
|
// Check multiple possible response structures
|
|
703
665
|
if (data && data.result) {
|
|
704
|
-
if (data.result.media && data.result.media[0] &&
|
|
705
|
-
|
|
706
|
-
|
|
666
|
+
if (data.result.media && data.result.media[0] &&
|
|
667
|
+
data.result.media[0].videos && data.result.media[0].videos[0] &&
|
|
668
|
+
data.result.media[0].videos[0].url) {
|
|
707
669
|
return data.result.media[0].videos[0].url;
|
|
708
670
|
}
|
|
709
671
|
if (data.result.video && data.result.video[0] && data.result.video[0].url) {
|
|
@@ -730,7 +692,7 @@ async function tryFallbackDownload(url, maxRetries = 3) {
|
|
|
730
692
|
}
|
|
731
693
|
}
|
|
732
694
|
];
|
|
733
|
-
|
|
695
|
+
|
|
734
696
|
for (const method of methods) {
|
|
735
697
|
try {
|
|
736
698
|
videoUrl = await method();
|
|
@@ -741,12 +703,12 @@ async function tryFallbackDownload(url, maxRetries = 3) {
|
|
|
741
703
|
}
|
|
742
704
|
break;
|
|
743
705
|
}
|
|
744
|
-
|
|
706
|
+
|
|
745
707
|
case "youtube": {
|
|
746
708
|
// YouTube doesn't need fallback - it has its own specific method
|
|
747
709
|
throw new Error("YouTube downloads should use the primary method, not fallback");
|
|
748
710
|
}
|
|
749
|
-
|
|
711
|
+
|
|
750
712
|
case "mediafire": {
|
|
751
713
|
// Try MediaFire method
|
|
752
714
|
try {
|
|
@@ -784,10 +746,10 @@ async function tryFallbackDownload(url, maxRetries = 3) {
|
|
|
784
746
|
};
|
|
785
747
|
mediaUrl = pickFrom(data);
|
|
786
748
|
if (mediaUrl) return mediaUrl;
|
|
787
|
-
} catch (_) {
|
|
749
|
+
} catch (_) {}
|
|
788
750
|
break;
|
|
789
751
|
}
|
|
790
|
-
|
|
752
|
+
|
|
791
753
|
default: {
|
|
792
754
|
// Generic fallback for unknown platforms
|
|
793
755
|
try {
|
|
@@ -803,18 +765,18 @@ async function tryFallbackDownload(url, maxRetries = 3) {
|
|
|
803
765
|
break;
|
|
804
766
|
}
|
|
805
767
|
}
|
|
806
|
-
|
|
768
|
+
|
|
807
769
|
// If we got a video URL, validate it
|
|
808
770
|
if (videoUrl && videoUrl.includes("http")) {
|
|
809
771
|
// Validate the URL is accessible
|
|
810
772
|
try {
|
|
811
|
-
const response = await axios.head(videoUrl, {
|
|
773
|
+
const response = await axios.head(videoUrl, {
|
|
812
774
|
timeout: 10000,
|
|
813
775
|
headers: {
|
|
814
776
|
'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'
|
|
815
777
|
}
|
|
816
778
|
});
|
|
817
|
-
|
|
779
|
+
|
|
818
780
|
if (response.status === 200) {
|
|
819
781
|
return videoUrl;
|
|
820
782
|
} else {
|
|
@@ -826,7 +788,7 @@ async function tryFallbackDownload(url, maxRetries = 3) {
|
|
|
826
788
|
} else {
|
|
827
789
|
throw new Error("No valid video URL obtained from any fallback method");
|
|
828
790
|
}
|
|
829
|
-
|
|
791
|
+
|
|
830
792
|
} catch (error) {
|
|
831
793
|
if (attempt === maxRetries) {
|
|
832
794
|
throw new Error(`Enhanced fallback method failed after ${maxRetries} attempts for ${platform}: ${error.message}`);
|
|
@@ -873,8 +835,9 @@ const MediaDownloader = async (url, options = {}) => {
|
|
|
873
835
|
|
|
874
836
|
// Verificar se é YouTube e lançar erro customizado
|
|
875
837
|
const platform = getPlatformType(url);
|
|
876
|
-
|
|
877
|
-
|
|
838
|
+
if (platform === "youtube") {
|
|
839
|
+
throw new Error("This URL is not from a supported platform. Supported platforms: Instagram, X(Twitter), TikTok, Facebook");
|
|
840
|
+
}
|
|
878
841
|
|
|
879
842
|
await cleanupTempFiles(); // Clean up previous temp files
|
|
880
843
|
|
|
@@ -886,21 +849,21 @@ const MediaDownloader = async (url, options = {}) => {
|
|
|
886
849
|
downloadedFilePath = await downloadSmartVideo(url, config);
|
|
887
850
|
} catch (error) {
|
|
888
851
|
const platform = getPlatformType(url);
|
|
889
|
-
|
|
890
|
-
// YouTube doesn't use fallback method - it has its own specific implementation
|
|
852
|
+
|
|
853
|
+
// YouTube doesn't use fallback method - it has its own specific implementation
|
|
891
854
|
if (platform === "youtube") {
|
|
892
855
|
throw new Error(`YouTube download failed: ${error.message}`);
|
|
893
856
|
}
|
|
894
|
-
|
|
857
|
+
|
|
895
858
|
try {
|
|
896
859
|
const fallbackUrl = await tryFallbackDownload(url);
|
|
897
|
-
|
|
860
|
+
|
|
898
861
|
// Validate fallback URL
|
|
899
862
|
const isValid = await validateVideoUrl(fallbackUrl, platform);
|
|
900
863
|
if (!isValid) {
|
|
901
864
|
throw new Error("Fallback URL validation failed");
|
|
902
865
|
}
|
|
903
|
-
|
|
866
|
+
|
|
904
867
|
downloadedFilePath = await downloadDirectVideo(fallbackUrl, config);
|
|
905
868
|
} catch (fallbackError) {
|
|
906
869
|
throw new Error(`Failed to download video with both methods: ${fallbackError.message}`);
|
|
@@ -1023,15 +986,15 @@ async function downloadSmartVideo(url, config) {
|
|
|
1023
986
|
throw new Error("No video URLs found in Facebook response");
|
|
1024
987
|
}
|
|
1025
988
|
videoUrl = data.Normal_video || data.HD;
|
|
1026
|
-
|
|
989
|
+
|
|
1027
990
|
// Validate the URL is accessible
|
|
1028
|
-
const response = await axios.head(videoUrl, {
|
|
991
|
+
const response = await axios.head(videoUrl, {
|
|
1029
992
|
timeout: 10000,
|
|
1030
993
|
headers: {
|
|
1031
994
|
'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'
|
|
1032
995
|
}
|
|
1033
996
|
}).catch(() => null);
|
|
1034
|
-
|
|
997
|
+
|
|
1035
998
|
if (!response || response.status !== 200) {
|
|
1036
999
|
throw new Error("Facebook video URL is not accessible");
|
|
1037
1000
|
}
|
|
@@ -1103,12 +1066,12 @@ async function downloadSmartVideo(url, config) {
|
|
|
1103
1066
|
// Validate and clean the Twitter URL
|
|
1104
1067
|
const cleanUrl = validateTwitterUrl(url);
|
|
1105
1068
|
const data = await TwitterDL(cleanUrl, {});
|
|
1106
|
-
|
|
1069
|
+
|
|
1107
1070
|
if (data && data.result) {
|
|
1108
1071
|
// Try multiple possible response structures
|
|
1109
|
-
if (data.result.media && data.result.media[0] &&
|
|
1110
|
-
|
|
1111
|
-
|
|
1072
|
+
if (data.result.media && data.result.media[0] &&
|
|
1073
|
+
data.result.media[0].videos && data.result.media[0].videos[0] &&
|
|
1074
|
+
data.result.media[0].videos[0].url) {
|
|
1112
1075
|
videoUrl = data.result.media[0].videos[0].url;
|
|
1113
1076
|
} else if (data.result.video && data.result.video[0] && data.result.video[0].url) {
|
|
1114
1077
|
videoUrl = data.result.video[0].url;
|
|
@@ -1119,8 +1082,8 @@ async function downloadSmartVideo(url, config) {
|
|
|
1119
1082
|
} else {
|
|
1120
1083
|
// Search for any video URL in the response
|
|
1121
1084
|
const findVideoUrl = (obj) => {
|
|
1122
|
-
if (typeof obj === 'string' && obj.includes('http') &&
|
|
1123
|
-
|
|
1085
|
+
if (typeof obj === 'string' && obj.includes('http') &&
|
|
1086
|
+
(obj.includes('.mp4') || obj.includes('video') || obj.includes('media'))) {
|
|
1124
1087
|
return obj;
|
|
1125
1088
|
}
|
|
1126
1089
|
if (typeof obj === 'object' && obj !== null) {
|
|
@@ -1131,7 +1094,7 @@ async function downloadSmartVideo(url, config) {
|
|
|
1131
1094
|
}
|
|
1132
1095
|
return null;
|
|
1133
1096
|
};
|
|
1134
|
-
|
|
1097
|
+
|
|
1135
1098
|
const foundUrl = findVideoUrl(data.result);
|
|
1136
1099
|
if (foundUrl) {
|
|
1137
1100
|
videoUrl = foundUrl;
|
|
@@ -1147,56 +1110,6 @@ async function downloadSmartVideo(url, config) {
|
|
|
1147
1110
|
}
|
|
1148
1111
|
break;
|
|
1149
1112
|
}
|
|
1150
|
-
case "youtube": {
|
|
1151
|
-
try {
|
|
1152
|
-
const data = await youtube(url);
|
|
1153
|
-
// Try to find the video URL in the response
|
|
1154
|
-
// Common patterns: data.url, data.mp4, data.link, or direct string
|
|
1155
|
-
|
|
1156
|
-
if (!data) throw new Error("No data returned from youtube downloader");
|
|
1157
|
-
|
|
1158
|
-
// Recursive function to find a valid video URL in the object
|
|
1159
|
-
const findVideoUrl = (obj) => {
|
|
1160
|
-
if (!obj) return null;
|
|
1161
|
-
if (typeof obj === 'string' && obj.startsWith('http')) return obj;
|
|
1162
|
-
|
|
1163
|
-
if (Array.isArray(obj)) {
|
|
1164
|
-
for (const item of obj) {
|
|
1165
|
-
const found = findVideoUrl(item);
|
|
1166
|
-
if (found) return found;
|
|
1167
|
-
}
|
|
1168
|
-
} else if (typeof obj === 'object') {
|
|
1169
|
-
// Prioritize certain keys
|
|
1170
|
-
const priorities = ['url', 'link', 'download', 'mp4', 'video', 'src'];
|
|
1171
|
-
for (const key of priorities) {
|
|
1172
|
-
if (obj[key]) {
|
|
1173
|
-
const found = findVideoUrl(obj[key]);
|
|
1174
|
-
if (found) return found;
|
|
1175
|
-
}
|
|
1176
|
-
}
|
|
1177
|
-
// If not found in priorities, check all keys
|
|
1178
|
-
for (const key of Object.keys(obj)) {
|
|
1179
|
-
if (!priorities.includes(key)) {
|
|
1180
|
-
const found = findVideoUrl(obj[key]);
|
|
1181
|
-
if (found) return found;
|
|
1182
|
-
}
|
|
1183
|
-
}
|
|
1184
|
-
}
|
|
1185
|
-
return null;
|
|
1186
|
-
};
|
|
1187
|
-
|
|
1188
|
-
videoUrl = findVideoUrl(data);
|
|
1189
|
-
|
|
1190
|
-
if (!videoUrl) {
|
|
1191
|
-
console.log('YouTube data dump:', JSON.stringify(data, null, 2)); // Debug log since we can't test
|
|
1192
|
-
throw new Error("Could not extract video URL from YouTube response");
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
} catch (error) {
|
|
1196
|
-
throw new Error(`YouTube download failed: ${error.message}`);
|
|
1197
|
-
}
|
|
1198
|
-
break;
|
|
1199
|
-
}
|
|
1200
1113
|
default:
|
|
1201
1114
|
throw new Error("Platform not supported or invalid link.");
|
|
1202
1115
|
}
|
|
@@ -1204,21 +1117,21 @@ async function downloadSmartVideo(url, config) {
|
|
|
1204
1117
|
if (!videoUrl || !videoUrl.includes("http")) {
|
|
1205
1118
|
throw new Error("Returned video URL is invalid or unavailable.");
|
|
1206
1119
|
}
|
|
1207
|
-
|
|
1120
|
+
|
|
1208
1121
|
// Download the video with better error handling
|
|
1209
|
-
const response = await axios({
|
|
1210
|
-
url: videoUrl,
|
|
1211
|
-
method: "GET",
|
|
1122
|
+
const response = await axios({
|
|
1123
|
+
url: videoUrl,
|
|
1124
|
+
method: "GET",
|
|
1212
1125
|
responseType: "stream",
|
|
1213
1126
|
timeout: 30000,
|
|
1214
1127
|
headers: {
|
|
1215
1128
|
'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'
|
|
1216
1129
|
}
|
|
1217
1130
|
});
|
|
1218
|
-
|
|
1131
|
+
|
|
1219
1132
|
// Create minimal unique file name in output dir
|
|
1220
|
-
let fileName = path.join(OUTPUT_DIR, `v_${uuidv4().slice(0,
|
|
1221
|
-
|
|
1133
|
+
let fileName = path.join(OUTPUT_DIR, `v_${uuidv4().slice(0,4)}.mp4`);
|
|
1134
|
+
|
|
1222
1135
|
const videoWriter = fs.createWriteStream(fileName);
|
|
1223
1136
|
response.data.pipe(videoWriter);
|
|
1224
1137
|
|
|
@@ -1243,19 +1156,19 @@ async function downloadSmartVideo(url, config) {
|
|
|
1243
1156
|
// Function for direct video download
|
|
1244
1157
|
async function downloadDirectVideo(url, config) {
|
|
1245
1158
|
try {
|
|
1246
|
-
const response = await axios({
|
|
1247
|
-
url: url,
|
|
1248
|
-
method: "GET",
|
|
1159
|
+
const response = await axios({
|
|
1160
|
+
url: url,
|
|
1161
|
+
method: "GET",
|
|
1249
1162
|
responseType: "stream",
|
|
1250
1163
|
timeout: 30000,
|
|
1251
1164
|
headers: {
|
|
1252
1165
|
'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'
|
|
1253
1166
|
}
|
|
1254
1167
|
});
|
|
1255
|
-
|
|
1168
|
+
|
|
1256
1169
|
// Create minimal unique file name in output dir
|
|
1257
|
-
let fileName = path.join(OUTPUT_DIR, `v_${uuidv4().slice(0,
|
|
1258
|
-
|
|
1170
|
+
let fileName = path.join(OUTPUT_DIR, `v_${uuidv4().slice(0,4)}.mp4`);
|
|
1171
|
+
|
|
1259
1172
|
const videoWriter = fs.createWriteStream(fileName);
|
|
1260
1173
|
response.data.pipe(videoWriter);
|
|
1261
1174
|
|
|
@@ -1303,7 +1216,7 @@ async function downloadGenericFile(url, preferredExt = null) {
|
|
|
1303
1216
|
else ext = 'bin';
|
|
1304
1217
|
}
|
|
1305
1218
|
|
|
1306
|
-
const fileName = path.join(OUTPUT_DIR, `v_${uuidv4().slice(0,
|
|
1219
|
+
const fileName = path.join(OUTPUT_DIR, `v_${uuidv4().slice(0,4)}.${ext}`);
|
|
1307
1220
|
const writer = fs.createWriteStream(fileName);
|
|
1308
1221
|
response.data.pipe(writer);
|
|
1309
1222
|
|
|
@@ -1318,7 +1231,7 @@ async function downloadGenericFile(url, preferredExt = null) {
|
|
|
1318
1231
|
|
|
1319
1232
|
// Function to rotate video
|
|
1320
1233
|
async function rotateVideo(fileName, rotation) {
|
|
1321
|
-
const outputPath = path.join(OUTPUT_DIR, `vr_${uuidv4().slice(0,
|
|
1234
|
+
const outputPath = path.join(OUTPUT_DIR, `vr_${uuidv4().slice(0,4)}.mp4`);
|
|
1322
1235
|
let angle;
|
|
1323
1236
|
switch (rotation.toLowerCase()) {
|
|
1324
1237
|
case "left": angle = "transpose=2"; break;
|
|
@@ -1369,8 +1282,8 @@ async function cleanupTempFiles() {
|
|
|
1369
1282
|
try {
|
|
1370
1283
|
if (!fs.existsSync(TEMP_DIR)) return;
|
|
1371
1284
|
const files = fs.readdirSync(TEMP_DIR);
|
|
1372
|
-
const tempFiles = files.filter(file =>
|
|
1373
|
-
/^temp_video.*\.mp4$/.test(file) ||
|
|
1285
|
+
const tempFiles = files.filter(file =>
|
|
1286
|
+
/^temp_video.*\.mp4$/.test(file) ||
|
|
1374
1287
|
/_audio\.mp3$/.test(file) ||
|
|
1375
1288
|
/_rotated\.mp4$/.test(file) ||
|
|
1376
1289
|
/_cropped\.mp4$/.test(file) ||
|
|
@@ -1402,9 +1315,9 @@ async function cleanupOutputFiles() {
|
|
|
1402
1315
|
if (now - st.mtimeMs > maxAgeMs) {
|
|
1403
1316
|
await safeUnlinkWithRetry(full);
|
|
1404
1317
|
}
|
|
1405
|
-
} catch (_) {
|
|
1318
|
+
} catch (_) {}
|
|
1406
1319
|
}
|
|
1407
|
-
} catch (_) {
|
|
1320
|
+
} catch (_) {}
|
|
1408
1321
|
}
|
|
1409
1322
|
|
|
1410
1323
|
// Schedule periodic cleanup (every 10 minutes)
|
|
@@ -1418,7 +1331,7 @@ cleanupOutputFiles();
|
|
|
1418
1331
|
// Function to auto-crop video
|
|
1419
1332
|
async function autoCrop(fileName) {
|
|
1420
1333
|
const inputPath = fileName;
|
|
1421
|
-
const outputPath = path.join(OUTPUT_DIR, `vc_${uuidv4().slice(0,
|
|
1334
|
+
const outputPath = path.join(OUTPUT_DIR, `vc_${uuidv4().slice(0,4)}.mp4`);
|
|
1422
1335
|
|
|
1423
1336
|
return new Promise((resolve, reject) => {
|
|
1424
1337
|
let cropValues = null;
|
|
@@ -1426,13 +1339,13 @@ async function autoCrop(fileName) {
|
|
|
1426
1339
|
.outputOptions('-vf', 'cropdetect=24:16:0')
|
|
1427
1340
|
.outputFormat('null')
|
|
1428
1341
|
.output('-')
|
|
1429
|
-
.on('stderr', function
|
|
1342
|
+
.on('stderr', function(stderrLine) {
|
|
1430
1343
|
const cropMatch = stderrLine.match(/crop=([0-9]+):([0-9]+):([0-9]+):([0-9]+)/);
|
|
1431
1344
|
if (cropMatch) {
|
|
1432
1345
|
cropValues = `crop=${cropMatch[1]}:${cropMatch[2]}:${cropMatch[3]}:${cropMatch[4]}`;
|
|
1433
1346
|
}
|
|
1434
1347
|
})
|
|
1435
|
-
.on('end', function
|
|
1348
|
+
.on('end', function() {
|
|
1436
1349
|
if (!cropValues) {
|
|
1437
1350
|
resolve(inputPath);
|
|
1438
1351
|
return;
|
|
@@ -1465,7 +1378,7 @@ async function checkAndCompressVideo(filePath, limitSizeMB, platform = null) {
|
|
|
1465
1378
|
return filePath;
|
|
1466
1379
|
}
|
|
1467
1380
|
|
|
1468
|
-
const outputPath = path.join(OUTPUT_DIR, `vx_${uuidv4().slice(0,
|
|
1381
|
+
const outputPath = path.join(OUTPUT_DIR, `vx_${uuidv4().slice(0,4)}.mp4`);
|
|
1469
1382
|
|
|
1470
1383
|
return new Promise((resolve, reject) => {
|
|
1471
1384
|
ffmpeg(filePath)
|
|
@@ -1500,17 +1413,17 @@ async function convertVideoFormat(inputPath, targetFormat) {
|
|
|
1500
1413
|
const supported = ["mp4", "mov", "webm", "mkv"];
|
|
1501
1414
|
if (!supported.includes(fmt)) return inputPath;
|
|
1502
1415
|
|
|
1503
|
-
const outputPath = path.join(OUTPUT_DIR, `vf_${uuidv4().slice(0,
|
|
1416
|
+
const outputPath = path.join(OUTPUT_DIR, `vf_${uuidv4().slice(0,4)}.${fmt}`);
|
|
1504
1417
|
|
|
1505
1418
|
const ff = ffmpeg(inputPath);
|
|
1506
1419
|
switch (fmt) {
|
|
1507
1420
|
case "mp4":
|
|
1508
1421
|
case "mov":
|
|
1509
1422
|
case "mkv":
|
|
1510
|
-
ff.outputOptions("-c:v",
|
|
1423
|
+
ff.outputOptions("-c:v","libx264","-pix_fmt","yuv420p","-c:a","aac","-b:a","192k");
|
|
1511
1424
|
break;
|
|
1512
1425
|
case "webm":
|
|
1513
|
-
ff.outputOptions("-c:v",
|
|
1426
|
+
ff.outputOptions("-c:v","libvpx-vp9","-b:v","0","-crf","30","-c:a","libopus","-b:a","128k");
|
|
1514
1427
|
break;
|
|
1515
1428
|
default:
|
|
1516
1429
|
return inputPath;
|
|
@@ -1567,14 +1480,14 @@ async function unshortenUrl(url) {
|
|
|
1567
1480
|
return status >= 200 && status < 400; // Accept redirects
|
|
1568
1481
|
}
|
|
1569
1482
|
});
|
|
1570
|
-
|
|
1483
|
+
|
|
1571
1484
|
// Get the final URL after all redirects
|
|
1572
1485
|
const finalUrl = response.request.res.responseUrl || response.config.url;
|
|
1573
1486
|
return finalUrl;
|
|
1574
1487
|
}
|
|
1575
|
-
|
|
1488
|
+
|
|
1576
1489
|
// For other URLs, use the original method
|
|
1577
|
-
const response = await axios.head(url, {
|
|
1490
|
+
const response = await axios.head(url, {
|
|
1578
1491
|
maxRedirects: 10,
|
|
1579
1492
|
timeout: 10000,
|
|
1580
1493
|
headers: {
|
|
@@ -1643,10 +1556,10 @@ const AudioDownloader = async (url, options = {}) => {
|
|
|
1643
1556
|
if (downloadedFilePath) {
|
|
1644
1557
|
// Extrair áudio em mp3
|
|
1645
1558
|
audioFilePath = await extractAudioMp3(downloadedFilePath);
|
|
1646
|
-
|
|
1559
|
+
|
|
1647
1560
|
// Remove o arquivo de vídeo temporário após extrair o áudio
|
|
1648
1561
|
await safeUnlinkWithRetry(downloadedFilePath);
|
|
1649
|
-
|
|
1562
|
+
|
|
1650
1563
|
const result = await uploadToGoFileIfNeeded(audioFilePath);
|
|
1651
1564
|
return result;
|
|
1652
1565
|
} else {
|
|
@@ -1662,7 +1575,7 @@ const AudioDownloader = async (url, options = {}) => {
|
|
|
1662
1575
|
// Função para extrair áudio em mp3 usando ffmpeg
|
|
1663
1576
|
async function extractAudioMp3(videoPath) {
|
|
1664
1577
|
return new Promise((resolve, reject) => {
|
|
1665
|
-
const audioPath = path.join(OUTPUT_DIR, `a_${uuidv4().slice(0,
|
|
1578
|
+
const audioPath = path.join(OUTPUT_DIR, `a_${uuidv4().slice(0,4)}.mp3`);
|
|
1666
1579
|
ffmpeg(videoPath)
|
|
1667
1580
|
.noVideo()
|
|
1668
1581
|
.audioCodec('libmp3lame')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "frostpv",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.11",
|
|
4
4
|
"description": "downloads",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"license": "ISC",
|
|
14
14
|
"dependencies": {
|
|
15
15
|
"axios": "1.12.0",
|
|
16
|
-
"btch-downloader": "6.0.
|
|
16
|
+
"btch-downloader": "6.0.25",
|
|
17
17
|
"btch-downloader-old": "npm:btch-downloader@4.0.15",
|
|
18
18
|
"express": "^5.1.0",
|
|
19
19
|
"ffmpeg-ffprobe-static": "^6.1.1-rc.5",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"path": "^0.12.7",
|
|
23
23
|
"twitter-downloader": "^1.1.8",
|
|
24
24
|
"uuid": "^11.1.0",
|
|
25
|
-
"@tobyg74/tiktok-api-dl": "^1.3.
|
|
25
|
+
"@tobyg74/tiktok-api-dl": "^1.3.7",
|
|
26
26
|
"ytdlp-nodejs": "2.3.4"
|
|
27
27
|
}
|
|
28
28
|
}
|