frostpv 1.0.22 → 1.0.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/index.js +319 -90
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -27,12 +27,13 @@ try { fs.mkdirSync(OUTPUT_DIR, { recursive: true }); } catch (_) { }
27
27
 
28
28
  const TEMP_DIR = path.join(os.tmpdir(), "downloader-dl-bot");
29
29
  try { fs.mkdirSync(TEMP_DIR, { recursive: true }); } catch (_) { }
30
- const OUTPUT_RETENTION_MIN = Number(process.env.OUTPUT_RETENTION_MIN || 5);
30
+ const OUTPUT_RETENTION_MIN = Math.max(5, Number(process.env.OUTPUT_RETENTION_MIN || 5));
31
31
 
32
- const GOFILE_API = "1eNR6qXgKMn6GGl4oRcfOlQnxrgSrNSL";
32
+ const GOFILE_API = "Cs05tkrbwIb0vNdXJWI5nee869RDJG37";
33
33
  const GOFILE_UPLOAD_URL = "https://upload.gofile.io/uploadfile";
34
34
  const SIZE_LIMIT_MB = 10;
35
35
  const SIZE_LIMIT_BYTES = SIZE_LIMIT_MB * 1024 * 1024;
36
+ const ABSOLUTE_MAX_BYTES = 1024 * 1024 * 1024; // 1GB limit to prevent RAM exhaustion
36
37
 
