frostpv 1.0.7 → 1.0.8
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 +197 -116
- package/package.json +1 -1
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 } = require("btch-downloader");
|
|
7
|
+
const { igdl, ttdl, fbdown, mediafire, capcut, gdrive, pinterest, youtube } = 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
|
|
|
@@ -73,7 +73,7 @@ const isVideoLink = (link) => {
|
|
|
73
73
|
const u = new URL(link);
|
|
74
74
|
const host = (u.hostname || '').toLowerCase();
|
|
75
75
|
if (host.endsWith('pintere21313213st.com')) return true;
|
|
76
|
-
} catch (_) {}
|
|
76
|
+
} catch (_) { }
|
|
77
77
|
return videoPlatforms.some(platform => link.startsWith(platform));
|
|
78
78
|
};
|
|
79
79
|
|
|
@@ -146,14 +146,14 @@ async function safeUnlinkWithRetry(filePath, maxRetries = 3) {
|
|
|
146
146
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
147
147
|
try {
|
|
148
148
|
const resolvedPath = path.resolve(filePath);
|
|
149
|
-
|
|
149
|
+
|
|
150
150
|
if (fs.existsSync(resolvedPath)) {
|
|
151
151
|
// Check if file is in use (Windows)
|
|
152
152
|
if (process.platform === 'win32' && isFileInUse(resolvedPath)) {
|
|
153
153
|
await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
|
|
154
154
|
continue;
|
|
155
155
|
}
|
|
156
|
-
|
|
156
|
+
|
|
157
157
|
fs.rmSync(resolvedPath, { force: true });
|
|
158
158
|
return true;
|
|
159
159
|
} else {
|
|
@@ -225,7 +225,7 @@ async function validateVideoUrl(url, platform) {
|
|
|
225
225
|
return status >= 200 && status < 400; // Accept redirects
|
|
226
226
|
}
|
|
227
227
|
});
|
|
228
|
-
|
|
228
|
+
|
|
229
229
|
return true;
|
|
230
230
|
} catch (error) {
|
|
231
231
|
return false;
|
|
@@ -237,18 +237,18 @@ function validateTwitterUrl(url) {
|
|
|
237
237
|
try {
|
|
238
238
|
// Remove query parameters that might cause issues
|
|
239
239
|
const cleanUrl = url.split('?')[0];
|
|
240
|
-
|
|
240
|
+
|
|
241
241
|
// Ensure it's a valid Twitter/X URL format
|
|
242
242
|
if (!cleanUrl.includes('x.com') && !cleanUrl.includes('twitter.com')) {
|
|
243
243
|
throw new Error("Not a valid Twitter/X URL");
|
|
244
244
|
}
|
|
245
|
-
|
|
245
|
+
|
|
246
246
|
// Check if it has the required structure
|
|
247
247
|
const urlParts = cleanUrl.split('/');
|
|
248
248
|
if (urlParts.length < 5) {
|
|
249
249
|
throw new Error("Invalid Twitter/X URL structure");
|
|
250
250
|
}
|
|
251
|
-
|
|
251
|
+
|
|
252
252
|
return cleanUrl;
|
|
253
253
|
} catch (error) {
|
|
254
254
|
throw new Error(`Twitter URL validation failed: ${error.message}`);
|
|
@@ -287,10 +287,10 @@ async function downloadYoutubeAudio(url, outputPath) {
|
|
|
287
287
|
try {
|
|
288
288
|
const ytdlp = new YtDlp({ ffmpegPath });
|
|
289
289
|
const cookiesPath = getYoutubeCookiesPath();
|
|
290
|
-
|
|
290
|
+
|
|
291
291
|
// Garantir que o nome do arquivo tenha a extensão correta
|
|
292
292
|
const baseName = outputPath.replace(/\.[^/.]+$/, ""); // Remove extensão se existir
|
|
293
|
-
|
|
293
|
+
|
|
294
294
|
// Primeiro, tentar com a API de opções
|
|
295
295
|
try {
|
|
296
296
|
const options = {
|
|
@@ -301,11 +301,11 @@ async function downloadYoutubeAudio(url, outputPath) {
|
|
|
301
301
|
},
|
|
302
302
|
output: outputPath
|
|
303
303
|
};
|
|
304
|
-
|
|
304
|
+
|
|
305
305
|
if (cookiesPath) {
|
|
306
306
|
options.cookies = cookiesPath;
|
|
307
307
|
}
|
|
308
|
-
|
|
308
|
+
|
|
309
309
|
// Remover logs detalhados para download de áudio do YouTube
|
|
310
310
|
const result = await ytdlp.downloadAsync(url, options);
|
|
311
311
|
// Verificar se o arquivo foi criado
|
|
@@ -314,7 +314,7 @@ async function downloadYoutubeAudio(url, outputPath) {
|
|
|
314
314
|
resolve(actualFileName);
|
|
315
315
|
return;
|
|
316
316
|
}
|
|
317
|
-
|
|
317
|
+
|
|
318
318
|
// Tentar encontrar o arquivo com extensão diferente
|
|
319
319
|
const files = fs.readdirSync('./');
|
|
320
320
|
const downloadedFile = files.find(file => file.startsWith(baseName.split('/').pop()));
|
|
@@ -325,18 +325,18 @@ async function downloadYoutubeAudio(url, outputPath) {
|
|
|
325
325
|
} catch (apiError) {
|
|
326
326
|
console.log('API method failed, trying direct args method:', apiError.message);
|
|
327
327
|
}
|
|
328
|
-
|
|
328
|
+
|
|
329
329
|
// Fallback: usar argumentos diretos para máxima qualidade de áudio
|
|
330
330
|
const args = [
|
|
331
331
|
url,
|
|
332
332
|
'-f', 'bestaudio[ext=mp3]/bestaudio',
|
|
333
333
|
'-o', outputPath
|
|
334
334
|
];
|
|
335
|
-
|
|
335
|
+
|
|
336
336
|
if (cookiesPath) {
|
|
337
337
|
args.push('--cookies', cookiesPath);
|
|
338
338
|
}
|
|
339
|
-
|
|
339
|
+
|
|
340
340
|
// Remover logs detalhados para download de áudio do YouTube
|
|
341
341
|
const result = await ytdlp.execAsync(args);
|
|
342
342
|
// Verificar se o arquivo foi criado
|
|
@@ -363,25 +363,53 @@ async function downloadYoutubeAudio(url, outputPath) {
|
|
|
363
363
|
// Enhanced function to get platform type from URL
|
|
364
364
|
function getPlatformType(url) {
|
|
365
365
|
const lowerUrl = url.toLowerCase();
|
|
366
|
-
|
|
366
|
+
|
|
367
367
|
if (lowerUrl.includes("instagram.com") || lowerUrl.includes("instagr.am")) return "instagram";
|
|
368
368
|
if (lowerUrl.includes("tiktok.com") || lowerUrl.includes("vm.tiktok.com") || lowerUrl.includes("vt.tiktok.com")) return "tiktok";
|
|
369
369
|
if (lowerUrl.includes("facebook.com") || lowerUrl.includes("fb.watch") || lowerUrl.includes("fb.com")) return "facebook";
|
|
370
370
|
if (lowerUrl.includes("mediafire.com")) return "mediafire";
|
|
371
371
|
if (lowerUrl.includes("x.com") || lowerUrl.includes("twitter.com")) return "twitter";
|
|
372
|
-
if (lowerUrl.includes("youtube.com") || lowerUrl.includes("
|
|
373
|
-
|
|
372
|
+
if (lowerUrl.includes("youtube.com") || lowerUrl.includes("youtu.be") || lowerUrl.includes("m.youtube.com")) return "youtube";
|
|
373
|
+
|
|
374
374
|
return "unknown";
|
|
375
375
|
}
|
|
376
376
|
|
|
377
|
+
// Function to get direct video URL from Twitter using yt-dlp
|
|
378
|
+
async function downloadTwitterWithYtDlp(url) {
|
|
379
|
+
return new Promise(async (resolve, reject) => {
|
|
380
|
+
try {
|
|
381
|
+
const ytdlp = new YtDlp({ ffmpegPath });
|
|
382
|
+
// Use -g to get URL, -f "best[ext=mp4]/best" to get best quality single file if possible
|
|
383
|
+
const args = [
|
|
384
|
+
url,
|
|
385
|
+
'-g',
|
|
386
|
+
'-f', 'best[ext=mp4]/best'
|
|
387
|
+
];
|
|
388
|
+
|
|
389
|
+
const result = await ytdlp.execAsync(args);
|
|
390
|
+
// yt-dlp might return multiple lines if it finds separate audio/video streams
|
|
391
|
+
// We take the first one, but for Twitter it is usually a single mp4 file
|
|
392
|
+
const videoUrl = result.trim().split('\n')[0];
|
|
393
|
+
|
|
394
|
+
if (videoUrl && videoUrl.startsWith('http')) {
|
|
395
|
+
resolve(videoUrl);
|
|
396
|
+
} else {
|
|
397
|
+
reject(new Error('yt-dlp did not return a valid URL'));
|
|
398
|
+
}
|
|
399
|
+
} catch (error) {
|
|
400
|
+
reject(error);
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
377
405
|
// Enhanced fallback download function with platform-specific methods
|
|
378
406
|
async function tryFallbackDownload(url, maxRetries = 3) {
|
|
379
407
|
const platform = getPlatformType(url);
|
|
380
|
-
|
|
408
|
+
|
|
381
409
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
382
410
|
try {
|
|
383
411
|
let videoUrl = null;
|
|
384
|
-
|
|
412
|
+
|
|
385
413
|
// Platform-specific fallback methods
|
|
386
414
|
switch (platform) {
|
|
387
415
|
case "instagram": {
|
|
@@ -427,7 +455,7 @@ async function tryFallbackDownload(url, maxRetries = 3) {
|
|
|
427
455
|
}
|
|
428
456
|
}
|
|
429
457
|
];
|
|
430
|
-
|
|
458
|
+
|
|
431
459
|
for (const method of methods) {
|
|
432
460
|
try {
|
|
433
461
|
videoUrl = await method();
|
|
@@ -438,7 +466,7 @@ async function tryFallbackDownload(url, maxRetries = 3) {
|
|
|
438
466
|
}
|
|
439
467
|
break;
|
|
440
468
|
}
|
|
441
|
-
|
|
469
|
+
|
|
442
470
|
case "tiktok": {
|
|
443
471
|
// Try multiple TikTok methods
|
|
444
472
|
const methods = [
|
|
@@ -473,7 +501,7 @@ async function tryFallbackDownload(url, maxRetries = 3) {
|
|
|
473
501
|
throw new Error("No valid video in Tiktok.Downloader v2 response");
|
|
474
502
|
}
|
|
475
503
|
];
|
|
476
|
-
|
|
504
|
+
|
|
477
505
|
for (const method of methods) {
|
|
478
506
|
try {
|
|
479
507
|
videoUrl = await method();
|
|
@@ -484,7 +512,7 @@ async function tryFallbackDownload(url, maxRetries = 3) {
|
|
|
484
512
|
}
|
|
485
513
|
break;
|
|
486
514
|
}
|
|
487
|
-
|
|
515
|
+
|
|
488
516
|
case "facebook": {
|
|
489
517
|
// Try multiple Facebook methods
|
|
490
518
|
const methods = [
|
|
@@ -508,7 +536,7 @@ async function tryFallbackDownload(url, maxRetries = 3) {
|
|
|
508
536
|
throw new Error("No valid video in fbdown response with custom headers");
|
|
509
537
|
}
|
|
510
538
|
];
|
|
511
|
-
|
|
539
|
+
|
|
512
540
|
for (const method of methods) {
|
|
513
541
|
try {
|
|
514
542
|
videoUrl = await method();
|
|
@@ -519,42 +547,46 @@ async function tryFallbackDownload(url, maxRetries = 3) {
|
|
|
519
547
|
}
|
|
520
548
|
break;
|
|
521
549
|
}
|
|
522
|
-
|
|
550
|
+
|
|
523
551
|
case "twitter": {
|
|
524
|
-
// Try
|
|
552
|
+
// Try using yt-dlp first (Most reliable), then fallback to existing methods
|
|
525
553
|
const methods = [
|
|
554
|
+
async () => {
|
|
555
|
+
// Priority 1: yt-dlp
|
|
556
|
+
return await downloadTwitterWithYtDlp(url);
|
|
557
|
+
},
|
|
526
558
|
async () => {
|
|
527
559
|
const cleanUrl = validateTwitterUrl(url);
|
|
528
560
|
const data = await TwitterDL(cleanUrl, {});
|
|
529
|
-
|
|
561
|
+
|
|
530
562
|
// Check multiple possible response structures
|
|
531
563
|
if (data && data.result) {
|
|
532
564
|
// Structure 1: data.result.media[0].videos[0].url
|
|
533
|
-
if (data.result.media && data.result.media[0] &&
|
|
534
|
-
|
|
535
|
-
|
|
565
|
+
if (data.result.media && data.result.media[0] &&
|
|
566
|
+
data.result.media[0].videos && data.result.media[0].videos[0] &&
|
|
567
|
+
data.result.media[0].videos[0].url) {
|
|
536
568
|
return data.result.media[0].videos[0].url;
|
|
537
569
|
}
|
|
538
|
-
|
|
570
|
+
|
|
539
571
|
// Structure 2: data.result.video[0].url
|
|
540
572
|
if (data.result.video && data.result.video[0] && data.result.video[0].url) {
|
|
541
573
|
return data.result.video[0].url;
|
|
542
574
|
}
|
|
543
|
-
|
|
575
|
+
|
|
544
576
|
// Structure 3: data.result.url (direct URL)
|
|
545
577
|
if (data.result.url) {
|
|
546
578
|
return data.result.url;
|
|
547
579
|
}
|
|
548
|
-
|
|
580
|
+
|
|
549
581
|
// Structure 4: data.result.media[0].url
|
|
550
582
|
if (data.result.media && data.result.media[0] && data.result.media[0].url) {
|
|
551
583
|
return data.result.media[0].url;
|
|
552
584
|
}
|
|
553
|
-
|
|
585
|
+
|
|
554
586
|
// Structure 5: Check for any video URL in the entire response
|
|
555
587
|
const findVideoUrl = (obj) => {
|
|
556
|
-
if (typeof obj === 'string' && obj.includes('http') &&
|
|
557
|
-
|
|
588
|
+
if (typeof obj === 'string' && obj.includes('http') &&
|
|
589
|
+
(obj.includes('.mp4') || obj.includes('video') || obj.includes('media'))) {
|
|
558
590
|
return obj;
|
|
559
591
|
}
|
|
560
592
|
if (typeof obj === 'object' && obj !== null) {
|
|
@@ -565,13 +597,13 @@ async function tryFallbackDownload(url, maxRetries = 3) {
|
|
|
565
597
|
}
|
|
566
598
|
return null;
|
|
567
599
|
};
|
|
568
|
-
|
|
600
|
+
|
|
569
601
|
const foundUrl = findVideoUrl(data.result);
|
|
570
602
|
if (foundUrl) {
|
|
571
603
|
return foundUrl;
|
|
572
604
|
}
|
|
573
605
|
}
|
|
574
|
-
|
|
606
|
+
|
|
575
607
|
throw new Error("No valid video URL found in TwitterDL response structure");
|
|
576
608
|
},
|
|
577
609
|
async () => {
|
|
@@ -587,30 +619,30 @@ async function tryFallbackDownload(url, maxRetries = 3) {
|
|
|
587
619
|
'Upgrade-Insecure-Requests': '1'
|
|
588
620
|
}
|
|
589
621
|
});
|
|
590
|
-
|
|
622
|
+
|
|
591
623
|
// Same structure checking as above
|
|
592
624
|
if (data && data.result) {
|
|
593
|
-
if (data.result.media && data.result.media[0] &&
|
|
594
|
-
|
|
595
|
-
|
|
625
|
+
if (data.result.media && data.result.media[0] &&
|
|
626
|
+
data.result.media[0].videos && data.result.media[0].videos[0] &&
|
|
627
|
+
data.result.media[0].videos[0].url) {
|
|
596
628
|
return data.result.media[0].videos[0].url;
|
|
597
629
|
}
|
|
598
|
-
|
|
630
|
+
|
|
599
631
|
if (data.result.video && data.result.video[0] && data.result.video[0].url) {
|
|
600
632
|
return data.result.video[0].url;
|
|
601
633
|
}
|
|
602
|
-
|
|
634
|
+
|
|
603
635
|
if (data.result.url) {
|
|
604
636
|
return data.result.url;
|
|
605
637
|
}
|
|
606
|
-
|
|
638
|
+
|
|
607
639
|
if (data.result.media && data.result.media[0] && data.result.media[0].url) {
|
|
608
640
|
return data.result.media[0].url;
|
|
609
641
|
}
|
|
610
|
-
|
|
642
|
+
|
|
611
643
|
const findVideoUrl = (obj) => {
|
|
612
|
-
if (typeof obj === 'string' && obj.includes('http') &&
|
|
613
|
-
|
|
644
|
+
if (typeof obj === 'string' && obj.includes('http') &&
|
|
645
|
+
(obj.includes('.mp4') || obj.includes('video') || obj.includes('media'))) {
|
|
614
646
|
return obj;
|
|
615
647
|
}
|
|
616
648
|
if (typeof obj === 'object' && obj !== null) {
|
|
@@ -621,13 +653,13 @@ async function tryFallbackDownload(url, maxRetries = 3) {
|
|
|
621
653
|
}
|
|
622
654
|
return null;
|
|
623
655
|
};
|
|
624
|
-
|
|
656
|
+
|
|
625
657
|
const foundUrl = findVideoUrl(data.result);
|
|
626
658
|
if (foundUrl) {
|
|
627
659
|
return foundUrl;
|
|
628
660
|
}
|
|
629
661
|
}
|
|
630
|
-
|
|
662
|
+
|
|
631
663
|
throw new Error("No valid video URL found in TwitterDL response with custom headers");
|
|
632
664
|
},
|
|
633
665
|
async () => {
|
|
@@ -663,9 +695,9 @@ async function tryFallbackDownload(url, maxRetries = 3) {
|
|
|
663
695
|
const data = await btchOld[funcName](cleanUrl);
|
|
664
696
|
// Check multiple possible response structures
|
|
665
697
|
if (data && data.result) {
|
|
666
|
-
if (data.result.media && data.result.media[0] &&
|
|
667
|
-
|
|
668
|
-
|
|
698
|
+
if (data.result.media && data.result.media[0] &&
|
|
699
|
+
data.result.media[0].videos && data.result.media[0].videos[0] &&
|
|
700
|
+
data.result.media[0].videos[0].url) {
|
|
669
701
|
return data.result.media[0].videos[0].url;
|
|
670
702
|
}
|
|
671
703
|
if (data.result.video && data.result.video[0] && data.result.video[0].url) {
|
|
@@ -692,7 +724,7 @@ async function tryFallbackDownload(url, maxRetries = 3) {
|
|
|
692
724
|
}
|
|
693
725
|
}
|
|
694
726
|
];
|
|
695
|
-
|
|
727
|
+
|
|
696
728
|
for (const method of methods) {
|
|
697
729
|
try {
|
|
698
730
|
videoUrl = await method();
|
|
@@ -703,12 +735,12 @@ async function tryFallbackDownload(url, maxRetries = 3) {
|
|
|
703
735
|
}
|
|
704
736
|
break;
|
|
705
737
|
}
|
|
706
|
-
|
|
738
|
+
|
|
707
739
|
case "youtube": {
|
|
708
740
|
// YouTube doesn't need fallback - it has its own specific method
|
|
709
741
|
throw new Error("YouTube downloads should use the primary method, not fallback");
|
|
710
742
|
}
|
|
711
|
-
|
|
743
|
+
|
|
712
744
|
case "mediafire": {
|
|
713
745
|
// Try MediaFire method
|
|
714
746
|
try {
|
|
@@ -746,10 +778,10 @@ async function tryFallbackDownload(url, maxRetries = 3) {
|
|
|
746
778
|
};
|
|
747
779
|
mediaUrl = pickFrom(data);
|
|
748
780
|
if (mediaUrl) return mediaUrl;
|
|
749
|
-
} catch (_) {}
|
|
781
|
+
} catch (_) { }
|
|
750
782
|
break;
|
|
751
783
|
}
|
|
752
|
-
|
|
784
|
+
|
|
753
785
|
default: {
|
|
754
786
|
// Generic fallback for unknown platforms
|
|
755
787
|
try {
|
|
@@ -765,18 +797,18 @@ async function tryFallbackDownload(url, maxRetries = 3) {
|
|
|
765
797
|
break;
|
|
766
798
|
}
|
|
767
799
|
}
|
|
768
|
-
|
|
800
|
+
|
|
769
801
|
// If we got a video URL, validate it
|
|
770
802
|
if (videoUrl && videoUrl.includes("http")) {
|
|
771
803
|
// Validate the URL is accessible
|
|
772
804
|
try {
|
|
773
|
-
const response = await axios.head(videoUrl, {
|
|
805
|
+
const response = await axios.head(videoUrl, {
|
|
774
806
|
timeout: 10000,
|
|
775
807
|
headers: {
|
|
776
808
|
'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'
|
|
777
809
|
}
|
|
778
810
|
});
|
|
779
|
-
|
|
811
|
+
|
|
780
812
|
if (response.status === 200) {
|
|
781
813
|
return videoUrl;
|
|
782
814
|
} else {
|
|
@@ -788,7 +820,7 @@ async function tryFallbackDownload(url, maxRetries = 3) {
|
|
|
788
820
|
} else {
|
|
789
821
|
throw new Error("No valid video URL obtained from any fallback method");
|
|
790
822
|
}
|
|
791
|
-
|
|
823
|
+
|
|
792
824
|
} catch (error) {
|
|
793
825
|
if (attempt === maxRetries) {
|
|
794
826
|
throw new Error(`Enhanced fallback method failed after ${maxRetries} attempts for ${platform}: ${error.message}`);
|
|
@@ -835,9 +867,8 @@ const MediaDownloader = async (url, options = {}) => {
|
|
|
835
867
|
|
|
836
868
|
// Verificar se é YouTube e lançar erro customizado
|
|
837
869
|
const platform = getPlatformType(url);
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
}
|
|
870
|
+
// YouTube is now supported
|
|
871
|
+
|
|
841
872
|
|
|
842
873
|
await cleanupTempFiles(); // Clean up previous temp files
|
|
843
874
|
|
|
@@ -849,21 +880,21 @@ const MediaDownloader = async (url, options = {}) => {
|
|
|
849
880
|
downloadedFilePath = await downloadSmartVideo(url, config);
|
|
850
881
|
} catch (error) {
|
|
851
882
|
const platform = getPlatformType(url);
|
|
852
|
-
|
|
853
|
-
// YouTube doesn't use fallback method - it has its own specific implementation
|
|
883
|
+
|
|
884
|
+
// YouTube doesn't use fallback method - it has its own specific implementation in downloadSmartVideo
|
|
854
885
|
if (platform === "youtube") {
|
|
855
886
|
throw new Error(`YouTube download failed: ${error.message}`);
|
|
856
887
|
}
|
|
857
|
-
|
|
888
|
+
|
|
858
889
|
try {
|
|
859
890
|
const fallbackUrl = await tryFallbackDownload(url);
|
|
860
|
-
|
|
891
|
+
|
|
861
892
|
// Validate fallback URL
|
|
862
893
|
const isValid = await validateVideoUrl(fallbackUrl, platform);
|
|
863
894
|
if (!isValid) {
|
|
864
895
|
throw new Error("Fallback URL validation failed");
|
|
865
896
|
}
|
|
866
|
-
|
|
897
|
+
|
|
867
898
|
downloadedFilePath = await downloadDirectVideo(fallbackUrl, config);
|
|
868
899
|
} catch (fallbackError) {
|
|
869
900
|
throw new Error(`Failed to download video with both methods: ${fallbackError.message}`);
|
|
@@ -986,15 +1017,15 @@ async function downloadSmartVideo(url, config) {
|
|
|
986
1017
|
throw new Error("No video URLs found in Facebook response");
|
|
987
1018
|
}
|
|
988
1019
|
videoUrl = data.Normal_video || data.HD;
|
|
989
|
-
|
|
1020
|
+
|
|
990
1021
|
// Validate the URL is accessible
|
|
991
|
-
const response = await axios.head(videoUrl, {
|
|
1022
|
+
const response = await axios.head(videoUrl, {
|
|
992
1023
|
timeout: 10000,
|
|
993
1024
|
headers: {
|
|
994
1025
|
'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'
|
|
995
1026
|
}
|
|
996
1027
|
}).catch(() => null);
|
|
997
|
-
|
|
1028
|
+
|
|
998
1029
|
if (!response || response.status !== 200) {
|
|
999
1030
|
throw new Error("Facebook video URL is not accessible");
|
|
1000
1031
|
}
|
|
@@ -1066,12 +1097,12 @@ async function downloadSmartVideo(url, config) {
|
|
|
1066
1097
|
// Validate and clean the Twitter URL
|
|
1067
1098
|
const cleanUrl = validateTwitterUrl(url);
|
|
1068
1099
|
const data = await TwitterDL(cleanUrl, {});
|
|
1069
|
-
|
|
1100
|
+
|
|
1070
1101
|
if (data && data.result) {
|
|
1071
1102
|
// Try multiple possible response structures
|
|
1072
|
-
if (data.result.media && data.result.media[0] &&
|
|
1073
|
-
|
|
1074
|
-
|
|
1103
|
+
if (data.result.media && data.result.media[0] &&
|
|
1104
|
+
data.result.media[0].videos && data.result.media[0].videos[0] &&
|
|
1105
|
+
data.result.media[0].videos[0].url) {
|
|
1075
1106
|
videoUrl = data.result.media[0].videos[0].url;
|
|
1076
1107
|
} else if (data.result.video && data.result.video[0] && data.result.video[0].url) {
|
|
1077
1108
|
videoUrl = data.result.video[0].url;
|
|
@@ -1082,8 +1113,8 @@ async function downloadSmartVideo(url, config) {
|
|
|
1082
1113
|
} else {
|
|
1083
1114
|
// Search for any video URL in the response
|
|
1084
1115
|
const findVideoUrl = (obj) => {
|
|
1085
|
-
if (typeof obj === 'string' && obj.includes('http') &&
|
|
1086
|
-
|
|
1116
|
+
if (typeof obj === 'string' && obj.includes('http') &&
|
|
1117
|
+
(obj.includes('.mp4') || obj.includes('video') || obj.includes('media'))) {
|
|
1087
1118
|
return obj;
|
|
1088
1119
|
}
|
|
1089
1120
|
if (typeof obj === 'object' && obj !== null) {
|
|
@@ -1094,7 +1125,7 @@ async function downloadSmartVideo(url, config) {
|
|
|
1094
1125
|
}
|
|
1095
1126
|
return null;
|
|
1096
1127
|
};
|
|
1097
|
-
|
|
1128
|
+
|
|
1098
1129
|
const foundUrl = findVideoUrl(data.result);
|
|
1099
1130
|
if (foundUrl) {
|
|
1100
1131
|
videoUrl = foundUrl;
|
|
@@ -1110,6 +1141,56 @@ async function downloadSmartVideo(url, config) {
|
|
|
1110
1141
|
}
|
|
1111
1142
|
break;
|
|
1112
1143
|
}
|
|
1144
|
+
case "youtube": {
|
|
1145
|
+
try {
|
|
1146
|
+
const data = await youtube(url);
|
|
1147
|
+
// Try to find the video URL in the response
|
|
1148
|
+
// Common patterns: data.url, data.mp4, data.link, or direct string
|
|
1149
|
+
|
|
1150
|
+
if (!data) throw new Error("No data returned from youtube downloader");
|
|
1151
|
+
|
|
1152
|
+
// Recursive function to find a valid video URL in the object
|
|
1153
|
+
const findVideoUrl = (obj) => {
|
|
1154
|
+
if (!obj) return null;
|
|
1155
|
+
if (typeof obj === 'string' && obj.startsWith('http')) return obj;
|
|
1156
|
+
|
|
1157
|
+
if (Array.isArray(obj)) {
|
|
1158
|
+
for (const item of obj) {
|
|
1159
|
+
const found = findVideoUrl(item);
|
|
1160
|
+
if (found) return found;
|
|
1161
|
+
}
|
|
1162
|
+
} else if (typeof obj === 'object') {
|
|
1163
|
+
// Prioritize certain keys
|
|
1164
|
+
const priorities = ['url', 'link', 'download', 'mp4', 'video', 'src'];
|
|
1165
|
+
for (const key of priorities) {
|
|
1166
|
+
if (obj[key]) {
|
|
1167
|
+
const found = findVideoUrl(obj[key]);
|
|
1168
|
+
if (found) return found;
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
// If not found in priorities, check all keys
|
|
1172
|
+
for (const key of Object.keys(obj)) {
|
|
1173
|
+
if (!priorities.includes(key)) {
|
|
1174
|
+
const found = findVideoUrl(obj[key]);
|
|
1175
|
+
if (found) return found;
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
return null;
|
|
1180
|
+
};
|
|
1181
|
+
|
|
1182
|
+
videoUrl = findVideoUrl(data);
|
|
1183
|
+
|
|
1184
|
+
if (!videoUrl) {
|
|
1185
|
+
console.log('YouTube data dump:', JSON.stringify(data, null, 2)); // Debug log since we can't test
|
|
1186
|
+
throw new Error("Could not extract video URL from YouTube response");
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
} catch (error) {
|
|
1190
|
+
throw new Error(`YouTube download failed: ${error.message}`);
|
|
1191
|
+
}
|
|
1192
|
+
break;
|
|
1193
|
+
}
|
|
1113
1194
|
default:
|
|
1114
1195
|
throw new Error("Platform not supported or invalid link.");
|
|
1115
1196
|
}
|
|
@@ -1117,21 +1198,21 @@ async function downloadSmartVideo(url, config) {
|
|
|
1117
1198
|
if (!videoUrl || !videoUrl.includes("http")) {
|
|
1118
1199
|
throw new Error("Returned video URL is invalid or unavailable.");
|
|
1119
1200
|
}
|
|
1120
|
-
|
|
1201
|
+
|
|
1121
1202
|
// Download the video with better error handling
|
|
1122
|
-
const response = await axios({
|
|
1123
|
-
url: videoUrl,
|
|
1124
|
-
method: "GET",
|
|
1203
|
+
const response = await axios({
|
|
1204
|
+
url: videoUrl,
|
|
1205
|
+
method: "GET",
|
|
1125
1206
|
responseType: "stream",
|
|
1126
1207
|
timeout: 30000,
|
|
1127
1208
|
headers: {
|
|
1128
1209
|
'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'
|
|
1129
1210
|
}
|
|
1130
1211
|
});
|
|
1131
|
-
|
|
1212
|
+
|
|
1132
1213
|
// Create minimal unique file name in output dir
|
|
1133
|
-
let fileName = path.join(OUTPUT_DIR, `v_${uuidv4().slice(0,4)}.mp4`);
|
|
1134
|
-
|
|
1214
|
+
let fileName = path.join(OUTPUT_DIR, `v_${uuidv4().slice(0, 4)}.mp4`);
|
|
1215
|
+
|
|
1135
1216
|
const videoWriter = fs.createWriteStream(fileName);
|
|
1136
1217
|
response.data.pipe(videoWriter);
|
|
1137
1218
|
|
|
@@ -1156,19 +1237,19 @@ async function downloadSmartVideo(url, config) {
|
|
|
1156
1237
|
// Function for direct video download
|
|
1157
1238
|
async function downloadDirectVideo(url, config) {
|
|
1158
1239
|
try {
|
|
1159
|
-
const response = await axios({
|
|
1160
|
-
url: url,
|
|
1161
|
-
method: "GET",
|
|
1240
|
+
const response = await axios({
|
|
1241
|
+
url: url,
|
|
1242
|
+
method: "GET",
|
|
1162
1243
|
responseType: "stream",
|
|
1163
1244
|
timeout: 30000,
|
|
1164
1245
|
headers: {
|
|
1165
1246
|
'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'
|
|
1166
1247
|
}
|
|
1167
1248
|
});
|
|
1168
|
-
|
|
1249
|
+
|
|
1169
1250
|
// Create minimal unique file name in output dir
|
|
1170
|
-
let fileName = path.join(OUTPUT_DIR, `v_${uuidv4().slice(0,4)}.mp4`);
|
|
1171
|
-
|
|
1251
|
+
let fileName = path.join(OUTPUT_DIR, `v_${uuidv4().slice(0, 4)}.mp4`);
|
|
1252
|
+
|
|
1172
1253
|
const videoWriter = fs.createWriteStream(fileName);
|
|
1173
1254
|
response.data.pipe(videoWriter);
|
|
1174
1255
|
|
|
@@ -1216,7 +1297,7 @@ async function downloadGenericFile(url, preferredExt = null) {
|
|
|
1216
1297
|
else ext = 'bin';
|
|
1217
1298
|
}
|
|
1218
1299
|
|
|
1219
|
-
const fileName = path.join(OUTPUT_DIR, `v_${uuidv4().slice(0,4)}.${ext}`);
|
|
1300
|
+
const fileName = path.join(OUTPUT_DIR, `v_${uuidv4().slice(0, 4)}.${ext}`);
|
|
1220
1301
|
const writer = fs.createWriteStream(fileName);
|
|
1221
1302
|
response.data.pipe(writer);
|
|
1222
1303
|
|
|
@@ -1231,7 +1312,7 @@ async function downloadGenericFile(url, preferredExt = null) {
|
|
|
1231
1312
|
|
|
1232
1313
|
// Function to rotate video
|
|
1233
1314
|
async function rotateVideo(fileName, rotation) {
|
|
1234
|
-
const outputPath = path.join(OUTPUT_DIR, `vr_${uuidv4().slice(0,4)}.mp4`);
|
|
1315
|
+
const outputPath = path.join(OUTPUT_DIR, `vr_${uuidv4().slice(0, 4)}.mp4`);
|
|
1235
1316
|
let angle;
|
|
1236
1317
|
switch (rotation.toLowerCase()) {
|
|
1237
1318
|
case "left": angle = "transpose=2"; break;
|
|
@@ -1282,8 +1363,8 @@ async function cleanupTempFiles() {
|
|
|
1282
1363
|
try {
|
|
1283
1364
|
if (!fs.existsSync(TEMP_DIR)) return;
|
|
1284
1365
|
const files = fs.readdirSync(TEMP_DIR);
|
|
1285
|
-
const tempFiles = files.filter(file =>
|
|
1286
|
-
/^temp_video.*\.mp4$/.test(file) ||
|
|
1366
|
+
const tempFiles = files.filter(file =>
|
|
1367
|
+
/^temp_video.*\.mp4$/.test(file) ||
|
|
1287
1368
|
/_audio\.mp3$/.test(file) ||
|
|
1288
1369
|
/_rotated\.mp4$/.test(file) ||
|
|
1289
1370
|
/_cropped\.mp4$/.test(file) ||
|
|
@@ -1315,9 +1396,9 @@ async function cleanupOutputFiles() {
|
|
|
1315
1396
|
if (now - st.mtimeMs > maxAgeMs) {
|
|
1316
1397
|
await safeUnlinkWithRetry(full);
|
|
1317
1398
|
}
|
|
1318
|
-
} catch (_) {}
|
|
1399
|
+
} catch (_) { }
|
|
1319
1400
|
}
|
|
1320
|
-
} catch (_) {}
|
|
1401
|
+
} catch (_) { }
|
|
1321
1402
|
}
|
|
1322
1403
|
|
|
1323
1404
|
// Schedule periodic cleanup (every 10 minutes)
|
|
@@ -1331,7 +1412,7 @@ cleanupOutputFiles();
|
|
|
1331
1412
|
// Function to auto-crop video
|
|
1332
1413
|
async function autoCrop(fileName) {
|
|
1333
1414
|
const inputPath = fileName;
|
|
1334
|
-
const outputPath = path.join(OUTPUT_DIR, `vc_${uuidv4().slice(0,4)}.mp4`);
|
|
1415
|
+
const outputPath = path.join(OUTPUT_DIR, `vc_${uuidv4().slice(0, 4)}.mp4`);
|
|
1335
1416
|
|
|
1336
1417
|
return new Promise((resolve, reject) => {
|
|
1337
1418
|
let cropValues = null;
|
|
@@ -1339,13 +1420,13 @@ async function autoCrop(fileName) {
|
|
|
1339
1420
|
.outputOptions('-vf', 'cropdetect=24:16:0')
|
|
1340
1421
|
.outputFormat('null')
|
|
1341
1422
|
.output('-')
|
|
1342
|
-
.on('stderr', function(stderrLine) {
|
|
1423
|
+
.on('stderr', function (stderrLine) {
|
|
1343
1424
|
const cropMatch = stderrLine.match(/crop=([0-9]+):([0-9]+):([0-9]+):([0-9]+)/);
|
|
1344
1425
|
if (cropMatch) {
|
|
1345
1426
|
cropValues = `crop=${cropMatch[1]}:${cropMatch[2]}:${cropMatch[3]}:${cropMatch[4]}`;
|
|
1346
1427
|
}
|
|
1347
1428
|
})
|
|
1348
|
-
.on('end', function() {
|
|
1429
|
+
.on('end', function () {
|
|
1349
1430
|
if (!cropValues) {
|
|
1350
1431
|
resolve(inputPath);
|
|
1351
1432
|
return;
|
|
@@ -1378,7 +1459,7 @@ async function checkAndCompressVideo(filePath, limitSizeMB, platform = null) {
|
|
|
1378
1459
|
return filePath;
|
|
1379
1460
|
}
|
|
1380
1461
|
|
|
1381
|
-
const outputPath = path.join(OUTPUT_DIR, `vx_${uuidv4().slice(0,4)}.mp4`);
|
|
1462
|
+
const outputPath = path.join(OUTPUT_DIR, `vx_${uuidv4().slice(0, 4)}.mp4`);
|
|
1382
1463
|
|
|
1383
1464
|
return new Promise((resolve, reject) => {
|
|
1384
1465
|
ffmpeg(filePath)
|
|
@@ -1413,17 +1494,17 @@ async function convertVideoFormat(inputPath, targetFormat) {
|
|
|
1413
1494
|
const supported = ["mp4", "mov", "webm", "mkv"];
|
|
1414
1495
|
if (!supported.includes(fmt)) return inputPath;
|
|
1415
1496
|
|
|
1416
|
-
const outputPath = path.join(OUTPUT_DIR, `vf_${uuidv4().slice(0,4)}.${fmt}`);
|
|
1497
|
+
const outputPath = path.join(OUTPUT_DIR, `vf_${uuidv4().slice(0, 4)}.${fmt}`);
|
|
1417
1498
|
|
|
1418
1499
|
const ff = ffmpeg(inputPath);
|
|
1419
1500
|
switch (fmt) {
|
|
1420
1501
|
case "mp4":
|
|
1421
1502
|
case "mov":
|
|
1422
1503
|
case "mkv":
|
|
1423
|
-
ff.outputOptions("-c:v","libx264","-pix_fmt","yuv420p","-c:a","aac","-b:a","192k");
|
|
1504
|
+
ff.outputOptions("-c:v", "libx264", "-pix_fmt", "yuv420p", "-c:a", "aac", "-b:a", "192k");
|
|
1424
1505
|
break;
|
|
1425
1506
|
case "webm":
|
|
1426
|
-
ff.outputOptions("-c:v","libvpx-vp9","-b:v","0","-crf","30","-c:a","libopus","-b:a","128k");
|
|
1507
|
+
ff.outputOptions("-c:v", "libvpx-vp9", "-b:v", "0", "-crf", "30", "-c:a", "libopus", "-b:a", "128k");
|
|
1427
1508
|
break;
|
|
1428
1509
|
default:
|
|
1429
1510
|
return inputPath;
|
|
@@ -1480,14 +1561,14 @@ async function unshortenUrl(url) {
|
|
|
1480
1561
|
return status >= 200 && status < 400; // Accept redirects
|
|
1481
1562
|
}
|
|
1482
1563
|
});
|
|
1483
|
-
|
|
1564
|
+
|
|
1484
1565
|
// Get the final URL after all redirects
|
|
1485
1566
|
const finalUrl = response.request.res.responseUrl || response.config.url;
|
|
1486
1567
|
return finalUrl;
|
|
1487
1568
|
}
|
|
1488
|
-
|
|
1569
|
+
|
|
1489
1570
|
// For other URLs, use the original method
|
|
1490
|
-
const response = await axios.head(url, {
|
|
1571
|
+
const response = await axios.head(url, {
|
|
1491
1572
|
maxRedirects: 10,
|
|
1492
1573
|
timeout: 10000,
|
|
1493
1574
|
headers: {
|
|
@@ -1556,10 +1637,10 @@ const AudioDownloader = async (url, options = {}) => {
|
|
|
1556
1637
|
if (downloadedFilePath) {
|
|
1557
1638
|
// Extrair áudio em mp3
|
|
1558
1639
|
audioFilePath = await extractAudioMp3(downloadedFilePath);
|
|
1559
|
-
|
|
1640
|
+
|
|
1560
1641
|
// Remove o arquivo de vídeo temporário após extrair o áudio
|
|
1561
1642
|
await safeUnlinkWithRetry(downloadedFilePath);
|
|
1562
|
-
|
|
1643
|
+
|
|
1563
1644
|
const result = await uploadToGoFileIfNeeded(audioFilePath);
|
|
1564
1645
|
return result;
|
|
1565
1646
|
} else {
|
|
@@ -1575,7 +1656,7 @@ const AudioDownloader = async (url, options = {}) => {
|
|
|
1575
1656
|
// Função para extrair áudio em mp3 usando ffmpeg
|
|
1576
1657
|
async function extractAudioMp3(videoPath) {
|
|
1577
1658
|
return new Promise((resolve, reject) => {
|
|
1578
|
-
const audioPath = path.join(OUTPUT_DIR, `a_${uuidv4().slice(0,4)}.mp3`);
|
|
1659
|
+
const audioPath = path.join(OUTPUT_DIR, `a_${uuidv4().slice(0, 4)}.mp3`);
|
|
1579
1660
|
ffmpeg(videoPath)
|
|
1580
1661
|
.noVideo()
|
|
1581
1662
|
.audioCodec('libmp3lame')
|