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/.github/workflows/npm-publish.yml +33 -0
- package/README.md +0 -0
- package/gofile.js +44 -0
- package/index.js +1490 -0
- package/package.json +28 -0
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
|
+
};
|