frostpv 1.0.1

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 ADDED
@@ -0,0 +1,1490 @@
1
+ const axios = require("axios");
2
+ const fs = require("fs");
3
+ const path = require("path");
4
+ const os = require("os");
5
+ const FormData = require("form-data");
6
+ const crypto = require("crypto");
7
+ const { igdl, ttdl, fbdown, mediafire, capcut, gdrive, pinterest } = require("btch-downloader");
8
+ const { TwitterDL } = require("twitter-downloader");
9
+ const btch = require("btch-downloader");
10
+ const btchOld = require("btch-downloader-old");
11
+
12
+ // Fallback libs
13
+ const Tiktok = require("@tobyg74/tiktok-api-dl");
14
+ const { YtDlp } = require('ytdlp-nodejs');
15
+ const ffmpegPath = require('ffmpeg-ffprobe-static').ffmpegPath;
16
+ const { v4: uuidv4 } = require('uuid');
17
+
18
+ const pathToFfmpeg = require("ffmpeg-ffprobe-static");
19
+ const ffmpeg = require("fluent-ffmpeg");
20
+ ffmpeg.setFfmpegPath(pathToFfmpeg.ffmpegPath);
21
+
22
+ // Output directory is the caller's working directory (keeps files next to the running app)
23
+ const OUTPUT_DIR = process.cwd();
24
+ try { fs.mkdirSync(OUTPUT_DIR, { recursive: true }); } catch (_) {}
25
+ // Keep an OS temp directory for any future transient needs
26
+ const TEMP_DIR = path.join(os.tmpdir(), "downloader-dl-bot");
27
+ try { fs.mkdirSync(TEMP_DIR, { recursive: true }); } catch (_) {}
28
+ // TTL (minutes) for files in OUTPUT_DIR (audio/video) before they are pruned
29
+ const OUTPUT_RETENTION_MIN = Number(process.env.OUTPUT_RETENTION_MIN || 5);
30
+
31
+ const GOFILE_API = "Vs4e2PL8n65ExPz6wgDlqSY8kcEBRrzN";
32
+ const GOFILE_UPLOAD_URL = "https://upload.gofile.io/uploadfile";
33
+ const SIZE_LIMIT_MB = 10;
34
+ const SIZE_LIMIT_BYTES = SIZE_LIMIT_MB * 1024 * 1024;
35
+
36
+ const videoPlatforms = [
37
+ "https://www.instagram.com",
38
+ "https://instagram.com",
39
+ "https://www.tiktok.com",
40
+ "https://tiktok.com",
41
+ "https://www.facebook.com",
42
+ "https://facebook.com",
43
+ "https://www.facebook.com/watch/",
44
+ "https://www.facebook.com/watch?v=",
45
+ "https://www.facebook.com/watch?",
46
+ "https://fb.watch",
47
+ "https://www.fb.watch",
48
+ "https://fb.com",
49
+ "https://www.fb.com",
50
+ "https://www.mediafire.com",
51
+ "https://mediafire.com",
52
+ "https://www.capcut.com",
53
+ "https://capcut.com",
54
+ "https://drive.google.com",
55
+ "https://www.google.com/drive",
56
+ "https://x.com",
57
+ "https://www.x.com",
58
+ "https://twitter.com",
59
+ "https://www.twitter.com",
60
+ "https://vm.tiktok.com/",
61
+ "https://vt.tiktok.com/"
62
+ ];
63
+
64
+ // Blacklist links
65
+ const linkCant = [{
66
+ link: "https://v12331m.tiktok.com",
67
+ reason: "Use real link like 'https://www.tiktok.com'. (just click on your link and copy the link in the browser)"
68
+ }];
69
+
70
+ // Function to check if a link corresponds to a video platform
71
+ const isVideoLink = (link) => {
72
+ return videoPlatforms.some(platform => link.startsWith(platform));
73
+ };
74
+
75
+ // Function to check if a link is blacklisted
76
+ const blacklistLink = (link) => {
77
+ return linkCant.find(item => link.includes(item.link));
78
+ };
79
+
80
+ const defaultConfig = {
81
+ autocrop: false,
82
+ limitSizeMB: null, // For compression, not for GoFile limit
83
+ rotation: null,
84
+ outputFormat: null,
85
+ };
86
+
87
+
88
+ const AUTH_STATE = { tokenHash: null };
89
+ const AUTH_SALT = (process.env.DOWNLOADER_SALT || "");
90
+ const EXPECTED_HASH = (process.env.DOWNLOADER_EXPECTED_HASH || "").trim() || null;
91
+
92
+ function setAuthKey(token) {
93
+ try {
94
+ const material = String(token) + String(AUTH_SALT || "");
95
+ const hash = crypto.createHash("sha256").update(material).digest("hex");
96
+ AUTH_STATE.tokenHash = hash;
97
+ } catch (_) { AUTH_STATE.tokenHash = null; }
98
+ }
99
+
100
+ function assertAuthorized() {
101
+ try {
102
+ if (EXPECTED_HASH && AUTH_STATE.tokenHash && AUTH_STATE.tokenHash === EXPECTED_HASH) return;
103
+
104
+ throw new Error("Unauthorized");
105
+ } catch (e) {
106
+ throw new Error("Unauthorized");
107
+ }
108
+ }
109
+
110
+
111
+ function safeUnlink(filePath) {
112
+ try {
113
+ const resolvedPath = path.resolve(filePath);
114
+ if (fs.existsSync(resolvedPath)) {
115
+ fs.rmSync(resolvedPath, { force: true });
116
+ }
117
+ } catch (error) {
118
+ console.error(`Error in safeUnlink for ${filePath}:`, error.message);
119
+ // Don't throw the error, just log it
120
+ // This prevents the ENOENT error from crashing the application
121
+ }
122
+ }
123
+
124
+ // Function to check if file is in use (Windows specific)
125
+ function isFileInUse(filePath) {
126
+ try {
127
+ // Try to open the file in exclusive mode
128
+ const fd = fs.openSync(filePath, 'r+');
129
+ fs.closeSync(fd);
130
+ return false; // File is not in use
131
+ } catch (error) {
132
+ if (error.code === 'EBUSY' || error.code === 'EACCES') {
133
+ return true; // File is in use
134
+ }
135
+ return false; // Other error, assume not in use
136
+ }
137
+ }
138
+
139
+ // Enhanced safe unlink with retry mechanism
140
+ async function safeUnlinkWithRetry(filePath, maxRetries = 3) {
141
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
142
+ try {
143
+ const resolvedPath = path.resolve(filePath);
144
+
145
+ if (fs.existsSync(resolvedPath)) {
146
+ // Check if file is in use (Windows)
147
+ if (process.platform === 'win32' && isFileInUse(resolvedPath)) {
148
+ await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
149
+ continue;
150
+ }
151
+
152
+ fs.rmSync(resolvedPath, { force: true });
153
+ return true;
154
+ } else {
155
+ return true;
156
+ }
157
+ } catch (error) {
158
+ if (attempt === maxRetries) {
159
+ return false;
160
+ }
161
+ // Wait before retry
162
+ await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
163
+ }
164
+ }
165
+ return false;
166
+ }
167
+
168
+ // Function to upload to GoFile if needed
169
+ async function uploadToGoFileIfNeeded(filePath) {
170
+ try {
171
+ const stats = fs.statSync(filePath);
172
+ const fileSizeInBytes = stats.size;
173
+
174
+ if (fileSizeInBytes <= SIZE_LIMIT_BYTES) {
175
+ return { type: "local", value: filePath };
176
+ }
177
+
178
+ const form = new FormData();
179
+ form.append("file", fs.createReadStream(filePath), path.basename(filePath));
180
+
181
+ const response = await axios.post(GOFILE_UPLOAD_URL, form, {
182
+ headers: {
183
+ ...form.getHeaders(),
184
+ "Authorization": `Bearer ${GOFILE_API}`,
185
+ },
186
+ maxContentLength: Infinity,
187
+ maxBodyLength: Infinity
188
+ });
189
+
190
+ if (response.data && response.data.status === "ok") {
191
+ const downloadLink = response.data.data?.downloadPage;
192
+ if (downloadLink) {
193
+ return { type: "gofile", value: downloadLink };
194
+ } else {
195
+ throw new Error("GoFile API response OK, but download link not found.");
196
+ }
197
+ } else {
198
+ throw new Error(`GoFile API Error: ${response.data?.status || 'Unknown error'}`);
199
+ }
200
+ } catch (error) {
201
+ console.error(`GoFile Upload Error for ${filePath}:`, error.message);
202
+ return { type: "local", value: filePath, error: `GoFile upload failed: ${error.message}` };
203
+ }
204
+ }
205
+
206
+ // Function to validate URL accessibility
207
+ async function validateVideoUrl(url, platform) {
208
+ try {
209
+ const response = await axios.head(url, {
210
+ timeout: 10000,
211
+ headers: {
212
+ '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',
213
+ 'Accept': 'video/webm,video/ogg,video/*;q=0.9,application/ogg;q=0.7,audio/*;q=0.6,*/*;q=0.5',
214
+ 'Accept-Language': 'en-US,en;q=0.5',
215
+ 'Accept-Encoding': 'gzip, deflate',
216
+ 'Connection': 'keep-alive',
217
+ 'Upgrade-Insecure-Requests': '1'
218
+ },
219
+ validateStatus: function (status) {
220
+ return status >= 200 && status < 400; // Accept redirects
221
+ }
222
+ });
223
+
224
+ return true;
225
+ } catch (error) {
226
+ return false;
227
+ }
228
+ }
229
+
230
+ // Function to validate and clean Twitter/X URLs
231
+ function validateTwitterUrl(url) {
232
+ try {
233
+ // Remove query parameters that might cause issues
234
+ const cleanUrl = url.split('?')[0];
235
+
236
+ // Ensure it's a valid Twitter/X URL format
237
+ if (!cleanUrl.includes('x.com') && !cleanUrl.includes('twitter.com')) {
238
+ throw new Error("Not a valid Twitter/X URL");
239
+ }
240
+
241
+ // Check if it has the required structure
242
+ const urlParts = cleanUrl.split('/');
243
+ if (urlParts.length < 5) {
244
+ throw new Error("Invalid Twitter/X URL structure");
245
+ }
246
+
247
+ return cleanUrl;
248
+ } catch (error) {
249
+ throw new Error(`Twitter URL validation failed: ${error.message}`);
250
+ }
251
+ }
252
+
253
+ // Função utilitária para obter cookies
254
+ function getYoutubeCookiesPath() {
255
+ const cookiesPath = path.resolve(process.cwd(), 'cookies.txt');
256
+ if (fs.existsSync(cookiesPath)) {
257
+ return cookiesPath;
258
+ }
259
+ return null;
260
+ }
261
+
262
+ // Função utilitária para obter duração do vídeo/áudio em segundos
263
+ async function getYoutubeDurationSeconds(url) {
264
+ try {
265
+ const ytdlp = new YtDlp({ ffmpegPath });
266
+ const info = await ytdlp.getInfoAsync(url);
267
+ if (info && info.duration) return info.duration;
268
+ return null;
269
+ } catch (e) {
270
+ return null;
271
+ }
272
+ }
273
+
274
+ // Função para baixar áudio do YouTube
275
+ async function downloadYoutubeAudio(url, outputPath) {
276
+ // Verificar duração máxima de 15 minutos
277
+ const duration = await getYoutubeDurationSeconds(url);
278
+ if (duration && duration > 900) {
279
+ throw new Error('The audio is longer than 15 minutes. Test limit: 15 minutes. This feature is still in beta.');
280
+ }
281
+ return new Promise(async (resolve, reject) => {
282
+ try {
283
+ const ytdlp = new YtDlp({ ffmpegPath });
284
+ const cookiesPath = getYoutubeCookiesPath();
285
+
286
+ // Garantir que o nome do arquivo tenha a extensão correta
287
+ const baseName = outputPath.replace(/\.[^/.]+$/, ""); // Remove extensão se existir
288
+
289
+ // Primeiro, tentar com a API de opções
290
+ try {
291
+ const options = {
292
+ format: {
293
+ filter: 'audioonly',
294
+ type: 'mp3',
295
+ quality: 'highestaudio'
296
+ },
297
+ output: outputPath
298
+ };
299
+
300
+ if (cookiesPath) {
301
+ options.cookies = cookiesPath;
302
+ }
303
+
304
+ // Remover logs detalhados para download de áudio do YouTube
305
+ const result = await ytdlp.downloadAsync(url, options);
306
+ // Verificar se o arquivo foi criado
307
+ const actualFileName = baseName + '.mp3';
308
+ if (fs.existsSync(actualFileName)) {
309
+ resolve(actualFileName);
310
+ return;
311
+ }
312
+
313
+ // Tentar encontrar o arquivo com extensão diferente
314
+ const files = fs.readdirSync('./');
315
+ const downloadedFile = files.find(file => file.startsWith(baseName.split('/').pop()));
316
+ if (downloadedFile) {
317
+ resolve(downloadedFile);
318
+ return;
319
+ }
320
+ } catch (apiError) {
321
+ console.log('API method failed, trying direct args method:', apiError.message);
322
+ }
323
+
324
+ // Fallback: usar argumentos diretos para máxima qualidade de áudio
325
+ const args = [
326
+ url,
327
+ '-f', 'bestaudio[ext=mp3]/bestaudio',
328
+ '-o', outputPath
329
+ ];
330
+
331
+ if (cookiesPath) {
332
+ args.push('--cookies', cookiesPath);
333
+ }
334
+
335
+ // Remover logs detalhados para download de áudio do YouTube
336
+ const result = await ytdlp.execAsync(args);
337
+ // Verificar se o arquivo foi criado
338
+ const actualFileName = baseName + '.mp3';
339
+ if (fs.existsSync(actualFileName)) {
340
+ resolve(actualFileName);
341
+ } else {
342
+ // Tentar encontrar o arquivo com extensão diferente
343
+ const files = fs.readdirSync('./');
344
+ const downloadedFile = files.find(file => file.startsWith(baseName.split('/').pop()));
345
+ if (downloadedFile) {
346
+ resolve(downloadedFile);
347
+ } else {
348
+ reject(new Error('YouTube audio download completed but file not found'));
349
+ }
350
+ }
351
+ } catch (error) {
352
+ console.error('YouTube audio download error:', error);
353
+ reject(new Error('This URL is not from a supported platform. Supported platforms: Instagram, X(Twitter), TikTok and Facebook'));
354
+ }
355
+ });
356
+ }
357
+
358
+ // Enhanced function to get platform type from URL
359
+ function getPlatformType(url) {
360
+ const lowerUrl = url.toLowerCase();
361
+
362
+ if (lowerUrl.includes("instagram.com") || lowerUrl.includes("instagr.am")) return "instagram";
363
+ if (lowerUrl.includes("tiktok.com") || lowerUrl.includes("vm.tiktok.com") || lowerUrl.includes("vt.tiktok.com")) return "tiktok";
364
+ if (lowerUrl.includes("facebook.com") || lowerUrl.includes("fb.watch") || lowerUrl.includes("fb.com")) return "facebook";
365
+ if (lowerUrl.includes("mediafire.com")) return "mediafire";
366
+ if (lowerUrl.includes("x.com") || lowerUrl.includes("twitter.com")) return "twitter";
367
+ if (lowerUrl.includes("youtube.com") || lowerUrl.includes("you112t12u.be") || lowerUrl.includes("m.y11outu314be.com")) return "youtu1354be";
368
+
369
+ return "unknown";
370
+ }
371
+
372
+ // Enhanced fallback download function with platform-specific methods
373
+ async function tryFallbackDownload(url, maxRetries = 3) {
374
+ const platform = getPlatformType(url);
375
+
376
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
377
+ try {
378
+ let videoUrl = null;
379
+
380
+ // Platform-specific fallback methods
381
+ switch (platform) {
382
+ case "instagram": {
383
+ // Try multiple Instagram methods
384
+ const methods = [
385
+ async () => {
386
+ const data = await igdl(url);
387
+ if (data && Array.isArray(data) && data[0] && data[0].url) {
388
+ return data[0].url;
389
+ }
390
+ throw new Error("No valid URL in igdl response");
391
+ },
392
+ async () => {
393
+ // Try with different headers
394
+ const data = await igdl(url, {
395
+ headers: {
396
+ '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',
397
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
398
+ 'Accept-Language': 'en-US,en;q=0.5',
399
+ 'Accept-Encoding': 'gzip, deflate',
400
+ 'Connection': 'keep-alive',
401
+ 'Upgrade-Insecure-Requests': '1'
402
+ }
403
+ });
404
+ if (data && Array.isArray(data) && data[0] && data[0].url) {
405
+ return data[0].url;
406
+ }
407
+ throw new Error("No valid URL in igdl response with custom headers");
408
+ },
409
+ async () => {
410
+ // Fallback: try using the old version of btch-downloader
411
+ try {
412
+ const { igdl: igdlOld } = btchOld;
413
+ if (typeof igdlOld === 'function') {
414
+ const data = await igdlOld(url);
415
+ if (data && Array.isArray(data) && data[0] && data[0].url) {
416
+ return data[0].url;
417
+ }
418
+ }
419
+ throw new Error("Old btch-downloader igdl not available or failed");
420
+ } catch (oldError) {
421
+ throw new Error(`Old btch-downloader fallback failed: ${oldError.message}`);
422
+ }
423
+ }
424
+ ];
425
+
426
+ for (const method of methods) {
427
+ try {
428
+ videoUrl = await method();
429
+ if (videoUrl) break;
430
+ } catch (methodError) {
431
+ continue;
432
+ }
433
+ }
434
+ break;
435
+ }
436
+
437
+ case "tiktok": {
438
+ // Try multiple TikTok methods
439
+ const methods = [
440
+ async () => {
441
+ const data = await ttdl(url);
442
+ if (data && data.video && data.video[0]) {
443
+ return data.video[0];
444
+ }
445
+ throw new Error("No valid video in ttdl response");
446
+ },
447
+ async () => {
448
+ const result = await Tiktok.Downloader(url, { version: "v1" });
449
+ if (result.status === "success" && result.result) {
450
+ if (result.result.video && result.result.video.playAddr && result.result.video.playAddr.length > 0) {
451
+ return result.result.video.playAddr[0];
452
+ } else if (result.result.videoHD) {
453
+ return result.result.videoHD;
454
+ }
455
+ }
456
+ throw new Error("No valid video in Tiktok.Downloader response");
457
+ },
458
+ async () => {
459
+ // Try with different version
460
+ const result = await Tiktok.Downloader(url, { version: "v2" });
461
+ if (result.status === "success" && result.result) {
462
+ if (result.result.video && result.result.video.playAddr && result.result.video.playAddr.length > 0) {
463
+ return result.result.video.playAddr[0];
464
+ } else if (result.result.videoHD) {
465
+ return result.result.videoHD;
466
+ }
467
+ }
468
+ throw new Error("No valid video in Tiktok.Downloader v2 response");
469
+ }
470
+ ];
471
+
472
+ for (const method of methods) {
473
+ try {
474
+ videoUrl = await method();
475
+ if (videoUrl) break;
476
+ } catch (methodError) {
477
+ continue;
478
+ }
479
+ }
480
+ break;
481
+ }
482
+
483
+ case "facebook": {
484
+ // Try multiple Facebook methods
485
+ const methods = [
486
+ async () => {
487
+ const data = await fbdown(url);
488
+ if (data && (data.Normal_video || data.HD)) {
489
+ return data.Normal_video || data.HD;
490
+ }
491
+ throw new Error("No valid video in fbdown response");
492
+ },
493
+ async () => {
494
+ // Try with different approach
495
+ const data = await fbdown(url, {
496
+ headers: {
497
+ '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'
498
+ }
499
+ });
500
+ if (data && (data.Normal_video || data.HD)) {
501
+ return data.Normal_video || data.HD;
502
+ }
503
+ throw new Error("No valid video in fbdown response with custom headers");
504
+ }
505
+ ];
506
+
507
+ for (const method of methods) {
508
+ try {
509
+ videoUrl = await method();
510
+ if (videoUrl) break;
511
+ } catch (methodError) {
512
+ continue;
513
+ }
514
+ }
515
+ break;
516
+ }
517
+
518
+ case "twitter": {
519
+ // Try multiple Twitter methods with better response handling
520
+ const methods = [
521
+ async () => {
522
+ const cleanUrl = validateTwitterUrl(url);
523
+ const data = await TwitterDL(cleanUrl, {});
524
+
525
+ // Check multiple possible response structures
526
+ if (data && data.result) {
527
+ // Structure 1: data.result.media[0].videos[0].url
528
+ if (data.result.media && data.result.media[0] &&
529
+ data.result.media[0].videos && data.result.media[0].videos[0] &&
530
+ data.result.media[0].videos[0].url) {
531
+ return data.result.media[0].videos[0].url;
532
+ }
533
+
534
+ // Structure 2: data.result.video[0].url
535
+ if (data.result.video && data.result.video[0] && data.result.video[0].url) {
536
+ return data.result.video[0].url;
537
+ }
538
+
539
+ // Structure 3: data.result.url (direct URL)
540
+ if (data.result.url) {
541
+ return data.result.url;
542
+ }
543
+
544
+ // Structure 4: data.result.media[0].url
545
+ if (data.result.media && data.result.media[0] && data.result.media[0].url) {
546
+ return data.result.media[0].url;
547
+ }
548
+
549
+ // Structure 5: Check for any video URL in the entire response
550
+ const findVideoUrl = (obj) => {
551
+ if (typeof obj === 'string' && obj.includes('http') &&
552
+ (obj.includes('.mp4') || obj.includes('video') || obj.includes('media'))) {
553
+ return obj;
554
+ }
555
+ if (typeof obj === 'object' && obj !== null) {
556
+ for (const key in obj) {
557
+ const result = findVideoUrl(obj[key]);
558
+ if (result) return result;
559
+ }
560
+ }
561
+ return null;
562
+ };
563
+
564
+ const foundUrl = findVideoUrl(data.result);
565
+ if (foundUrl) {
566
+ return foundUrl;
567
+ }
568
+ }
569
+
570
+ throw new Error("No valid video URL found in TwitterDL response structure");
571
+ },
572
+ async () => {
573
+ // Try with different options and headers
574
+ const cleanUrl = validateTwitterUrl(url);
575
+ const data = await TwitterDL(cleanUrl, {
576
+ headers: {
577
+ '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',
578
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
579
+ 'Accept-Language': 'en-US,en;q=0.5',
580
+ 'Accept-Encoding': 'gzip, deflate',
581
+ 'Connection': 'keep-alive',
582
+ 'Upgrade-Insecure-Requests': '1'
583
+ }
584
+ });
585
+
586
+ // Same structure checking as above
587
+ if (data && data.result) {
588
+ if (data.result.media && data.result.media[0] &&
589
+ data.result.media[0].videos && data.result.media[0].videos[0] &&
590
+ data.result.media[0].videos[0].url) {
591
+ return data.result.media[0].videos[0].url;
592
+ }
593
+
594
+ if (data.result.video && data.result.video[0] && data.result.video[0].url) {
595
+ return data.result.video[0].url;
596
+ }
597
+
598
+ if (data.result.url) {
599
+ return data.result.url;
600
+ }
601
+
602
+ if (data.result.media && data.result.media[0] && data.result.media[0].url) {
603
+ return data.result.media[0].url;
604
+ }
605
+
606
+ const findVideoUrl = (obj) => {
607
+ if (typeof obj === 'string' && obj.includes('http') &&
608
+ (obj.includes('.mp4') || obj.includes('video') || obj.includes('media'))) {
609
+ return obj;
610
+ }
611
+ if (typeof obj === 'object' && obj !== null) {
612
+ for (const key in obj) {
613
+ const result = findVideoUrl(obj[key]);
614
+ if (result) return result;
615
+ }
616
+ }
617
+ return null;
618
+ };
619
+
620
+ const foundUrl = findVideoUrl(data.result);
621
+ if (foundUrl) {
622
+ return foundUrl;
623
+ }
624
+ }
625
+
626
+ throw new Error("No valid video URL found in TwitterDL response with custom headers");
627
+ },
628
+ async () => {
629
+ // Try with a different approach - use the same method as primary download
630
+ try {
631
+ const cleanUrl = validateTwitterUrl(url);
632
+ const data = await TwitterDL(cleanUrl, {});
633
+ if (
634
+ !data ||
635
+ !data.result ||
636
+ !data.result.media ||
637
+ !data.result.media[0] ||
638
+ !data.result.media[0].videos ||
639
+ !data.result.media[0].videos[0] ||
640
+ !data.result.media[0].videos[0].url
641
+ ) {
642
+ throw new Error("No video URL found in Twitter response");
643
+ }
644
+ return data.result.media[0].videos[0].url;
645
+ } catch (error) {
646
+ throw new Error(`Twitter fallback method 3 failed: ${error.message}`);
647
+ }
648
+ },
649
+ async () => {
650
+ // Fallback: try using the old version of btch-downloader for Twitter/X
651
+ try {
652
+ const cleanUrl = validateTwitterUrl(url);
653
+ // Try to find any Twitter-related function in the old version
654
+ const possibleFunctions = ['twitter', 'twitterdl', 'tw', 'x', 'xdown', 'xdl'];
655
+ for (const funcName of possibleFunctions) {
656
+ if (typeof btchOld[funcName] === 'function') {
657
+ try {
658
+ const data = await btchOld[funcName](cleanUrl);
659
+ // Check multiple possible response structures
660
+ if (data && data.result) {
661
+ if (data.result.media && data.result.media[0] &&
662
+ data.result.media[0].videos && data.result.media[0].videos[0] &&
663
+ data.result.media[0].videos[0].url) {
664
+ return data.result.media[0].videos[0].url;
665
+ }
666
+ if (data.result.video && data.result.video[0] && data.result.video[0].url) {
667
+ return data.result.video[0].url;
668
+ }
669
+ if (data.result.url) {
670
+ return data.result.url;
671
+ }
672
+ }
673
+ if (data && data.url) {
674
+ return data.url;
675
+ }
676
+ if (Array.isArray(data) && data[0] && data[0].url) {
677
+ return data[0].url;
678
+ }
679
+ } catch (e) {
680
+ continue;
681
+ }
682
+ }
683
+ }
684
+ throw new Error("No Twitter functions found in old btch-downloader");
685
+ } catch (oldError) {
686
+ throw new Error(`Old btch-downloader Twitter fallback failed: ${oldError.message}`);
687
+ }
688
+ }
689
+ ];
690
+
691
+ for (const method of methods) {
692
+ try {
693
+ videoUrl = await method();
694
+ if (videoUrl) break;
695
+ } catch (methodError) {
696
+ continue;
697
+ }
698
+ }
699
+ break;
700
+ }
701
+
702
+ case "youtube": {
703
+ // YouTube doesn't need fallback - it has its own specific method
704
+ throw new Error("YouTube downloads should use the primary method, not fallback");
705
+ }
706
+
707
+ case "mediafire": {
708
+ // Try MediaFire method
709
+ try {
710
+ const data = await mediafire(url);
711
+ if (data && data.url) {
712
+ videoUrl = data.url;
713
+ } else {
714
+ throw new Error("No valid URL in mediafire response");
715
+ }
716
+ } catch (methodError) {
717
+ // Continue to next attempt
718
+ }
719
+ break;
720
+ }
721
+
722
+ default: {
723
+ // Generic fallback for unknown platforms
724
+ try {
725
+ const data = await igdl(url);
726
+ if (data && Array.isArray(data) && data[0] && data[0].url) {
727
+ videoUrl = data[0].url;
728
+ } else {
729
+ throw new Error("No valid URL in generic igdl response");
730
+ }
731
+ } catch (methodError) {
732
+ // Continue to next attempt
733
+ }
734
+ break;
735
+ }
736
+ }
737
+
738
+ // If we got a video URL, validate it
739
+ if (videoUrl && videoUrl.includes("http")) {
740
+ // Validate the URL is accessible
741
+ try {
742
+ const response = await axios.head(videoUrl, {
743
+ timeout: 10000,
744
+ headers: {
745
+ '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'
746
+ }
747
+ });
748
+
749
+ if (response.status === 200) {
750
+ return videoUrl;
751
+ } else {
752
+ throw new Error(`URL validation failed with status ${response.status}`);
753
+ }
754
+ } catch (validationError) {
755
+ throw new Error(`Video URL is not accessible: ${validationError.message}`);
756
+ }
757
+ } else {
758
+ throw new Error("No valid video URL obtained from any fallback method");
759
+ }
760
+
761
+ } catch (error) {
762
+ if (attempt === maxRetries) {
763
+ throw new Error(`Enhanced fallback method failed after ${maxRetries} attempts for ${platform}: ${error.message}`);
764
+ }
765
+ // Wait before retry (exponential backoff)
766
+ const delay = Math.min(1000 * Math.pow(2, attempt - 1), 5000);
767
+ await new Promise(resolve => setTimeout(resolve, delay));
768
+ }
769
+ }
770
+ }
771
+
772
+ // Main MediaDownloader function
773
+ const MediaDownloader = async (url, options = {}) => {
774
+ assertAuthorized();
775
+ const config = { ...defaultConfig, ...options };
776
+
777
+ if (!url || !url.includes("http")) {
778
+ throw new Error("Please specify a valid video URL.");
779
+ }
780
+
781
+ try {
782
+ url = await unshortenUrl(extractUrlFromString(url));
783
+ } catch (error) {
784
+ // Use original URL if unshortening fails
785
+ }
786
+
787
+ if (url.includes("snapchat.com")) {
788
+ throw new Error("Snapchat links are not supported for download. Download videos from Instagram, X, TikTok, and Facebook");
789
+ }
790
+ if (url.includes("threads.com")) {
791
+ throw new Error("Threads links are not supported for download. Download videos from Instagram, X, TikTok, and Facebook");
792
+ }
793
+ if (url.includes("pinterest.com")) {
794
+ throw new Error("Pinterest links are not supported for download. Download videos from Instagram, X, TikTok, and Facebook");
795
+ }
796
+ if (url.includes("pin.it")) {
797
+ throw new Error("Pinterest links are not supported for download. Download videos from Instagram, X, TikTok, and Facebook");
798
+ }
799
+
800
+ const blacklisted = blacklistLink(url);
801
+ if (blacklisted) {
802
+ throw new Error(`URL not supported: ${blacklisted.reason}`);
803
+ }
804
+
805
+ // Verificar se o link está na lista de plataformas suportadas
806
+ if (!isVideoLink(url)) {
807
+ throw new Error("This URL is not from a supported platform. Supported platforms: Instagram, X(Twitter), TikTok and Facebook");
808
+ }
809
+
810
+ // Verificar se é YouTube e lançar erro customizado
811
+ const platform = getPlatformType(url);
812
+ if (platform === "youtube") {
813
+ throw new Error("This URL is not from a supported platform. Supported platforms: Instagram, X(Twitter), TikTok and Facebook");
814
+ }
815
+
816
+ await cleanupTempFiles(); // Clean up previous temp files
817
+
818
+ let downloadedFilePath = null;
819
+
820
+ try {
821
+ // Try primary download method
822
+ try {
823
+ downloadedFilePath = await downloadSmartVideo(url, config);
824
+ } catch (error) {
825
+ const platform = getPlatformType(url);
826
+
827
+ // YouTube doesn't use fallback method - it has its own specific implementation
828
+ if (platform === "youtube") {
829
+ throw new Error(`YouTube download failed: ${error.message}`);
830
+ }
831
+
832
+ try {
833
+ const fallbackUrl = await tryFallbackDownload(url);
834
+
835
+ // Validate fallback URL
836
+ const isValid = await validateVideoUrl(fallbackUrl, platform);
837
+ if (!isValid) {
838
+ throw new Error("Fallback URL validation failed");
839
+ }
840
+
841
+ downloadedFilePath = await downloadDirectVideo(fallbackUrl, config);
842
+ } catch (fallbackError) {
843
+ throw new Error(`Failed to download video with both methods: ${fallbackError.message}`);
844
+ }
845
+ }
846
+
847
+ if (downloadedFilePath) {
848
+ const result = await uploadToGoFileIfNeeded(downloadedFilePath);
849
+ return result;
850
+ } else {
851
+ throw new Error("Failed to obtain downloaded file path.");
852
+ }
853
+ } catch (error) {
854
+ // Clean up any remaining temp files on error
855
+ await cleanupTempFiles();
856
+ throw new Error(`Error in downloader videos: ${error.message}`);
857
+ }
858
+ };
859
+
860
+ // Function to process downloaded file
861
+ async function processDownloadedFile(fileName, config, platform = null) {
862
+ let processedFile = path.resolve(fileName);
863
+ if (config.rotation) {
864
+ processedFile = await rotateVideo(processedFile, config.rotation);
865
+ }
866
+ if (config.autocrop) {
867
+ processedFile = await autoCrop(processedFile);
868
+ }
869
+ processedFile = await checkAndCompressVideo(processedFile, config.limitSizeMB, platform);
870
+ if (config.outputFormat) {
871
+ processedFile = await convertVideoFormat(processedFile, String(config.outputFormat).toLowerCase());
872
+ }
873
+ return processedFile;
874
+ }
875
+
876
+ // Function for platform-specific video download
877
+ async function downloadSmartVideo(url, config) {
878
+ try {
879
+ let videoUrl = null;
880
+ let platform = getPlatformType(url);
881
+
882
+ switch (platform) {
883
+ case "instagram": {
884
+ let data = null;
885
+ let attempts = 0;
886
+ const maxAttempts = 3;
887
+ while (attempts < maxAttempts && !data) {
888
+ attempts++;
889
+ try {
890
+ data = await igdl(url);
891
+ if (data && Array.isArray(data) && data[0] && data[0].url) {
892
+ videoUrl = data[0].url;
893
+ break;
894
+ } else {
895
+ throw new Error("Invalid response structure from igdl");
896
+ }
897
+ } catch (error) {
898
+ if (attempts === maxAttempts) {
899
+ throw new Error(`Instagram download failed after ${maxAttempts} attempts: ${error.message}`);
900
+ }
901
+ await new Promise(resolve => setTimeout(resolve, 1000 * attempts));
902
+ }
903
+ }
904
+ if (!videoUrl) {
905
+ throw new Error("Video unavailable or invalid link for Instagram.");
906
+ }
907
+ break;
908
+ }
909
+ case "tiktok": {
910
+ let data = null;
911
+ let fallbackError = null;
912
+ try {
913
+ data = await ttdl(url);
914
+ if (!data || !data.video || !data.video[0]) {
915
+ throw new Error("Invalid response from ttdl");
916
+ }
917
+ videoUrl = data.video[0];
918
+ } catch (error) {
919
+ // Fallback: @tobyg74/tiktok-api-dl
920
+ try {
921
+ const result = await Tiktok.Downloader(url, { version: "v1" });
922
+ if (result.status === "success" && result.result) {
923
+ if (result.result.video && result.result.video.playAddr && result.result.video.playAddr.length > 0) {
924
+ videoUrl = result.result.video.playAddr[0];
925
+ } else if (result.result.videoHD) {
926
+ videoUrl = result.result.videoHD;
927
+ } else {
928
+ throw new Error("No valid video found in fallback v1");
929
+ }
930
+ } else {
931
+ throw new Error("Fallback v1 returned unsuccessful status");
932
+ }
933
+ } catch (err) {
934
+ // Try v2
935
+ try {
936
+ const result = await Tiktok.Downloader(url, { version: "v2" });
937
+ if (result.status === "success" && result.result) {
938
+ if (result.result.video && result.result.video.playAddr && result.result.video.playAddr.length > 0) {
939
+ videoUrl = result.result.video.playAddr[0];
940
+ } else if (result.result.videoHD) {
941
+ videoUrl = result.result.videoHD;
942
+ } else {
943
+ throw new Error("No valid video found in fallback v2");
944
+ }
945
+ } else {
946
+ throw new Error("Fallback v2 returned unsuccessful status");
947
+ }
948
+ } catch (err2) {
949
+ fallbackError = err2;
950
+ throw new Error(`TikTok download failed: ${error.message} | Fallback v1: ${err.message} | Fallback v2: ${err2.message}`);
951
+ }
952
+ }
953
+ }
954
+ break;
955
+ }
956
+ case "facebook": {
957
+ try {
958
+ const data = await fbdown(url);
959
+ if (!data || (!data.Normal_video && !data.HD)) {
960
+ throw new Error("No video URLs found in Facebook response");
961
+ }
962
+ videoUrl = data.Normal_video || data.HD;
963
+
964
+ // Validate the URL is accessible
965
+ const response = await axios.head(videoUrl, {
966
+ timeout: 10000,
967
+ headers: {
968
+ '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'
969
+ }
970
+ }).catch(() => null);
971
+
972
+ if (!response || response.status !== 200) {
973
+ throw new Error("Facebook video URL is not accessible");
974
+ }
975
+ } catch (error) {
976
+ throw new Error(`Facebook download failed: ${error.message}`);
977
+ }
978
+ break;
979
+ }
980
+ case "mediafire": {
981
+ try {
982
+ const data = await mediafire(url);
983
+ if (!data || !data.url) {
984
+ throw new Error("No URL found in MediaFire response");
985
+ }
986
+ videoUrl = data.url;
987
+ } catch (error) {
988
+ throw new Error(`MediaFire download failed: ${error.message}`);
989
+ }
990
+ break;
991
+ }
992
+ case "twitter": {
993
+ try {
994
+ // Validate and clean the Twitter URL
995
+ const cleanUrl = validateTwitterUrl(url);
996
+ const data = await TwitterDL(cleanUrl, {});
997
+
998
+ if (data && data.result) {
999
+ // Try multiple possible response structures
1000
+ if (data.result.media && data.result.media[0] &&
1001
+ data.result.media[0].videos && data.result.media[0].videos[0] &&
1002
+ data.result.media[0].videos[0].url) {
1003
+ videoUrl = data.result.media[0].videos[0].url;
1004
+ } else if (data.result.video && data.result.video[0] && data.result.video[0].url) {
1005
+ videoUrl = data.result.video[0].url;
1006
+ } else if (data.result.url) {
1007
+ videoUrl = data.result.url;
1008
+ } else if (data.result.media && data.result.media[0] && data.result.media[0].url) {
1009
+ videoUrl = data.result.media[0].url;
1010
+ } else {
1011
+ // Search for any video URL in the response
1012
+ const findVideoUrl = (obj) => {
1013
+ if (typeof obj === 'string' && obj.includes('http') &&
1014
+ (obj.includes('.mp4') || obj.includes('video') || obj.includes('media'))) {
1015
+ return obj;
1016
+ }
1017
+ if (typeof obj === 'object' && obj !== null) {
1018
+ for (const key in obj) {
1019
+ const result = findVideoUrl(obj[key]);
1020
+ if (result) return result;
1021
+ }
1022
+ }
1023
+ return null;
1024
+ };
1025
+
1026
+ const foundUrl = findVideoUrl(data.result);
1027
+ if (foundUrl) {
1028
+ videoUrl = foundUrl;
1029
+ } else {
1030
+ throw new Error("No video URL found in Twitter response structure");
1031
+ }
1032
+ }
1033
+ } else {
1034
+ throw new Error("Invalid Twitter response structure");
1035
+ }
1036
+ } catch (error) {
1037
+ throw new Error(`Twitter download failed: ${error.message}`);
1038
+ }
1039
+ break;
1040
+ }
1041
+ default:
1042
+ throw new Error("Platform not supported or invalid link.");
1043
+ }
1044
+
1045
+ if (!videoUrl || !videoUrl.includes("http")) {
1046
+ throw new Error("Returned video URL is invalid or unavailable.");
1047
+ }
1048
+
1049
+ // Download the video with better error handling
1050
+ const response = await axios({
1051
+ url: videoUrl,
1052
+ method: "GET",
1053
+ responseType: "stream",
1054
+ timeout: 30000,
1055
+ headers: {
1056
+ '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'
1057
+ }
1058
+ });
1059
+
1060
+ // Create minimal unique file name in output dir
1061
+ let fileName = path.join(OUTPUT_DIR, `v_${uuidv4().slice(0,4)}.mp4`);
1062
+
1063
+ const videoWriter = fs.createWriteStream(fileName);
1064
+ response.data.pipe(videoWriter);
1065
+
1066
+ return new Promise((resolve, reject) => {
1067
+ videoWriter.on("finish", async () => {
1068
+ try {
1069
+ const finalFilePath = await processDownloadedFile(fileName, config, platform);
1070
+ resolve(finalFilePath);
1071
+ } catch (error) {
1072
+ reject(new Error(`Error processing downloaded video: ${error.message}`));
1073
+ }
1074
+ });
1075
+ videoWriter.on("error", (error) => {
1076
+ reject(new Error(`Error saving video: ${error.message}`));
1077
+ });
1078
+ });
1079
+ } catch (error) {
1080
+ throw new Error(`Failed to download video: ${error.message}`);
1081
+ }
1082
+ }
1083
+
1084
+ // Function for direct video download
1085
+ async function downloadDirectVideo(url, config) {
1086
+ try {
1087
+ const response = await axios({
1088
+ url: url,
1089
+ method: "GET",
1090
+ responseType: "stream",
1091
+ timeout: 30000,
1092
+ headers: {
1093
+ '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'
1094
+ }
1095
+ });
1096
+
1097
+ // Create minimal unique file name in output dir
1098
+ let fileName = path.join(OUTPUT_DIR, `v_${uuidv4().slice(0,4)}.mp4`);
1099
+
1100
+ const videoWriter = fs.createWriteStream(fileName);
1101
+ response.data.pipe(videoWriter);
1102
+
1103
+ return new Promise((resolve, reject) => {
1104
+ videoWriter.on("finish", async () => {
1105
+ try {
1106
+ const finalFilePath = await processDownloadedFile(fileName, config, "unknown");
1107
+ resolve(finalFilePath);
1108
+ } catch (error) {
1109
+ reject(new Error(`Error processing downloaded video: ${error.message}`));
1110
+ }
1111
+ });
1112
+ videoWriter.on("error", (error) => {
1113
+ reject(new Error(`Error saving video: ${error.message}`));
1114
+ });
1115
+ });
1116
+ } catch (error) {
1117
+ throw new Error(`Failed to download video directly: ${error.message}`);
1118
+ }
1119
+ }
1120
+
1121
+ // Function to rotate video
1122
+ async function rotateVideo(fileName, rotation) {
1123
+ const outputPath = path.join(OUTPUT_DIR, `vr_${uuidv4().slice(0,4)}.mp4`);
1124
+ let angle;
1125
+ switch (rotation.toLowerCase()) {
1126
+ case "left": angle = "transpose=2"; break;
1127
+ case "right": angle = "transpose=1"; break;
1128
+ case "180":
1129
+ case "flip": angle = "transpose=2,transpose=2"; break;
1130
+ default: throw new Error("Invalid rotation value. Use 'left', 'right', '180', or 'flip'.");
1131
+ }
1132
+
1133
+ return new Promise((resolve, reject) => {
1134
+ ffmpeg(fileName)
1135
+ .videoFilters(angle)
1136
+ .output(outputPath)
1137
+ .on('end', async () => {
1138
+ await safeUnlinkWithRetry(fileName);
1139
+ resolve(outputPath);
1140
+ })
1141
+ .on('error', (err) => reject(new Error(`Error during video rotation: ${err.message}`)))
1142
+ .run();
1143
+ });
1144
+ }
1145
+
1146
+ // Function to extract URL from string
1147
+ function extractUrlFromString(text) {
1148
+ const urlRegex = /(https?:\/\/[^\s]+)/;
1149
+ const match = text.match(urlRegex);
1150
+ return match ? match[0] : null;
1151
+ }
1152
+
1153
+ // Function to delete temporary videos
1154
+ async function deleteTempVideos() {
1155
+ try {
1156
+ if (!fs.existsSync(TEMP_DIR)) return;
1157
+ const files = fs.readdirSync(TEMP_DIR);
1158
+ const tempVideoFiles = files.filter(file => /^temp_video.*\.mp4$/.test(file));
1159
+ for (const file of tempVideoFiles) {
1160
+ safeUnlink(path.join(TEMP_DIR, file));
1161
+ }
1162
+ } catch (error) {
1163
+ if (error && error.code !== 'ENOENT') {
1164
+ console.error(`Error reading directory for temp file deletion: ${error.message}`);
1165
+ }
1166
+ }
1167
+ }
1168
+
1169
+ // Enhanced cleanup function for all temporary files
1170
+ async function cleanupTempFiles() {
1171
+ try {
1172
+ if (!fs.existsSync(TEMP_DIR)) return;
1173
+ const files = fs.readdirSync(TEMP_DIR);
1174
+ const tempFiles = files.filter(file =>
1175
+ /^temp_video.*\.mp4$/.test(file) ||
1176
+ /_audio\.mp3$/.test(file) ||
1177
+ /_rotated\.mp4$/.test(file) ||
1178
+ /_cropped\.mp4$/.test(file) ||
1179
+ /_compressed\.mp4$/.test(file)
1180
+ );
1181
+ for (const file of tempFiles) {
1182
+ await safeUnlinkWithRetry(path.join(TEMP_DIR, file));
1183
+ }
1184
+ } catch (error) {
1185
+ if (error && error.code !== 'ENOENT') {
1186
+ console.error(`Error during temp file cleanup: ${error.message}`);
1187
+ }
1188
+ }
1189
+ }
1190
+
1191
+ // Cleanup files in output folder older than OUTPUT_RETENTION_MIN
1192
+ async function cleanupOutputFiles() {
1193
+ try {
1194
+ if (!fs.existsSync(OUTPUT_DIR)) return;
1195
+ const files = fs.readdirSync(OUTPUT_DIR);
1196
+ const now = Date.now();
1197
+ const maxAgeMs = OUTPUT_RETENTION_MIN * 60 * 1000;
1198
+ for (const file of files) {
1199
+ // Only consider our generated short names
1200
+ if (!/^(v_|vr_|vc_|vx_|vf_|a_)[0-9a-f]{4}/i.test(file)) continue;
1201
+ const full = path.join(OUTPUT_DIR, file);
1202
+ try {
1203
+ const st = fs.statSync(full);
1204
+ if (now - st.mtimeMs > maxAgeMs) {
1205
+ await safeUnlinkWithRetry(full);
1206
+ }
1207
+ } catch (_) {}
1208
+ }
1209
+ } catch (_) {}
1210
+ }
1211
+
1212
+ // Schedule periodic cleanup (every 10 minutes)
1213
+ setInterval(() => {
1214
+ cleanupTempFiles();
1215
+ cleanupOutputFiles();
1216
+ }, 60 * 1000);
1217
+ // Run once at startup
1218
+ cleanupOutputFiles();
1219
+
1220
+ // Function to auto-crop video
1221
+ async function autoCrop(fileName) {
1222
+ const inputPath = fileName;
1223
+ const outputPath = path.join(OUTPUT_DIR, `vc_${uuidv4().slice(0,4)}.mp4`);
1224
+
1225
+ return new Promise((resolve, reject) => {
1226
+ let cropValues = null;
1227
+ ffmpeg(inputPath)
1228
+ .outputOptions('-vf', 'cropdetect=24:16:0')
1229
+ .outputFormat('null')
1230
+ .output('-')
1231
+ .on('stderr', function(stderrLine) {
1232
+ const cropMatch = stderrLine.match(/crop=([0-9]+):([0-9]+):([0-9]+):([0-9]+)/);
1233
+ if (cropMatch) {
1234
+ cropValues = `crop=${cropMatch[1]}:${cropMatch[2]}:${cropMatch[3]}:${cropMatch[4]}`;
1235
+ }
1236
+ })
1237
+ .on('end', function() {
1238
+ if (!cropValues) {
1239
+ resolve(inputPath);
1240
+ return;
1241
+ }
1242
+ ffmpeg(inputPath)
1243
+ .outputOptions('-vf', cropValues)
1244
+ .output(outputPath)
1245
+ .on('end', async () => {
1246
+ await safeUnlinkWithRetry(inputPath);
1247
+ resolve(outputPath);
1248
+ })
1249
+ .on('error', (err) => reject(new Error(`Error during auto-cropping: ${err.message}`)))
1250
+ .run();
1251
+ });
1252
+ });
1253
+ }
1254
+
1255
+ // Function to check and compress video
1256
+ async function checkAndCompressVideo(filePath, limitSizeMB, platform = null) {
1257
+ // Não comprimir vídeos do YouTube
1258
+ if (platform === "youtube") return filePath;
1259
+
1260
+ if (!limitSizeMB) return filePath;
1261
+
1262
+ const stats = fs.statSync(filePath);
1263
+ const fileSizeInBytes = stats.size;
1264
+ const limitSizeInBytes = limitSizeMB * 1024 * 1024;
1265
+
1266
+ if (fileSizeInBytes <= limitSizeInBytes) {
1267
+ return filePath;
1268
+ }
1269
+
1270
+ const outputPath = path.join(OUTPUT_DIR, `vx_${uuidv4().slice(0,4)}.mp4`);
1271
+
1272
+ return new Promise((resolve, reject) => {
1273
+ ffmpeg(filePath)
1274
+ .outputOptions(
1275
+ '-vf', 'scale=trunc(iw/2)*2:trunc(ih/2)*2',
1276
+ '-crf', '28',
1277
+ '-preset', 'slow'
1278
+ )
1279
+ .output(outputPath)
1280
+ .on('end', async () => {
1281
+ await safeUnlinkWithRetry(filePath);
1282
+ resolve(outputPath);
1283
+ })
1284
+ .on('error', (err) => {
1285
+ resolve(filePath);
1286
+ })
1287
+ .run();
1288
+ });
1289
+ }
1290
+
1291
+ // Video format conversion
1292
+ async function convertVideoFormat(inputPath, targetFormat) {
1293
+ try {
1294
+ const fmt = String(targetFormat).toLowerCase();
1295
+
1296
+ if (fmt === "mp3") {
1297
+ const audioPath = await extractAudioMp3(inputPath);
1298
+ await safeUnlinkWithRetry(inputPath);
1299
+ return audioPath;
1300
+ }
1301
+
1302
+ const supported = ["mp4", "mov", "webm", "mkv"];
1303
+ if (!supported.includes(fmt)) return inputPath;
1304
+
1305
+ const outputPath = path.join(OUTPUT_DIR, `vf_${uuidv4().slice(0,4)}.${fmt}`);
1306
+
1307
+ const ff = ffmpeg(inputPath);
1308
+ switch (fmt) {
1309
+ case "mp4":
1310
+ case "mov":
1311
+ case "mkv":
1312
+ ff.outputOptions("-c:v","libx264","-pix_fmt","yuv420p","-c:a","aac","-b:a","192k");
1313
+ break;
1314
+ case "webm":
1315
+ ff.outputOptions("-c:v","libvpx-vp9","-b:v","0","-crf","30","-c:a","libopus","-b:a","128k");
1316
+ break;
1317
+ default:
1318
+ return inputPath;
1319
+ }
1320
+
1321
+ return await new Promise((resolve) => {
1322
+ ff.output(outputPath)
1323
+ .on("end", async () => { await safeUnlinkWithRetry(inputPath); resolve(outputPath); })
1324
+ .on("error", () => resolve(inputPath))
1325
+ .run();
1326
+ });
1327
+ } catch (_) {
1328
+ return inputPath;
1329
+ }
1330
+ }
1331
+
1332
+ // Function to get file name from URL
1333
+ function getFileName(url) {
1334
+ try {
1335
+ const urlObj = new URL(url);
1336
+ const pathname = urlObj.pathname;
1337
+ const parts = pathname.split('/');
1338
+ let fileName = parts[parts.length - 1];
1339
+ if (!fileName || fileName.includes('.')) {
1340
+ fileName = `video_${Date.now()}.mp4`;
1341
+ }
1342
+ return fileName;
1343
+ } catch (error) {
1344
+ console.error("Error getting file name from URL:", error.message);
1345
+ return `video_${Date.now()}.mp4`;
1346
+ }
1347
+ }
1348
+
1349
+ // Function to unshorten URLs
1350
+ async function unshortenUrl(url) {
1351
+ try {
1352
+ // Special handling for Facebook URLs
1353
+ if (url.includes('facebook.com') || url.includes('fb.watch') || url.includes('fb.com')) {
1354
+ const response = await axios.get(url, {
1355
+ maxRedirects: 10,
1356
+ timeout: 10000,
1357
+ headers: {
1358
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
1359
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
1360
+ 'Accept-Language': 'en-US,en;q=0.5',
1361
+ 'Accept-Encoding': 'gzip, deflate',
1362
+ 'Connection': 'keep-alive',
1363
+ 'Upgrade-Insecure-Requests': '1'
1364
+ },
1365
+ validateStatus: function (status) {
1366
+ return status >= 200 && status < 400; // Accept redirects
1367
+ }
1368
+ });
1369
+
1370
+ // Get the final URL after all redirects
1371
+ const finalUrl = response.request.res.responseUrl || response.config.url;
1372
+ return finalUrl;
1373
+ }
1374
+
1375
+ // For other URLs, use the original method
1376
+ const response = await axios.head(url, {
1377
+ maxRedirects: 10,
1378
+ timeout: 10000,
1379
+ headers: {
1380
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
1381
+ }
1382
+ });
1383
+ return response.request.res.responseUrl || url;
1384
+ } catch (error) {
1385
+ return url;
1386
+ }
1387
+ }
1388
+
1389
+ // Função para baixar áudio em mp3
1390
+ const AudioDownloader = async (url, options = {}) => {
1391
+ assertAuthorized();
1392
+ const config = { ...defaultConfig, ...options };
1393
+
1394
+ if (!url || !url.includes("http")) {
1395
+ throw new Error("Please specify a valid video URL.");
1396
+ }
1397
+
1398
+ url = await unshortenUrl(extractUrlFromString(url));
1399
+
1400
+ const blacklisted = blacklistLink(url);
1401
+ if (blacklisted) {
1402
+ throw new Error(`URL not supported: ${blacklisted.reason}`);
1403
+ }
1404
+
1405
+ // Verificar se o link está na lista de plataformas suportadas
1406
+ if (!isVideoLink(url)) {
1407
+ throw new Error("This URL is not from a supported platform. Supported platforms: Instagram, X(Twitter), TikTok, Facebook, and YouTube");
1408
+ }
1409
+
1410
+ await cleanupTempFiles(); // Clean up previous temp files
1411
+
1412
+ let downloadedFilePath = null;
1413
+ let audioFilePath = null;
1414
+
1415
+ try {
1416
+ let platform = getPlatformType(url);
1417
+ if (platform === "y124outube") {
1418
+ // Baixar áudio do YouTube usando ytdlp-nodejs
1419
+ let fileName = "temp_audio.mp3";
1420
+ let count = 1;
1421
+ while (fs.existsSync(fileName)) {
1422
+ fileName = `temp_audio_${count}.mp3`;
1423
+ count++;
1424
+ }
1425
+ await downloadYoutubeAudio(url, fileName);
1426
+ audioFilePath = fileName;
1427
+ const result = await uploadToGoFileIfNeeded(audioFilePath);
1428
+ return result;
1429
+ }
1430
+ // Baixar vídeo normalmente
1431
+ try {
1432
+ downloadedFilePath = await downloadSmartVideo(url, config);
1433
+ } catch (error) {
1434
+ try {
1435
+ const fallbackUrl = await tryFallbackDownload(url);
1436
+ downloadedFilePath = await downloadDirectVideo(fallbackUrl, config);
1437
+ } catch (fallbackError) {
1438
+ throw new Error(`Failed to download video with both methods: ${fallbackError.message}`);
1439
+ }
1440
+ }
1441
+
1442
+ if (downloadedFilePath) {
1443
+ // Extrair áudio em mp3
1444
+ audioFilePath = await extractAudioMp3(downloadedFilePath);
1445
+
1446
+ // Remove o arquivo de vídeo temporário após extrair o áudio
1447
+ await safeUnlinkWithRetry(downloadedFilePath);
1448
+
1449
+ const result = await uploadToGoFileIfNeeded(audioFilePath);
1450
+ return result;
1451
+ } else {
1452
+ throw new Error("Failed to obtain downloaded file path.");
1453
+ }
1454
+ } catch (error) {
1455
+ // Clean up any remaining temp files on error
1456
+ await cleanupTempFiles();
1457
+ throw new Error(`Error in AudioDownloader: ${error.message}`);
1458
+ }
1459
+ };
1460
+
1461
+ // Função para extrair áudio em mp3 usando ffmpeg
1462
+ async function extractAudioMp3(videoPath) {
1463
+ return new Promise((resolve, reject) => {
1464
+ const audioPath = path.join(OUTPUT_DIR, `a_${uuidv4().slice(0,4)}.mp3`);
1465
+ ffmpeg(videoPath)
1466
+ .noVideo()
1467
+ .audioCodec('libmp3lame')
1468
+ .audioBitrate(192)
1469
+ .format('mp3')
1470
+ .save(audioPath)
1471
+ .on('end', () => {
1472
+ resolve(audioPath);
1473
+ })
1474
+ .on('error', (err) => {
1475
+ reject(new Error(`Error extracting audio: ${err.message}`));
1476
+ });
1477
+ });
1478
+ }
1479
+
1480
+ MediaDownloader.isVideoLink = isVideoLink;
1481
+ AudioDownloader.isVideoLink = isVideoLink;
1482
+
1483
+ module.exports = {
1484
+ MediaDownloader,
1485
+ AudioDownloader,
1486
+ cleanupTempFiles,
1487
+ safeUnlink,
1488
+ safeUnlinkWithRetry,
1489
+ setAuthKey
1490
+ };