37
38
  const videoPlatforms = [
38
39
  "https://www.instagram.com",
@@ -173,7 +174,7 @@ async function safeUnlinkWithRetry(filePath, maxRetries = 3) {
173
174
  }
174
175
 
175
176
  // Function to upload to GoFile if needed
176
- async function uploadToGoFileIfNeeded(filePath) {
177
+ async function uploadToGoFileIfNeeded(filePath, signal = null) {
177
178
  try {
178
179
  const stats = fs.statSync(filePath);
179
180
  const fileSizeInBytes = stats.size;
@@ -194,10 +195,11 @@ async function uploadToGoFileIfNeeded(filePath) {
194
195
  headers: {
195
196
  ...form.getHeaders(),
196
197
  "Authorization": `Bearer ${GOFILE_API}`,
197
- "Content-Length": form.getLengthSync()
198
+ "Content-Length": form.getLengthSync()
198
199
  },
199
200
  maxContentLength: Infinity,
200
- maxBodyLength: Infinity
201
+ maxBodyLength: Infinity,
202
+ signal: signal
201
203
  });
202
204
 
203
205
  if (response.data && response.data.status === "ok") {
@@ -761,6 +763,9 @@ async function tryFallbackDownload(url, maxRetries = 3) {
761
763
  const MediaDownloader = async (url, options = {}) => {
762
764
  assertAuthorized();
763
765
  const config = { ...defaultConfig, ...options };
766
+ const signal = config.signal;
767
+
768
+ if (signal?.aborted) throw new Error("AbortError");
764
769
 
765
770
  if (!url || !url.includes("http")) {
766
771
  throw new Error("Please specify a valid video URL.");
@@ -793,26 +798,46 @@ const MediaDownloader = async (url, options = {}) => {
793
798
  // Verificar se é YouTube e lançar erro customizado
794
799
  const platform = getPlatformType(url);
795
800
  if (platform === "youtube") {
796
- throw new Error("This URL is not from a supported platform. Supported platforms: Instagram, X(Twitter), TikTok, Facebook");
801
+ throw new Error("This URL is not from a supported platform. Supported platforms: Instagram, X(Twitter), TikTok, Facebook");
797
802
  }
798
803
 
799
- await cleanupTempFiles(); // Clean up previous temp files
804
+ // No longer cleanup at start to avoid race conditions during concurrent downloads
805
+ // await cleanupTempFiles(); // Clean up previous temp files
800
806
 
801
807
  let downloadedFilePath = null;
808
+ let primaryError = null;
802
809
 
803
810
  try {
804
- // Try primary download method
811
+ // 1. Try primary download method
805
812
  try {
806
- downloadedFilePath = await downloadSmartVideo(url, config);
813
+ downloadedFilePath = await downloadSmartVideo(url, config, signal);
814
+
815
+ // Verify integrity of primary download
816
+ if (downloadedFilePath) {
817
+ try {
818
+ const stats = fs.statSync(downloadedFilePath);
819
+ if (stats.size < 500) {
820
+ throw new Error(`Downloaded file is too small, likely corrupted.`);
821
+ }
822
+ } catch (statError) {
823
+ throw new Error(`Failed to verify file integrity`);
824
+ }
825
+ }
807
826
  } catch (error) {
808
- const platform = getPlatformType(url);
827
+ if (signal?.aborted || error.message?.includes("AbortError")) throw error;
828
+ primaryError = error;
829
+ downloadedFilePath = null; // Ensure null to trigger fallback
830
+ }
809
831
 
810
- // YouTube doesn't use fallback method - it has its own specific implementation
811
- if (platform === "youtube") {
812
- throw new Error(`YouTube download failed: ${error.message}`);
813
- }
832
+ // 2. If primary failed OR file was invalid, try fallback
833
+ if (!downloadedFilePath) {
834
+ const platform = getPlatformType(url);
835
+ // YouTube doesn't use fallback method - it has its own specific implementation
836
+ if (platform === "youtube") {
837
+ throw new Error(`YouTube download failed: ${primaryError?.message || 'Unknown error'}`);
838
+ }
814
839
 
815
- try {
840
+ try {
816
841
  const fallbackUrl = await tryFallbackDownload(url);
817
842
 
818
843
  // Validate fallback URL
@@ -821,8 +846,19 @@ const MediaDownloader = async (url, options = {}) => {
821
846
  throw new Error("Fallback URL validation failed");
822
847
  }
823
848
 
824
- downloadedFilePath = await downloadDirectVideo(fallbackUrl, config);
849
+ downloadedFilePath = await downloadDirectVideo(fallbackUrl, config, signal);
850
+
851
+ // Verify integrity of fallback download
852
+ if (downloadedFilePath) {
853
+ const stats = fs.statSync(downloadedFilePath);
854
+ if (stats.size < 500) {
855
+ await safeUnlinkWithRetry(downloadedFilePath); // Clean up bad file
856
+ throw new Error(`Fallback downloaded file is too small, likely corrupted.`);
857
+ }
858
+ }
859
+
825
860
  } catch (fallbackError) {
861
+ // If both failed, throw a combined error
826
862
  throw new Error(`Failed to download video with both methods: ${fallbackError.message}`);
827
863
  }
828
864
  }
@@ -831,10 +867,10 @@ const MediaDownloader = async (url, options = {}) => {
831
867
  if (config.noGofile) {
832
868
  return { type: "local", value: downloadedFilePath };
833
869
  }
834
- const result = await uploadToGoFileIfNeeded(downloadedFilePath);
870
+ const result = await uploadToGoFileIfNeeded(downloadedFilePath, signal);
835
871
  return result;
836
872
  } else {
837
- throw new Error("Failed to obtain downloaded file path.");
873
+ throw new Error("Failed to obtain downloaded file path (both methods failed).");
838
874
  }
839
875
  } catch (error) {
840
876
  // Clean up any remaining temp files on error
@@ -844,23 +880,27 @@ const MediaDownloader = async (url, options = {}) => {
844
880
  };
845
881
 
846
882
  // Function to process downloaded file
847
- async function processDownloadedFile(fileName, config, platform = null) {
883
+ async function processDownloadedFile(fileName, config, platform = null, signal = null) {
848
884
  let processedFile = path.resolve(fileName);
849
885
  if (config.rotation) {
850
- processedFile = await rotateVideo(processedFile, config.rotation);
886
+ if (signal?.aborted) throw new Error("AbortError");
887
+ processedFile = await rotateVideo(processedFile, config.rotation, signal);
851
888
  }
852
889
  if (config.autocrop) {
853
- processedFile = await autoCrop(processedFile);
890
+ if (signal?.aborted) throw new Error("AbortError");
891
+ processedFile = await autoCrop(processedFile, signal);
854
892
  }
855
- processedFile = await checkAndCompressVideo(processedFile, config.limitSizeMB, platform);
893
+ if (signal?.aborted) throw new Error("AbortError");
894
+ processedFile = await checkAndCompressVideo(processedFile, config.limitSizeMB, platform, signal);
856
895
  if (config.outputFormat) {
857
- processedFile = await convertVideoFormat(processedFile, String(config.outputFormat).toLowerCase());
896
+ if (signal?.aborted) throw new Error("AbortError");
897
+ processedFile = await convertVideoFormat(processedFile, String(config.outputFormat).toLowerCase(), signal);
858
898
  }
859
899
  return processedFile;
860
900
  }
861
901
 
862
902
  // Function for platform-specific video download
863
- async function downloadSmartVideo(url, config) {
903
+ async function downloadSmartVideo(url, config, signal = null) {
864
904
  try {
865
905
  let videoUrl = null;
866
906
  let platform = getPlatformType(url);
@@ -1094,21 +1134,78 @@ async function downloadSmartVideo(url, config) {
1094
1134
  method: "GET",
1095
1135
  responseType: "stream",
1096
1136
  timeout: 30000,
1137
+ signal: signal,
1097
1138
  headers: {
1098
1139
  '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'
1099
1140
  }
1100
1141
  });
1142
+
1143
+ // Check content-length early if available
1144
+ const contentLength = parseInt(response.headers['content-length']);
1145
+ if (!isNaN(contentLength) && contentLength > ABSOLUTE_MAX_BYTES) {
1146
+ throw new Error(`Video is too large (${(contentLength / (1024 * 1024)).toFixed(2)}MB). Max limit is 1GB.`);
1147
+ }
1148
+
1149
+ if (!isNaN(contentLength) && contentLength < 200) {
1150
+ }
1101
1151
 
1102
1152
  // Create minimal unique file name in output dir
1103
- let fileName = path.join(OUTPUT_DIR, `v_${uuidv4().slice(0, 4)}.mp4`);
1153
+ let fileName = path.join(OUTPUT_DIR, `v_${uuidv4().replace(/-/g, '').slice(0, 12)}.mp4`);
1104
1154
 
1105
1155
  const videoWriter = fs.createWriteStream(fileName);
1156
+
1157
+ // Byte counter to abort if stream exceeds limit
1158
+ let downloadedBytes = 0;
1159
+ response.data.on('data', (chunk) => {
1160
+ downloadedBytes += chunk.length;
1161
+ if (downloadedBytes > ABSOLUTE_MAX_BYTES) {
1162
+ response.data.destroy(); // Stop the download
1163
+ videoWriter.destroy();
1164
+ safeUnlink(fileName);
1165
+ // We can't easily throw from here to the outer scope, but the writer error/finish will handle it
1166
+ }
1167
+ });
1168
+
1169
+ // Catch errors on the incoming source stream
1170
+ response.data.on('error', (err) => {
1171
+ videoWriter.destroy();
1172
+ safeUnlink(fileName);
1173
+ });
1174
+
1106
1175
  response.data.pipe(videoWriter);
1107
1176
 
1177
+ if (signal) {
1178
+ if (signal.aborted) {
1179
+ response.data.destroy();
1180
+ videoWriter.destroy();
1181
+ safeUnlink(fileName);
1182
+ reject(new Error("AbortError"));
1183
+ return;
1184
+ }
1185
+ signal.addEventListener('abort', () => {
1186
+ response.data.destroy();
1187
+ videoWriter.destroy();
1188
+ safeUnlink(fileName);
1189
+ // We don't reject here because the writer error/finish will handle it or the outer timeout will catch it
1190
+ // But rejecting helps propagate the abort faster
1191
+ });
1192
+ }
1193
+
1108
1194
  return new Promise((resolve, reject) => {
1109
1195
  videoWriter.on("finish", async () => {
1110
1196
  try {
1111
- const finalFilePath = await processDownloadedFile(fileName, config, platform);
1197
+ if (downloadedBytes > ABSOLUTE_MAX_BYTES) {
1198
+ throw new Error("Video exceeded 1GB limit.");
1199
+ }
1200
+
1201
+ // Check if file is empty
1202
+ const stats = fs.statSync(fileName);
1203
+ if (stats.size < 200) {
1204
+ await safeUnlinkWithRetry(fileName);
1205
+ throw new Error("Downloaded file is too small or corrupted (likely download failed)");
1206
+ }
1207
+
1208
+ const finalFilePath = await processDownloadedFile(fileName, config, platform, signal);
1112
1209
  triggerGC(); // Limpar memória após processamento pesado
1113
1210
  resolve(finalFilePath);
1114
1211
 
@@ -1127,7 +1224,7 @@ async function downloadSmartVideo(url, config) {
1127
1224
 
1128
1225
 
1129
1226
  // Function for direct video download
1130
- async function downloadDirectVideo(url, config) {
1227
+ async function downloadDirectVideo(url, config, signal = null) {
1131
1228
  try {
1132
1229
  const response = await axios({
1133
1230
  url: url,
@@ -1138,17 +1235,62 @@ async function downloadDirectVideo(url, config) {
1138
1235
  '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'
1139
1236
  }
1140
1237
  });
1238
+
1239
+ // Check content-length early if available
1240
+ const contentLength = parseInt(response.headers['content-length']);
1241
+ if (!isNaN(contentLength) && contentLength > ABSOLUTE_MAX_BYTES) {
1242
+ throw new Error(`Video is too large (${(contentLength / (1024 * 1024)).toFixed(2)}MB). Max limit is 1GB.`);
1243
+ }
1244
+
1245
+ if (!isNaN(contentLength) && contentLength < 200) {
1246
+ }
1141
1247
 
1142
1248
  // Create minimal unique file name in output dir
1143
- let fileName = path.join(OUTPUT_DIR, `v_${uuidv4().slice(0, 4)}.mp4`);
1249
+ let fileName = path.join(OUTPUT_DIR, `v_${uuidv4().replace(/-/g, '').slice(0, 12)}.mp4`);
1144
1250
 
1145
1251
  const videoWriter = fs.createWriteStream(fileName);
1252
+
1253
+ // Byte counter
1254
+ let downloadedBytes = 0;
1255
+ response.data.on('data', (chunk) => {
1256
+ downloadedBytes += chunk.length;
1257
+ if (downloadedBytes > ABSOLUTE_MAX_BYTES) {
1258
+ response.data.destroy();
1259
+ videoWriter.destroy();
1260
+ safeUnlink(fileName);
1261
+ }
1262
+ });
1263
+
1264
+ // Catch errors on the incoming source stream
1265
+ response.data.on('error', (err) => {
1266
+ videoWriter.destroy();
1267
+ safeUnlink(fileName);
1268
+ });
1269
+
1146
1270
  response.data.pipe(videoWriter);
1147
1271
 
1272
+ if (signal) {
1273
+ signal.addEventListener('abort', () => {
1274
+ videoWriter.destroy();
1275
+ safeUnlink(fileName);
1276
+ });
1277
+ }
1278
+
1148
1279
  return new Promise((resolve, reject) => {
1149
1280
  videoWriter.on("finish", async () => {
1150
1281
  try {
1151
- const finalFilePath = await processDownloadedFile(fileName, config, "unknown");
1282
+ if (downloadedBytes > ABSOLUTE_MAX_BYTES) {
1283
+ throw new Error("Video exceeded 1GB limit.");
1284
+ }
1285
+
1286
+ // Check if file is empty
1287
+ const stats = fs.statSync(fileName);
1288
+ if (stats.size < 200) {
1289
+ await safeUnlinkWithRetry(fileName);
1290
+ throw new Error("Downloaded file is too small or corrupted (likely download failed)");
1291
+ }
1292
+
1293
+ const finalFilePath = await processDownloadedFile(fileName, config, "unknown", signal);
1152
1294
  resolve(finalFilePath);
1153
1295
  } catch (error) {
1154
1296
  reject(new Error(`Error processing downloaded video: ${error.message}`));
@@ -1176,6 +1318,11 @@ async function downloadGenericFile(url, preferredExt = null) {
1176
1318
  }
1177
1319
  });
1178
1320
 
1321
+ // Check content-length early if available
1322
+ const contentLength = parseInt(response.headers['content-length']);
1323
+ if (!isNaN(contentLength) && contentLength < 200) {
1324
+ }
1325
+
1179
1326
  // Try to infer extension from content-type
1180
1327
  const ct = (response.headers && response.headers['content-type']) || '';
1181
1328
  let ext = preferredExt;
@@ -1189,7 +1336,7 @@ async function downloadGenericFile(url, preferredExt = null) {
1189
1336
  else ext = 'bin';
1190
1337
  }
1191
1338
 
1192
- const fileName = path.join(OUTPUT_DIR, `v_${uuidv4().slice(0, 4)}.${ext}`);
1339
+ const fileName = path.join(OUTPUT_DIR, `v_${uuidv4().replace(/-/g, '').slice(0, 12)}.${ext}`);
1193
1340
  const writer = fs.createWriteStream(fileName);
1194
1341
  response.data.pipe(writer);
1195
1342
 
@@ -1203,8 +1350,8 @@ async function downloadGenericFile(url, preferredExt = null) {
1203
1350
  }
1204
1351
 
1205
1352
  // Function to rotate video
1206
- async function rotateVideo(fileName, rotation) {
1207
- const outputPath = path.join(OUTPUT_DIR, `vr_${uuidv4().slice(0, 4)}.mp4`);
1353
+ async function rotateVideo(fileName, rotation, signal = null) {
1354
+ const outputPath = path.join(OUTPUT_DIR, `vr_${uuidv4().replace(/-/g, '').slice(0, 12)}.mp4`);
1208
1355
  let angle;
1209
1356
  switch (rotation.toLowerCase()) {
1210
1357
  case "left": angle = "transpose=2"; break;
@@ -1215,13 +1362,27 @@ async function rotateVideo(fileName, rotation) {
1215
1362
  }
1216
1363
 
1217
1364
  return new Promise((resolve, reject) => {
1218
- ffmpeg(fileName)
1365
+ const ff = ffmpeg(fileName)
1219
1366
  .videoFilters(angle)
1220
- .output(outputPath)
1221
- .on('end', async () => {
1222
- await safeUnlinkWithRetry(fileName);
1223
- resolve(outputPath);
1224
- })
1367
+ .audioCodec('copy')
1368
+ .output(outputPath);
1369
+
1370
+ if (signal) {
1371
+ if (signal.aborted) {
1372
+ reject(new Error("AbortError"));
1373
+ return;
1374
+ }
1375
+ signal.addEventListener('abort', () => {
1376
+ ff.kill('SIGKILL');
1377
+ safeUnlink(outputPath);
1378
+ reject(new Error("AbortError"));
1379
+ });
1380
+ }
1381
+
1382
+ ff.on('end', async () => {
1383
+ await safeUnlinkWithRetry(fileName);
1384
+ resolve(outputPath);
1385
+ })
1225
1386
  .on('error', (err) => reject(new Error(`Error during video rotation: ${err.message}`)))
1226
1387
  .run();
1227
1388
  });
@@ -1256,14 +1417,19 @@ async function cleanupTempFiles() {
1256
1417
  if (!fs.existsSync(TEMP_DIR)) return;
1257
1418
  const files = fs.readdirSync(TEMP_DIR);
1258
1419
  const tempFiles = files.filter(file =>
1259
- /^temp_video.*\.mp4$/.test(file) ||
1260
- /_audio\.mp3$/.test(file) ||
1261
- /_rotated\.mp4$/.test(file) ||
1262
- /_cropped\.mp4$/.test(file) ||
1263
- /_compressed\.mp4$/.test(file)
1420
+ /_compressed\.mp4$/.test(file)
1264
1421
  );
1422
+ const now = Date.now();
1423
+ const GRACE_PERIOD_MS = 10 * 60 * 1000; // 10 minutes
1424
+
1265
1425
  for (const file of tempFiles) {
1266
- await safeUnlinkWithRetry(path.join(TEMP_DIR, file));
1426
+ const full = path.join(TEMP_DIR, file);
1427
+ try {
1428
+ const st = fs.statSync(full);
1429
+ if (now - st.mtimeMs > GRACE_PERIOD_MS) {
1430
+ await safeUnlinkWithRetry(full);
1431
+ }
1432
+ } catch (_) { }
1267
1433
  }
1268
1434
  triggerGC();
1269
1435
  } catch (error) {
@@ -1282,12 +1448,14 @@ async function cleanupOutputFiles() {
1282
1448
  const now = Date.now();
1283
1449
  const maxAgeMs = OUTPUT_RETENTION_MIN * 60 * 1000;
1284
1450
  for (const file of files) {
1285
- // Only consider our generated short names
1286
- if (!/^(v_|vr_|vc_|vx_|vf_|a_)[0-9a-f]{4}/i.test(file)) continue;
1451
+ // Only consider our generated names with exactly 12 char hex suffix
1452
+ if (!/^(v_|vr_|vc_|vx_|vf_|a_)[0-9a-f]{12}/i.test(file)) continue;
1287
1453
  const full = path.join(OUTPUT_DIR, file);
1288
1454
  try {
1289
1455
  const st = fs.statSync(full);
1290
- if (now - st.mtimeMs > maxAgeMs) {
1456
+ const GRACE_PERIOD_MS = 10 * 60 * 1000; // 10 minutes
1457
+ // Only delete if matches retention AND is not extremely new (grace period)
1458
+ if (now - st.mtimeMs > maxAgeMs && now - st.mtimeMs > GRACE_PERIOD_MS) {
1291
1459
  await safeUnlinkWithRetry(full);
1292
1460
  }
1293
1461
  } catch (_) { }
@@ -1306,42 +1474,65 @@ setInterval(() => {
1306
1474
  cleanupOutputFiles();
1307
1475
 
1308
1476
  // Function to auto-crop video
1309
- async function autoCrop(fileName) {
1477
+ async function autoCrop(fileName, signal = null) {
1310
1478
  const inputPath = fileName;
1311
- const outputPath = path.join(OUTPUT_DIR, `vc_${uuidv4().slice(0, 4)}.mp4`);
1479
+ const outputPath = path.join(OUTPUT_DIR, `vc_${uuidv4().replace(/-/g, '').slice(0, 12)}.mp4`);
1312
1480
 
1313
1481
  return new Promise((resolve, reject) => {
1314
1482
  let cropValues = null;
1315
- ffmpeg(inputPath)
1483
+ const ffDetect = ffmpeg(inputPath)
1316
1484
  .outputOptions('-vf', 'cropdetect=24:16:0')
1317
1485
  .outputFormat('null')
1318
- .output('-')
1319
- .on('stderr', function (stderrLine) {
1320
- const cropMatch = stderrLine.match(/crop=([0-9]+):([0-9]+):([0-9]+):([0-9]+)/);
1321
- if (cropMatch) {
1322
- cropValues = `crop=${cropMatch[1]}:${cropMatch[2]}:${cropMatch[3]}:${cropMatch[4]}`;
1323
- }
1324
- })
1486
+ .output('-');
1487
+
1488
+ if (signal) {
1489
+ if (signal.aborted) {
1490
+ reject(new Error("AbortError"));
1491
+ return;
1492
+ }
1493
+ signal.addEventListener('abort', () => {
1494
+ ffDetect.kill('SIGKILL');
1495
+ reject(new Error("AbortError"));
1496
+ });
1497
+ }
1498
+
1499
+ ffDetect.on('stderr', function (stderrLine) {
1500
+ const cropMatch = stderrLine.match(/crop=([0-9]+):([0-9]+):([0-9]+):([0-9]+)/);
1501
+ if (cropMatch) {
1502
+ cropValues = `crop=${cropMatch[1]}:${cropMatch[2]}:${cropMatch[3]}:${cropMatch[4]}`;
1503
+ }
1504
+ })
1325
1505
  .on('end', function () {
1326
1506
  if (!cropValues) {
1327
1507
  resolve(inputPath);
1328
1508
  return;
1329
1509
  }
1330
- ffmpeg(inputPath)
1510
+ const ffCrop = ffmpeg(inputPath)
1331
1511
  .outputOptions('-vf', cropValues)
1332
- .output(outputPath)
1333
- .on('end', async () => {
1334
- await safeUnlinkWithRetry(inputPath);
1335
- resolve(outputPath);
1336
- })
1512
+ .audioCodec('copy')
1513
+ .output(outputPath);
1514
+
1515
+ if (signal) {
1516
+ signal.addEventListener('abort', () => {
1517
+ ffCrop.kill('SIGKILL');
1518
+ safeUnlink(outputPath);
1519
+ reject(new Error("AbortError"));
1520
+ });
1521
+ }
1522
+
1523
+ ffCrop.on('end', async () => {
1524
+ await safeUnlinkWithRetry(inputPath);
1525
+ resolve(outputPath);
1526
+ })
1337
1527
  .on('error', (err) => reject(new Error(`Error during auto-cropping: ${err.message}`)))
1338
1528
  .run();
1339
- });
1529
+ })
1530
+ .run();
1340
1531
  });
1341
1532
  }
1342
1533
 
1343
1534
  // Function to check and compress video
1344
- async function checkAndCompressVideo(filePath, limitSizeMB, platform = null) {
1535
+ async function checkAndCompressVideo(filePath, limitSizeMB, platform = null, signal = null) {
1345
1536
  // Não comprimir vídeos do YouTube
1346
1537
  if (platform === "youtube") return filePath;
1347
1538
 
@@ -1355,20 +1546,34 @@ async function checkAndCompressVideo(filePath, limitSizeMB, platform = null) {
1355
1546
  return filePath;
1356
1547
  }
1357
1548
 
1358
- const outputPath = path.join(OUTPUT_DIR, `vx_${uuidv4().slice(0, 4)}.mp4`);
1549
+ const outputPath = path.join(OUTPUT_DIR, `vx_${uuidv4().replace(/-/g, '').slice(0, 12)}.mp4`);
1359
1550
 
1360
1551
  return new Promise((resolve, reject) => {
1361
- ffmpeg(filePath)
1552
+ const ff = ffmpeg(filePath)
1362
1553
  .outputOptions(
1363
1554
  '-vf', 'scale=trunc(iw/2)*2:trunc(ih/2)*2',
1364
1555
  '-crf', '28',
1365
1556
  '-preset', 'slow'
1366
1557
  )
1367
- .output(outputPath)
1368
- .on('end', async () => {
1369
- await safeUnlinkWithRetry(filePath);
1370
- resolve(outputPath);
1371
- })
1558
+ .audioCodec('aac')
1559
+ .output(outputPath);
1560
+
1561
+ if (signal) {
1562
+ if (signal.aborted) {
1563
+ reject(new Error("AbortError"));
1564
+ return;
1565
+ }
1566
+ signal.addEventListener('abort', () => {
1567
+ ff.kill('SIGKILL');
1568
+ safeUnlink(outputPath);
1569
+ reject(new Error("AbortError"));
1570
+ });
1571
+ }
1572
+
1573
+ ff.on('end', async () => {
1574
+ await safeUnlinkWithRetry(filePath);
1575
+ resolve(outputPath);
1576
+ })
1372
1577
  .on('error', (err) => {
1373
1578
  resolve(filePath);
1374
1579
  })
@@ -1377,12 +1582,12 @@ async function checkAndCompressVideo(filePath, limitSizeMB, platform = null) {
1377
1582
  }
1378
1583
 
1379
1584
  // Video format conversion
1380
- async function convertVideoFormat(inputPath, targetFormat) {
1585
+ async function convertVideoFormat(inputPath, targetFormat, signal = null) {
1381
1586
  try {
1382
1587
  const fmt = String(targetFormat).toLowerCase();
1383
1588
 
1384
1589
  if (fmt === "mp3") {
1385
- const audioPath = await extractAudioMp3(inputPath);
1590
+ const audioPath = await extractAudioMp3(inputPath, signal);
1386
1591
  await safeUnlinkWithRetry(inputPath);
1387
1592
  return audioPath;
1388
1593
  }
@@ -1390,9 +1595,17 @@ async function convertVideoFormat(inputPath, targetFormat) {
1390
1595
  const supported = ["mp4", "mov", "webm", "mkv"];
1391
1596
  if (!supported.includes(fmt)) return inputPath;
1392
1597
 
1393
- const outputPath = path.join(OUTPUT_DIR, `vf_${uuidv4().slice(0, 4)}.${fmt}`);
1598
+ const outputPath = path.join(OUTPUT_DIR, `vf_${uuidv4().replace(/-/g, '').slice(0, 12)}.${fmt}`);
1394
1599
 
1395
1600
  const ff = ffmpeg(inputPath);
1601
+ if (signal) {
1602
+ if (signal.aborted) throw new Error("AbortError");
1603
+ signal.addEventListener('abort', () => {
1604
+ ff.kill('SIGKILL');
1605
+ safeUnlink(outputPath);
1606
+ });
1607
+ }
1608
+
1396
1609
  switch (fmt) {
1397
1610
  case "mp4":
1398
1611
  case "mov":
@@ -1481,6 +1694,9 @@ async function unshortenUrl(url) {
1481
1694
  const AudioDownloader = async (url, options = {}) => {
1482
1695
  assertAuthorized();
1483
1696
  const config = { ...defaultConfig, ...options };
1697
+ const signal = config.signal;
1698
+
1699
+ if (signal?.aborted) throw new Error("AbortError");
1484
1700
 
1485
1701
  if (!url || !url.includes("http")) {
1486
1702
  throw new Error("Please specify a valid video URL.");
@@ -1498,7 +1714,8 @@ const AudioDownloader = async (url, options = {}) => {
1498
1714
  throw new Error("This URL is not from a supported platform. Supported platforms: Instagram, X(Twitter), TikTok, Facebook, and YouTube");
1499
1715
  }
1500
1716
 
1501
- await cleanupTempFiles(); // Clean up previous temp files
1717
+ // No longer cleanup at start to avoid race conditions during concurrent downloads
1718
+ // await cleanupTempFiles(); // Clean up previous temp files
1502
1719
 
1503
1720
  let downloadedFilePath = null;
1504
1721
  let audioFilePath = null;
@@ -1515,16 +1732,16 @@ const AudioDownloader = async (url, options = {}) => {
1515
1732
  }
1516
1733
  await downloadYoutubeAudio(url, fileName);
1517
1734
  audioFilePath = fileName;
1518
- const result = await uploadToGoFileIfNeeded(audioFilePath);
1735
+ const result = await uploadToGoFileIfNeeded(audioFilePath, signal);
1519
1736
  return result;
1520
1737
  }
1521
1738
  // Baixar vídeo normalmente
1522
1739
  try {
1523
- downloadedFilePath = await downloadSmartVideo(url, config);
1740
+ downloadedFilePath = await downloadSmartVideo(url, config, signal);
1524
1741
  } catch (error) {
1525
1742
  try {
1526
1743
  const fallbackUrl = await tryFallbackDownload(url);
1527
- downloadedFilePath = await downloadDirectVideo(fallbackUrl, config);
1744
+ downloadedFilePath = await downloadDirectVideo(fallbackUrl, config, signal);
1528
1745
  } catch (fallbackError) {
1529
1746
  throw new Error(`Failed to download video with both methods: ${fallbackError.message}`);
1530
1747
  }
@@ -1532,12 +1749,12 @@ const AudioDownloader = async (url, options = {}) => {
1532
1749
 
1533
1750
  if (downloadedFilePath) {
1534
1751
  // Extrair áudio em mp3
1535
- audioFilePath = await extractAudioMp3(downloadedFilePath);
1752
+ audioFilePath = await extractAudioMp3(downloadedFilePath, signal);
1536
1753
 
1537
1754
  // Remove o arquivo de vídeo temporário após extrair o áudio
1538
1755
  await safeUnlinkWithRetry(downloadedFilePath);
1539
1756
 
1540
- const result = await uploadToGoFileIfNeeded(audioFilePath);
1757
+ const result = await uploadToGoFileIfNeeded(audioFilePath, signal);
1541
1758
  triggerGC();
1542
1759
  return result;
1543
1760
 
@@ -1551,20 +1768,32 @@ const AudioDownloader = async (url, options = {}) => {
1551
1768
  }
1552
1769
  };
1553
1770
 
1554
- // Função para extrair áudio em mp3 usando ffmpeg
1555
- async function extractAudioMp3(videoPath) {
1771
+ async function extractAudioMp3(videoPath, signal = null) {
1556
1772
  return new Promise((resolve, reject) => {
1557
- const audioPath = path.join(OUTPUT_DIR, `a_${uuidv4().slice(0, 4)}.mp3`);
1558
- ffmpeg(videoPath)
1773
+ const audioPath = path.join(OUTPUT_DIR, `a_${uuidv4().replace(/-/g, '').slice(0, 12)}.mp3`);
1774
+ const ff = ffmpeg(videoPath)
1559
1775
  .noVideo()
1560
1776
  .audioCodec('libmp3lame')
1561
1777
  .audioBitrate(192)
1562
1778
  .format('mp3')
1563
- .save(audioPath)
1564
- .on('end', () => {
1565
- triggerGC();
1566
- resolve(audioPath);
1567
- })
1779
+ .save(audioPath);
1780
+
1781
+ if (signal) {
1782
+ if (signal.aborted) {
1783
+ reject(new Error("AbortError"));
1784
+ return;
1785
+ }
1786
+ signal.addEventListener('abort', () => {
1787
+ ff.kill();
1788
+ safeUnlink(audioPath);
1789
+ reject(new Error("AbortError"));
1790
+ });
1791
+ }
1792
+
1793
+ ff.on('end', () => {
1794
+ triggerGC();
1795
+ resolve(audioPath);
1796
+ })
1568
1797
 
1569
1798
  .on('error', (err) => {
1570
1799
  reject(new Error(`Error extracting audio: ${err.message}`));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frostpv",
3
- "version": "1.0.22",
3
+ "version": "1.0.25",
4
4
  "description": "downloads",
5
5
  "main": "index.js",
6
6
  "scripts": {