frostpv 1.0.14 → 1.0.16
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 +290 -170
- package/package.json +6 -5
package/index.js
CHANGED
|
@@ -4,14 +4,15 @@ 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, twitter } = require("btch-downloader");
|
|
7
|
+
const { igdl, ttdl, fbdown, mediafire, capcut, youtube, gdrive, pinterest, twitter } = 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");
|
|
11
|
+
const ytdl = require("@distube/ytdl-core");
|
|
12
|
+
|
|
11
13
|
|
|
12
|
-
// Fallback libs
|
|
13
14
|
const Tiktok = require("@tobyg74/tiktok-api-dl");
|
|
14
|
-
const
|
|
15
|
+
const ab = require("ab-downloader");
|
|
15
16
|
const ffmpegPath = require('ffmpeg-ffprobe-static').ffmpegPath;
|
|
16
17
|
const { v4: uuidv4 } = require('uuid');
|
|
17
18
|
|
|
@@ -19,13 +20,10 @@ const pathToFfmpeg = require("ffmpeg-ffprobe-static");
|
|
|
19
20
|
const ffmpeg = require("fluent-ffmpeg");
|
|
20
21
|
ffmpeg.setFfmpegPath(pathToFfmpeg.ffmpegPath);
|
|
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
24
|
try { fs.mkdirSync(OUTPUT_DIR, { recursive: true }); } catch (_) { }
|
|
25
|
-
// Keep an OS temp directory for any future transient needs
|
|
26
25
|
const TEMP_DIR = path.join(os.tmpdir(), "downloader-dl-bot");
|
|
27
26
|
try { fs.mkdirSync(TEMP_DIR, { recursive: true }); } catch (_) { }
|
|
28
|
-
// TTL (minutes) for files in OUTPUT_DIR (audio/video) before they are pruned
|
|
29
27
|
const OUTPUT_RETENTION_MIN = Number(process.env.OUTPUT_RETENTION_MIN || 5);
|
|
30
28
|
|
|
31
29
|
const GOFILE_API = "Vs4e2PL8n65ExPz6wgDlqSY8kcEBRrzN";
|
|
@@ -46,6 +44,10 @@ const videoPlatforms = [
|
|
|
46
44
|
"https://fb.watch",
|
|
47
45
|
"https://www.fb.watch",
|
|
48
46
|
"https://fb.com",
|
|
47
|
+
"https://www.youtube.com",
|
|
48
|
+
"https://youtube.com",
|
|
49
|
+
"https://www.youtu.be",
|
|
50
|
+
"https://youtu.be",
|
|
49
51
|
"https://www.fb.com",
|
|
50
52
|
"https://web.facebook.com",
|
|
51
53
|
"https://www.mediafire.com",
|
|
@@ -85,8 +87,9 @@ const blacklistLink = (link) => {
|
|
|
85
87
|
|
|
86
88
|
const defaultConfig = {
|
|
87
89
|
autocrop: false,
|
|
88
|
-
limitSizeMB: null,
|
|
90
|
+
limitSizeMB: null,
|
|
89
91
|
rotation: null,
|
|
92
|
+
YTBmaxduration: 30,
|
|
90
93
|
outputFormat: null,
|
|
91
94
|
};
|
|
92
95
|
|
|
@@ -265,102 +268,6 @@ function getYoutubeCookiesPath() {
|
|
|
265
268
|
return null;
|
|
266
269
|
}
|
|
267
270
|
|
|
268
|
-
// Função utilitária para obter duração do vídeo/áudio em segundos
|
|
269
|
-
async function getYoutubeDurationSeconds(url) {
|
|
270
|
-
try {
|
|
271
|
-
const ytdlp = new YtDlp({ ffmpegPath });
|
|
272
|
-
const info = await ytdlp.getInfoAsync(url);
|
|
273
|
-
if (info && info.duration) return info.duration;
|
|
274
|
-
return null;
|
|
275
|
-
} catch (e) {
|
|
276
|
-
return null;
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// Função para baixar áudio do YouTube
|
|
281
|
-
async function downloadYoutubeAudio(url, outputPath) {
|
|
282
|
-
// Verificar duração máxima de 15 minutos
|
|
283
|
-
const duration = await getYoutubeDurationSeconds(url);
|
|
284
|
-
if (duration && duration > 900) {
|
|
285
|
-
throw new Error('The audio is longer than 15 minutes. Test limit: 15 minutes. This feature is still in beta.');
|
|
286
|
-
}
|
|
287
|
-
return new Promise(async (resolve, reject) => {
|
|
288
|
-
try {
|
|
289
|
-
const ytdlp = new YtDlp({ ffmpegPath });
|
|
290
|
-
const cookiesPath = getYoutubeCookiesPath();
|
|
291
|
-
|
|
292
|
-
// Garantir que o nome do arquivo tenha a extensão correta
|
|
293
|
-
const baseName = outputPath.replace(/\.[^/.]+$/, ""); // Remove extensão se existir
|
|
294
|
-
|
|
295
|
-
// Primeiro, tentar com a API de opções
|
|
296
|
-
try {
|
|
297
|
-
const options = {
|
|
298
|
-
format: {
|
|
299
|
-
filter: 'audioonly',
|
|
300
|
-
type: 'mp3',
|
|
301
|
-
quality: 'highestaudio'
|
|
302
|
-
},
|
|
303
|
-
output: outputPath
|
|
304
|
-
};
|
|
305
|
-
|
|
306
|
-
if (cookiesPath) {
|
|
307
|
-
options.cookies = cookiesPath;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// Remover logs detalhados para download de áudio do YouTube
|
|
311
|
-
const result = await ytdlp.downloadAsync(url, options);
|
|
312
|
-
// Verificar se o arquivo foi criado
|
|
313
|
-
const actualFileName = baseName + '.mp3';
|
|
314
|
-
if (fs.existsSync(actualFileName)) {
|
|
315
|
-
resolve(actualFileName);
|
|
316
|
-
return;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
// Tentar encontrar o arquivo com extensão diferente
|
|
320
|
-
const files = fs.readdirSync('./');
|
|
321
|
-
const downloadedFile = files.find(file => file.startsWith(baseName.split('/').pop()));
|
|
322
|
-
if (downloadedFile) {
|
|
323
|
-
resolve(downloadedFile);
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
|
-
} catch (apiError) {
|
|
327
|
-
console.log('API method failed, trying direct args method:', apiError.message);
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
// Fallback: usar argumentos diretos para máxima qualidade de áudio
|
|
331
|
-
const args = [
|
|
332
|
-
url,
|
|
333
|
-
'-f', 'bestaudio[ext=mp3]/bestaudio',
|
|
334
|
-
'-o', outputPath
|
|
335
|
-
];
|
|
336
|
-
|
|
337
|
-
if (cookiesPath) {
|
|
338
|
-
args.push('--cookies', cookiesPath);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
// Remover logs detalhados para download de áudio do YouTube
|
|
342
|
-
const result = await ytdlp.execAsync(args);
|
|
343
|
-
// Verificar se o arquivo foi criado
|
|
344
|
-
const actualFileName = baseName + '.mp3';
|
|
345
|
-
if (fs.existsSync(actualFileName)) {
|
|
346
|
-
resolve(actualFileName);
|
|
347
|
-
} else {
|
|
348
|
-
// Tentar encontrar o arquivo com extensão diferente
|
|
349
|
-
const files = fs.readdirSync('./');
|
|
350
|
-
const downloadedFile = files.find(file => file.startsWith(baseName.split('/').pop()));
|
|
351
|
-
if (downloadedFile) {
|
|
352
|
-
resolve(downloadedFile);
|
|
353
|
-
} else {
|
|
354
|
-
reject(new Error('YouTube audio download completed but file not found'));
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
} catch (error) {
|
|
358
|
-
console.error('YouTube audio download error:', error);
|
|
359
|
-
reject(new Error('This URL is not from a supported platform. Supported platforms: Instagram, X(Twitter), TikTok and Facebook'));
|
|
360
|
-
}
|
|
361
|
-
});
|
|
362
|
-
}
|
|
363
|
-
|
|
364
271
|
// Enhanced function to get platform type from URL
|
|
365
272
|
function getPlatformType(url) {
|
|
366
273
|
const lowerUrl = url.toLowerCase();
|
|
@@ -370,12 +277,11 @@ function getPlatformType(url) {
|
|
|
370
277
|
if (lowerUrl.includes("facebook.com") || lowerUrl.includes("fb.watch") || lowerUrl.includes("fb.com")) return "facebook";
|
|
371
278
|
if (lowerUrl.includes("mediafire.com")) return "mediafire";
|
|
372
279
|
if (lowerUrl.includes("x.com") || lowerUrl.includes("twitter.com")) return "twitter";
|
|
373
|
-
if (lowerUrl.includes("youtube.com") || lowerUrl.includes("
|
|
280
|
+
if (lowerUrl.includes("youtube.com") || lowerUrl.includes("youtu.be") || lowerUrl.includes("www.youtube.com")) return "youtube";
|
|
374
281
|
|
|
375
282
|
return "unknown";
|
|
376
283
|
}
|
|
377
284
|
|
|
378
|
-
// Enhanced fallback download function with platform-specific methods
|
|
379
285
|
async function tryFallbackDownload(url, maxRetries = 3) {
|
|
380
286
|
const platform = getPlatformType(url);
|
|
381
287
|
|
|
@@ -383,10 +289,8 @@ async function tryFallbackDownload(url, maxRetries = 3) {
|
|
|
383
289
|
try {
|
|
384
290
|
let videoUrl = null;
|
|
385
291
|
|
|
386
|
-
// Platform-specific fallback methods
|
|
387
292
|
switch (platform) {
|
|
388
293
|
case "instagram": {
|
|
389
|
-
// Try multiple Instagram methods
|
|
390
294
|
const methods = [
|
|
391
295
|
async () => {
|
|
392
296
|
const data = await igdl(url);
|
|
@@ -413,7 +317,6 @@ async function tryFallbackDownload(url, maxRetries = 3) {
|
|
|
413
317
|
throw new Error("No valid URL in igdl response with custom headers");
|
|
414
318
|
},
|
|
415
319
|
async () => {
|
|
416
|
-
// Fallback: try using the old version of btch-downloader
|
|
417
320
|
try {
|
|
418
321
|
const { igdl: igdlOld } = btchOld;
|
|
419
322
|
if (typeof igdlOld === 'function') {
|
|
@@ -422,10 +325,17 @@ async function tryFallbackDownload(url, maxRetries = 3) {
|
|
|
422
325
|
return data[0].url;
|
|
423
326
|
}
|
|
424
327
|
}
|
|
425
|
-
throw new Error("Old
|
|
328
|
+
throw new Error("Old downloader not available or failed");
|
|
426
329
|
} catch (oldError) {
|
|
427
|
-
throw new Error(`Old
|
|
330
|
+
throw new Error(`Old downloader fallback failed: ${oldError.message}`);
|
|
428
331
|
}
|
|
332
|
+
},
|
|
333
|
+
async () => {
|
|
334
|
+
const data = await ab.igdl(url);
|
|
335
|
+
if (data && Array.isArray(data) && data[0] && data[0].url) {
|
|
336
|
+
return data[0].url;
|
|
337
|
+
}
|
|
338
|
+
throw new Error("No valid URL");
|
|
429
339
|
}
|
|
430
340
|
];
|
|
431
341
|
|
|
@@ -441,7 +351,7 @@ async function tryFallbackDownload(url, maxRetries = 3) {
|
|
|
441
351
|
}
|
|
442
352
|
|
|
443
353
|
case "tiktok": {
|
|
444
|
-
|
|
354
|
+
|
|
445
355
|
const methods = [
|
|
446
356
|
async () => {
|
|
447
357
|
const data = await ttdl(url);
|
|
@@ -472,6 +382,13 @@ async function tryFallbackDownload(url, maxRetries = 3) {
|
|
|
472
382
|
}
|
|
473
383
|
}
|
|
474
384
|
throw new Error("No valid video in Tiktok.Downloader v2 response");
|
|
385
|
+
},
|
|
386
|
+
async () => {
|
|
387
|
+
const data = await ab.ttdl(url);
|
|
388
|
+
if (data && data.video && data.video[0]) {
|
|
389
|
+
return data.video[0];
|
|
390
|
+
}
|
|
391
|
+
throw new Error("No valid video");
|
|
475
392
|
}
|
|
476
393
|
];
|
|
477
394
|
|
|
@@ -507,6 +424,13 @@ async function tryFallbackDownload(url, maxRetries = 3) {
|
|
|
507
424
|
return data.Normal_video || data.HD;
|
|
508
425
|
}
|
|
509
426
|
throw new Error("No valid video in fbdown response with custom headers");
|
|
427
|
+
},
|
|
428
|
+
async () => {
|
|
429
|
+
const data = await ab.fbdown(url);
|
|
430
|
+
if (data && (data.Normal_video || data.HD)) {
|
|
431
|
+
return data.Normal_video || data.HD;
|
|
432
|
+
}
|
|
433
|
+
throw new Error("No valid video");
|
|
510
434
|
}
|
|
511
435
|
];
|
|
512
436
|
|
|
@@ -653,7 +577,6 @@ async function tryFallbackDownload(url, maxRetries = 3) {
|
|
|
653
577
|
}
|
|
654
578
|
},
|
|
655
579
|
async () => {
|
|
656
|
-
// Fallback: try using the old version of btch-downloader for Twitter/X
|
|
657
580
|
try {
|
|
658
581
|
const cleanUrl = validateTwitterUrl(url);
|
|
659
582
|
// Try to find any Twitter-related function in the old version
|
|
@@ -687,10 +610,20 @@ async function tryFallbackDownload(url, maxRetries = 3) {
|
|
|
687
610
|
}
|
|
688
611
|
}
|
|
689
612
|
}
|
|
690
|
-
throw new Error("No Twitter functions found
|
|
613
|
+
throw new Error("No Twitter functions found");
|
|
691
614
|
} catch (oldError) {
|
|
692
|
-
throw new Error(`
|
|
615
|
+
throw new Error(`Twitter fallback failed: ${oldError.message}`);
|
|
693
616
|
}
|
|
617
|
+
},
|
|
618
|
+
async () => {
|
|
619
|
+
const data = await ab.twitter(url);
|
|
620
|
+
if (data && data.result && data.result.url) {
|
|
621
|
+
return data.result.url;
|
|
622
|
+
}
|
|
623
|
+
if (data && Array.isArray(data) && data[0] && data[0].url) {
|
|
624
|
+
return data[0].url;
|
|
625
|
+
}
|
|
626
|
+
throw new Error("No valid video");
|
|
694
627
|
}
|
|
695
628
|
];
|
|
696
629
|
|
|
@@ -706,12 +639,10 @@ async function tryFallbackDownload(url, maxRetries = 3) {
|
|
|
706
639
|
}
|
|
707
640
|
|
|
708
641
|
case "youtube": {
|
|
709
|
-
// YouTube doesn't need fallback - it has its own specific method
|
|
710
642
|
throw new Error("YouTube downloads should use the primary method, not fallback");
|
|
711
643
|
}
|
|
712
644
|
|
|
713
645
|
case "mediafire": {
|
|
714
|
-
// Try MediaFire method
|
|
715
646
|
try {
|
|
716
647
|
const data = await mediafire(url);
|
|
717
648
|
if (data && data.url) {
|
|
@@ -720,7 +651,6 @@ async function tryFallbackDownload(url, maxRetries = 3) {
|
|
|
720
651
|
throw new Error("No valid URL in mediafire response");
|
|
721
652
|
}
|
|
722
653
|
} catch (methodError) {
|
|
723
|
-
// Continue to next attempt
|
|
724
654
|
}
|
|
725
655
|
break;
|
|
726
656
|
}
|
|
@@ -752,7 +682,6 @@ async function tryFallbackDownload(url, maxRetries = 3) {
|
|
|
752
682
|
}
|
|
753
683
|
|
|
754
684
|
default: {
|
|
755
|
-
// Generic fallback for unknown platforms
|
|
756
685
|
try {
|
|
757
686
|
const data = await igdl(url);
|
|
758
687
|
if (data && Array.isArray(data) && data[0] && data[0].url) {
|
|
@@ -761,15 +690,12 @@ async function tryFallbackDownload(url, maxRetries = 3) {
|
|
|
761
690
|
throw new Error("No valid URL in generic igdl response");
|
|
762
691
|
}
|
|
763
692
|
} catch (methodError) {
|
|
764
|
-
// Continue to next attempt
|
|
765
693
|
}
|
|
766
694
|
break;
|
|
767
695
|
}
|
|
768
696
|
}
|
|
769
697
|
|
|
770
|
-
// If we got a video URL, validate it
|
|
771
698
|
if (videoUrl && videoUrl.includes("http")) {
|
|
772
|
-
// Validate the URL is accessible
|
|
773
699
|
try {
|
|
774
700
|
const response = await axios.head(videoUrl, {
|
|
775
701
|
timeout: 10000,
|
|
@@ -829,58 +755,80 @@ const MediaDownloader = async (url, options = {}) => {
|
|
|
829
755
|
throw new Error(`URL not supported: ${blacklisted.reason}`);
|
|
830
756
|
}
|
|
831
757
|
|
|
832
|
-
|
|
833
|
-
if (!isVideoLink(url)) {
|
|
834
|
-
throw new Error("This URL is not from a supported platform. Supported platforms: Instagram, X(Twitter), TikTok, Facebook");
|
|
835
|
-
}
|
|
758
|
+
await deleteTempVideos(); // Clean up previous temp files
|
|
836
759
|
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
760
|
+
if (url.includes('youtube') || url.includes('youtu.be')) {
|
|
761
|
+
let cookies = options.YTBcookies || (options.YTBcookie ? [options.YTBcookie] : []);
|
|
762
|
+
|
|
763
|
+
// Automatically load external cookies if none provided
|
|
764
|
+
if (cookies.length === 0) {
|
|
765
|
+
cookies = loadExternalCookies();
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
if (cookies.length === 0) {
|
|
769
|
+
throw new Error("YouTube download requires a cookie. Please Provide a valid cookie in 'cookies.json' or options.");
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// Determine if audio-only is requested
|
|
773
|
+
const isAudioOnly = options.outputFormat === 'mp3' || options.downloadAudio === true || config.downloadAudio === true;
|
|
774
|
+
|
|
775
|
+
const videofile = await downloadYoutubeVideo(url, config, cookies, config.YTBmaxduration, isAudioOnly);
|
|
776
|
+
if (videofile) {
|
|
777
|
+
const result = await uploadToGoFileIfNeeded(videofile);
|
|
778
|
+
return result;
|
|
779
|
+
} else {
|
|
780
|
+
throw new Error("URL not supported. Please provide a video URL from a valid platform.");
|
|
781
|
+
}
|
|
841
782
|
}
|
|
842
783
|
|
|
843
|
-
|
|
784
|
+
else if (url.includes("http")) {
|
|
785
|
+
const blacklisted = blacklistLink(url);
|
|
786
|
+
if (blacklisted) {
|
|
787
|
+
throw new Error(`URL not supported: ${blacklisted.reason}`);
|
|
788
|
+
}
|
|
844
789
|
|
|
845
|
-
|
|
790
|
+
// Verificar se o link está na lista de plataformas suportadas
|
|
791
|
+
if (!isVideoLink(url)) {
|
|
792
|
+
throw new Error("This URL is not from a supported platform. Supported platforms: Instagram, X(Twitter), TikTok, Facebook");
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
let downloadedFilePath = null;
|
|
846
796
|
|
|
847
|
-
try {
|
|
848
|
-
// Try primary download method
|
|
849
797
|
try {
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
798
|
+
// Try primary download method
|
|
799
|
+
try {
|
|
800
|
+
downloadedFilePath = await downloadSmartVideo(url, config);
|
|
801
|
+
} catch (error) {
|
|
802
|
+
const platform = getPlatformType(url);
|
|
853
803
|
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
throw new Error(`YouTube download failed: ${error.message}`);
|
|
857
|
-
}
|
|
804
|
+
try {
|
|
805
|
+
const fallbackUrl = await tryFallbackDownload(url);
|
|
858
806
|
|
|
859
|
-
|
|
860
|
-
|
|
807
|
+
// Validate fallback URL
|
|
808
|
+
const isValid = await validateVideoUrl(fallbackUrl, platform);
|
|
809
|
+
if (!isValid) {
|
|
810
|
+
throw new Error("Fallback URL validation failed");
|
|
811
|
+
}
|
|
861
812
|
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
throw new Error("Fallback URL validation failed");
|
|
813
|
+
downloadedFilePath = await downloadDirectVideo(fallbackUrl, config);
|
|
814
|
+
} catch (fallbackError) {
|
|
815
|
+
throw new Error(`Failed to download video with both methods: ${fallbackError.message}`);
|
|
866
816
|
}
|
|
867
|
-
|
|
868
|
-
downloadedFilePath = await downloadDirectVideo(fallbackUrl, config);
|
|
869
|
-
} catch (fallbackError) {
|
|
870
|
-
throw new Error(`Failed to download video with both methods: ${fallbackError.message}`);
|
|
871
817
|
}
|
|
872
|
-
}
|
|
873
818
|
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
819
|
+
if (downloadedFilePath) {
|
|
820
|
+
const result = await uploadToGoFileIfNeeded(downloadedFilePath);
|
|
821
|
+
return result;
|
|
822
|
+
} else {
|
|
823
|
+
throw new Error("Failed to obtain downloaded file path.");
|
|
824
|
+
}
|
|
825
|
+
} catch (error) {
|
|
826
|
+
// Clean up any remaining temp files on error
|
|
827
|
+
await cleanupTempFiles();
|
|
828
|
+
throw new Error(`Error in downloader videos: ${error.message}`);
|
|
879
829
|
}
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
await cleanupTempFiles();
|
|
883
|
-
throw new Error(`Error in downloader videos: ${error.message}`);
|
|
830
|
+
} else {
|
|
831
|
+
throw new Error("Please specify a video URL from Instagram, facebook or TikTok...");
|
|
884
832
|
}
|
|
885
833
|
};
|
|
886
834
|
|
|
@@ -943,7 +891,6 @@ async function downloadSmartVideo(url, config) {
|
|
|
943
891
|
}
|
|
944
892
|
videoUrl = data.video[0];
|
|
945
893
|
} catch (error) {
|
|
946
|
-
// Fallback: @tobyg74/tiktok-api-dl
|
|
947
894
|
try {
|
|
948
895
|
const result = await Tiktok.Downloader(url, { version: "v1" });
|
|
949
896
|
if (result.status === "success" && result.result) {
|
|
@@ -1018,10 +965,7 @@ async function downloadSmartVideo(url, config) {
|
|
|
1018
965
|
}
|
|
1019
966
|
case "pinterest": {
|
|
1020
967
|
try {
|
|
1021
|
-
// Supports both pin links and search queries
|
|
1022
968
|
const data = await pinterest(url);
|
|
1023
|
-
// data may be array or object depending on query or pin
|
|
1024
|
-
// Try to find best url (image or video)
|
|
1025
969
|
let directUrl = null;
|
|
1026
970
|
const pickFrom = (obj) => {
|
|
1027
971
|
if (!obj) return null;
|
|
@@ -1082,7 +1026,6 @@ async function downloadSmartVideo(url, config) {
|
|
|
1082
1026
|
if (highQuality) {
|
|
1083
1027
|
videoUrl = highQuality.url;
|
|
1084
1028
|
} else {
|
|
1085
|
-
// Fallback: pega o primeiro que tiver url
|
|
1086
1029
|
const anyVideo = data.url.find(v => v.url);
|
|
1087
1030
|
videoUrl = anyVideo ? anyVideo.url : data.url[0];
|
|
1088
1031
|
}
|
|
@@ -1093,7 +1036,6 @@ async function downloadSmartVideo(url, config) {
|
|
|
1093
1036
|
}
|
|
1094
1037
|
|
|
1095
1038
|
if (!videoUrl) {
|
|
1096
|
-
// Fallback para estrutura antiga ou diferente
|
|
1097
1039
|
if (Array.isArray(data) && data[0] && data[0].url) {
|
|
1098
1040
|
videoUrl = data[0].url;
|
|
1099
1041
|
} else if (data && data.HD) {
|
|
@@ -1102,7 +1044,7 @@ async function downloadSmartVideo(url, config) {
|
|
|
1102
1044
|
}
|
|
1103
1045
|
|
|
1104
1046
|
if (!videoUrl) {
|
|
1105
|
-
throw new Error("No video URL found in
|
|
1047
|
+
throw new Error("No video URL found in downloader twitter response");
|
|
1106
1048
|
}
|
|
1107
1049
|
} catch (error) {
|
|
1108
1050
|
throw new Error(`Twitter download failed: ${error.message}`);
|
|
@@ -1255,9 +1197,187 @@ async function rotateVideo(fileName, rotation) {
|
|
|
1255
1197
|
|
|
1256
1198
|
// Function to extract URL from string
|
|
1257
1199
|
function extractUrlFromString(text) {
|
|
1258
|
-
const urlRegex = /(https?:\/\/[^\s]+)
|
|
1200
|
+
const urlRegex = /(https?:\/\/[^\s]+)/g;
|
|
1259
1201
|
const match = text.match(urlRegex);
|
|
1260
|
-
return match ? match[0] :
|
|
1202
|
+
return match ? match[0] : text;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
async function downloadYoutubeVideo(url, config, cookies, YTBmaxduration, isAudioOnly = false) {
|
|
1206
|
+
let lastError = null;
|
|
1207
|
+
|
|
1208
|
+
for (let i = 0; i < Math.min(cookies.length, 6); i++) {
|
|
1209
|
+
const currentCookie = cookies[i];
|
|
1210
|
+
try {
|
|
1211
|
+
const parsedCookies = parseCookies(currentCookie);
|
|
1212
|
+
|
|
1213
|
+
if (parsedCookies.length === 0) {
|
|
1214
|
+
continue;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
const agent = ytdl.createAgent(parsedCookies);
|
|
1218
|
+
const info = await ytdl.getInfo(url, { agent });
|
|
1219
|
+
|
|
1220
|
+
const durationSeconds = parseInt(info.videoDetails.lengthSeconds, 10);
|
|
1221
|
+
if (durationSeconds > YTBmaxduration) {
|
|
1222
|
+
throw new Error(`❌ The video is longer than ${YTBmaxduration} seconds. Aborting.`);
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
let bestFormat;
|
|
1226
|
+
if (isAudioOnly) {
|
|
1227
|
+
// Filter for audio-only formats
|
|
1228
|
+
const audioFormats = info.formats.filter(f => !f.hasVideo && f.hasAudio);
|
|
1229
|
+
bestFormat = audioFormats.sort((a, b) => (b.audioBitrate || 0) - (a.audioBitrate || 0))[0];
|
|
1230
|
+
|
|
1231
|
+
if (!bestFormat) {
|
|
1232
|
+
// Fallback: any format with audio
|
|
1233
|
+
bestFormat = info.formats.filter(f => f.hasAudio).sort((a, b) => (b.audioBitrate || 0) - (a.audioBitrate || 0))[0];
|
|
1234
|
+
}
|
|
1235
|
+
if (!bestFormat) {
|
|
1236
|
+
throw new Error('❌ No suitable audio format found.');
|
|
1237
|
+
}
|
|
1238
|
+
} else {
|
|
1239
|
+
let formats = info.formats.filter(format => {
|
|
1240
|
+
return format.contentLength && parseInt(format.contentLength) <= 10 * 1024 * 1024 && // ≤ 10 MB
|
|
1241
|
+
format.hasAudio && format.hasVideo;
|
|
1242
|
+
});
|
|
1243
|
+
|
|
1244
|
+
if (formats.length === 0) {
|
|
1245
|
+
formats = info.formats.filter(format => {
|
|
1246
|
+
return format.hasAudio && format.hasVideo;
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
if (formats.length === 0) {
|
|
1251
|
+
throw new Error('❌ No suitable video format found.');
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
bestFormat = formats.sort((a, b) => b.height - a.height)[0];
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
const ext = isAudioOnly ? 'mp3' : 'mp4';
|
|
1258
|
+
// Create minimal unique file name in output dir
|
|
1259
|
+
let fileName = path.join(OUTPUT_DIR, `v_${uuidv4().slice(0, 4)}.${ext}`);
|
|
1260
|
+
|
|
1261
|
+
const videoStream = ytdl(url, {
|
|
1262
|
+
format: bestFormat,
|
|
1263
|
+
agent
|
|
1264
|
+
});
|
|
1265
|
+
|
|
1266
|
+
const videoWriter = fs.createWriteStream(fileName);
|
|
1267
|
+
videoStream.pipe(videoWriter);
|
|
1268
|
+
|
|
1269
|
+
return await new Promise((resolve, reject) => {
|
|
1270
|
+
videoWriter.on('finish', async () => {
|
|
1271
|
+
try {
|
|
1272
|
+
let finalFilePath = fileName;
|
|
1273
|
+
if (!isAudioOnly) {
|
|
1274
|
+
finalFilePath = await processDownloadedFile(fileName, config, "youtube");
|
|
1275
|
+
}
|
|
1276
|
+
resolve(finalFilePath);
|
|
1277
|
+
} catch (error) {
|
|
1278
|
+
reject(error);
|
|
1279
|
+
}
|
|
1280
|
+
});
|
|
1281
|
+
videoWriter.on('error', (error) => reject(error));
|
|
1282
|
+
});
|
|
1283
|
+
|
|
1284
|
+
} catch (error) {
|
|
1285
|
+
console.error(`Attempt with cookie ${i + 1} failed: ${error.message}`);
|
|
1286
|
+
lastError = error;
|
|
1287
|
+
// If the error is about duration, don't retry with other cookies
|
|
1288
|
+
if (error.message.includes('longer than')) {
|
|
1289
|
+
throw error;
|
|
1290
|
+
}
|
|
1291
|
+
continue; // Try next cookie
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
throw new Error(`An error occurred while downloading the YouTube video (all ${Math.min(cookies.length, 10)} cookies failed): ${lastError?.message}`);
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
/**
|
|
1299
|
+
* Automatically loads cookies from cookies.json if present
|
|
1300
|
+
* @returns {Array} Array of cookie sets (one or more accounts)
|
|
1301
|
+
*/
|
|
1302
|
+
function loadExternalCookies() {
|
|
1303
|
+
const cookiesPath = path.join(process.cwd(), 'cookies.json');
|
|
1304
|
+
if (!fs.existsSync(cookiesPath)) return [];
|
|
1305
|
+
|
|
1306
|
+
try {
|
|
1307
|
+
const fileContent = fs.readFileSync(cookiesPath, 'utf8');
|
|
1308
|
+
const parsed = JSON.parse(fileContent);
|
|
1309
|
+
if (!Array.isArray(parsed)) return [];
|
|
1310
|
+
|
|
1311
|
+
// If it's an array of objects, it's ONE account.
|
|
1312
|
+
// If it's an array of arrays, it's MULTIPLE accounts.
|
|
1313
|
+
if (parsed.length > 0) {
|
|
1314
|
+
if (Array.isArray(parsed[0])) {
|
|
1315
|
+
// Multiple accounts: return them as strings for the rotation logic
|
|
1316
|
+
return parsed.map(acc => JSON.stringify(acc));
|
|
1317
|
+
} else {
|
|
1318
|
+
// Single account: return the whole JSON as a string
|
|
1319
|
+
return [fileContent];
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
} catch (e) {
|
|
1323
|
+
console.error("Error loading external cookies:", e.message);
|
|
1324
|
+
}
|
|
1325
|
+
return [];
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
/**
|
|
1329
|
+
* Parses cookies in various formats (JSON, Netscape, or Header String)
|
|
1330
|
+
* @param {string} cookieInput
|
|
1331
|
+
* @returns {Array} Array of cookie objects
|
|
1332
|
+
*/
|
|
1333
|
+
function parseCookies(cookieInput) {
|
|
1334
|
+
if (!cookieInput) return [];
|
|
1335
|
+
|
|
1336
|
+
const trimmedInput = cookieInput.trim();
|
|
1337
|
+
|
|
1338
|
+
// 1. Try JSON (EditThisCookie export)
|
|
1339
|
+
try {
|
|
1340
|
+
const parsed = JSON.parse(trimmedInput);
|
|
1341
|
+
if (Array.isArray(parsed)) return parsed;
|
|
1342
|
+
if (typeof parsed === 'object') return [parsed];
|
|
1343
|
+
} catch (e) { }
|
|
1344
|
+
|
|
1345
|
+
// 2. Try Netscape format (tab-separated)
|
|
1346
|
+
if (trimmedInput.includes('\t')) {
|
|
1347
|
+
const lines = trimmedInput.split(/\r?\n/);
|
|
1348
|
+
const cookies = [];
|
|
1349
|
+
for (const line of lines) {
|
|
1350
|
+
if (!line.trim() || line.startsWith('#')) continue;
|
|
1351
|
+
const parts = line.split('\t');
|
|
1352
|
+
if (parts.length >= 7) {
|
|
1353
|
+
cookies.push({
|
|
1354
|
+
domain: parts[0],
|
|
1355
|
+
path: parts[2],
|
|
1356
|
+
secure: parts[3].toUpperCase() === 'TRUE',
|
|
1357
|
+
expirationDate: parseInt(parts[4]),
|
|
1358
|
+
name: parts[5],
|
|
1359
|
+
value: parts[6]
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
if (cookies.length > 0) return cookies;
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
// 3. Fallback: Cookie header string (name=value; name2=value2)
|
|
1367
|
+
return trimmedInput.split(';').map(v => v.split('=')).reduce((acc, v) => {
|
|
1368
|
+
if (v.length >= 2) {
|
|
1369
|
+
const name = v[0].trim();
|
|
1370
|
+
const value = v.slice(1).join('=').trim();
|
|
1371
|
+
if (name && value) {
|
|
1372
|
+
acc.push({
|
|
1373
|
+
name: name,
|
|
1374
|
+
value: value,
|
|
1375
|
+
domain: '.youtube.com'
|
|
1376
|
+
});
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
return acc;
|
|
1380
|
+
}, []);
|
|
1261
1381
|
}
|
|
1262
1382
|
|
|
1263
1383
|
// Function to delete temporary videos
|
|
@@ -1528,7 +1648,7 @@ const AudioDownloader = async (url, options = {}) => {
|
|
|
1528
1648
|
try {
|
|
1529
1649
|
let platform = getPlatformType(url);
|
|
1530
1650
|
if (platform === "y124outube") {
|
|
1531
|
-
|
|
1651
|
+
|
|
1532
1652
|
let fileName = "temp_audio.mp3";
|
|
1533
1653
|
let count = 1;
|
|
1534
1654
|
while (fs.existsSync(fileName)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "frostpv",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.16",
|
|
4
4
|
"description": "downloads",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
"author": "Delta",
|
|
13
13
|
"license": "ISC",
|
|
14
14
|
"dependencies": {
|
|
15
|
+
"@distube/ytdl-core": "^4.16.12",
|
|
16
|
+
"@tobyg74/tiktok-api-dl": "^1.3.7",
|
|
17
|
+
"ab-downloader": "^1.0.1",
|
|
15
18
|
"axios": "1.12.0",
|
|
16
19
|
"btch-downloader": "6.0.25",
|
|
17
20
|
"btch-downloader-old": "npm:btch-downloader@4.0.15",
|
|
@@ -21,8 +24,6 @@
|
|
|
21
24
|
"fs": "^0.0.1-security",
|
|
22
25
|
"path": "^0.12.7",
|
|
23
26
|
"twitter-downloader": "^1.1.8",
|
|
24
|
-
"uuid": "^11.1.0"
|
|
25
|
-
"@tobyg74/tiktok-api-dl": "^1.3.7",
|
|
26
|
-
"ytdlp-nodejs": "2.3.4"
|
|
27
|
+
"uuid": "^11.1.0"
|
|
27
28
|
}
|
|
28
|
-
}
|
|
29
|
+
}
|