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