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.
Files changed (2) hide show
  1. package/index.js +204 -117
  2. 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("you112t12u.be") || lowerUrl.includes("m.y11outu314be.com")) return "youtu1354be";
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 multiple Twitter methods with better response handling
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
- data.result.media[0].videos && data.result.media[0].videos[0] &&
535
- data.result.media[0].videos[0].url) {
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
- (obj.includes('.mp4') || obj.includes('video') || obj.includes('media'))) {
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
- data.result.media[0].videos && data.result.media[0].videos[0] &&
595
- data.result.media[0].videos[0].url) {
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
- (obj.includes('.mp4') || obj.includes('video') || obj.includes('media'))) {
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
- data.result.media[0].videos && data.result.media[0].videos[0] &&
668
- data.result.media[0].videos[0].url) {
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
- if (platform === "youtube") {
839
- throw new Error("This URL is not from a supported platform. Supported platforms: Instagram, X(Twitter), TikTok, Facebook");
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
- data.result.media[0].videos && data.result.media[0].videos[0] &&
1074
- data.result.media[0].videos[0].url) {
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
- (obj.includes('.mp4') || obj.includes('video') || obj.includes('media'))) {
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.7",
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.20",
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",