frostpv 1.0.24 → 1.0.26

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 +360 -99
  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.");
@@ -796,20 +801,40 @@ const MediaDownloader = async (url, options = {}) => {
796
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
 
832
+ // 2. If primary failed OR file was invalid, try fallback
833
+ if (!downloadedFilePath) {
834
+ const platform = getPlatformType(url);
810
835
  // YouTube doesn't use fallback method - it has its own specific implementation
811
836
  if (platform === "youtube") {
812
- throw new Error(`YouTube download failed: ${error.message}`);
837
+ throw new Error(`YouTube download failed: ${primaryError?.message || 'Unknown error'}`);
813
838
  }
814
839
 
815
840
  try {
@@ -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,80 @@ 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
  });
1101
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
+ }
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
+ throw new Error("AbortError");
1183
+ }
1184
+ }
1185
+
1108
1186
  return new Promise((resolve, reject) => {
1187
+ // Handle abort inside the Promise so reject is in scope
1188
+ if (signal) {
1189
+ signal.addEventListener('abort', () => {
1190
+ response.data.destroy();
1191
+ videoWriter.destroy();
1192
+ safeUnlink(fileName);
1193
+ reject(new Error("AbortError"));
1194
+ }, { once: true });
1195
+ }
1196
+
1109
1197
  videoWriter.on("finish", async () => {
1110
1198
  try {
1111
- const finalFilePath = await processDownloadedFile(fileName, config, platform);
1199
+ if (downloadedBytes > ABSOLUTE_MAX_BYTES) {
1200
+ throw new Error("Video exceeded 1GB limit.");
1201
+ }
1202
+
1203
+ // Check if file is empty
1204
+ const stats = fs.statSync(fileName);
1205
+ if (stats.size < 200) {
1206
+ await safeUnlinkWithRetry(fileName);
1207
+ throw new Error("Downloaded file is too small or corrupted (likely download failed)");
1208
+ }
1209
+
1210
+ const finalFilePath = await processDownloadedFile(fileName, config, platform, signal);
1112
1211
  triggerGC(); // Limpar memória após processamento pesado
1113
1212
  resolve(finalFilePath);
1114
1213
 
@@ -1127,7 +1226,7 @@ async function downloadSmartVideo(url, config) {
1127
1226
 
1128
1227
 
1129
1228
  // Function for direct video download
1130
- async function downloadDirectVideo(url, config) {
1229
+ async function downloadDirectVideo(url, config, signal = null) {
1131
1230
  try {
1132
1231
  const response = await axios({
1133
1232
  url: url,
@@ -1139,16 +1238,64 @@ async function downloadDirectVideo(url, config) {
1139
1238
  }
1140
1239
  });
1141
1240
 
1241
+ // Check content-length early if available
1242
+ const contentLength = parseInt(response.headers['content-length']);
1243
+ if (!isNaN(contentLength) && contentLength > ABSOLUTE_MAX_BYTES) {
1244
+ throw new Error(`Video is too large (${(contentLength / (1024 * 1024)).toFixed(2)}MB). Max limit is 1GB.`);
1245
+ }
1246
+
1247
+ if (!isNaN(contentLength) && contentLength < 200) {
1248
+ }
1249
+
1142
1250
  // Create minimal unique file name in output dir
1143
- let fileName = path.join(OUTPUT_DIR, `v_${uuidv4().slice(0, 4)}.mp4`);
1251
+ let fileName = path.join(OUTPUT_DIR, `v_${uuidv4().replace(/-/g, '').slice(0, 12)}.mp4`);
1144
1252
 
1145
1253
  const videoWriter = fs.createWriteStream(fileName);
1254
+
1255
+ // Byte counter
1256
+ let downloadedBytes = 0;
1257
+ response.data.on('data', (chunk) => {
1258
+ downloadedBytes += chunk.length;
1259
+ if (downloadedBytes > ABSOLUTE_MAX_BYTES) {
1260
+ response.data.destroy();
1261
+ videoWriter.destroy();
1262
+ safeUnlink(fileName);
1263
+ }
1264
+ });
1265
+
1266
+ // Catch errors on the incoming source stream
1267
+ response.data.on('error', (err) => {
1268
+ videoWriter.destroy();
1269
+ safeUnlink(fileName);
1270
+ });
1271
+
1146
1272
  response.data.pipe(videoWriter);
1147
1273
 
1148
1274
  return new Promise((resolve, reject) => {
1275
+ // Handle abort inside the Promise so reject is in scope
1276
+ if (signal) {
1277
+ signal.addEventListener('abort', () => {
1278
+ response.data.destroy();
1279
+ videoWriter.destroy();
1280
+ safeUnlink(fileName);
1281
+ reject(new Error("AbortError"));
1282
+ }, { once: true });
1283
+ }
1284
+
1149
1285
  videoWriter.on("finish", async () => {
1150
1286
  try {
1151
- const finalFilePath = await processDownloadedFile(fileName, config, "unknown");
1287
+ if (downloadedBytes > ABSOLUTE_MAX_BYTES) {
1288
+ throw new Error("Video exceeded 1GB limit.");
1289
+ }
1290
+
1291
+ // Check if file is empty
1292
+ const stats = fs.statSync(fileName);
1293
+ if (stats.size < 200) {
1294
+ await safeUnlinkWithRetry(fileName);
1295
+ throw new Error("Downloaded file is too small or corrupted (likely download failed)");
1296
+ }
1297
+
1298
+ const finalFilePath = await processDownloadedFile(fileName, config, "unknown", signal);
1152
1299
  resolve(finalFilePath);
1153
1300
  } catch (error) {
1154
1301
  reject(new Error(`Error processing downloaded video: ${error.message}`));
@@ -1176,6 +1323,11 @@ async function downloadGenericFile(url, preferredExt = null) {
1176
1323
  }
1177
1324
  });
1178
1325
 
1326
+ // Check content-length early if available
1327
+ const contentLength = parseInt(response.headers['content-length']);
1328
+ if (!isNaN(contentLength) && contentLength < 200) {
1329
+ }
1330
+
1179
1331
  // Try to infer extension from content-type
1180
1332
  const ct = (response.headers && response.headers['content-type']) || '';
1181
1333
  let ext = preferredExt;
@@ -1189,7 +1341,7 @@ async function downloadGenericFile(url, preferredExt = null) {
1189
1341
  else ext = 'bin';
1190
1342
  }
1191
1343
 
1192
- const fileName = path.join(OUTPUT_DIR, `v_${uuidv4().slice(0, 4)}.${ext}`);
1344
+ const fileName = path.join(OUTPUT_DIR, `v_${uuidv4().replace(/-/g, '').slice(0, 12)}.${ext}`);
1193
1345
  const writer = fs.createWriteStream(fileName);
1194
1346
  response.data.pipe(writer);
1195
1347
 
@@ -1203,8 +1355,8 @@ async function downloadGenericFile(url, preferredExt = null) {
1203
1355
  }
1204
1356
 
1205
1357
  // Function to rotate video
1206
- async function rotateVideo(fileName, rotation) {
1207
- const outputPath = path.join(OUTPUT_DIR, `vr_${uuidv4().slice(0, 4)}.mp4`);
1358
+ async function rotateVideo(fileName, rotation, signal = null) {
1359
+ const outputPath = path.join(OUTPUT_DIR, `vr_${uuidv4().replace(/-/g, '').slice(0, 12)}.mp4`);
1208
1360
  let angle;
1209
1361
  switch (rotation.toLowerCase()) {
1210
1362
  case "left": angle = "transpose=2"; break;
@@ -1215,14 +1367,27 @@ async function rotateVideo(fileName, rotation) {
1215
1367
  }
1216
1368
 
1217
1369
  return new Promise((resolve, reject) => {
1218
- ffmpeg(fileName)
1370
+ const ff = ffmpeg(fileName)
1219
1371
  .videoFilters(angle)
1220
1372
  .audioCodec('copy')
1221
- .output(outputPath)
1222
- .on('end', async () => {
1223
- await safeUnlinkWithRetry(fileName);
1224
- resolve(outputPath);
1225
- })
1373
+ .output(outputPath);
1374
+
1375
+ if (signal) {
1376
+ if (signal.aborted) {
1377
+ reject(new Error("AbortError"));
1378
+ return;
1379
+ }
1380
+ signal.addEventListener('abort', () => {
1381
+ ff.kill('SIGKILL');
1382
+ safeUnlink(outputPath);
1383
+ reject(new Error("AbortError"));
1384
+ }, { once: true });
1385
+ }
1386
+
1387
+ ff.on('end', async () => {
1388
+ await safeUnlinkWithRetry(fileName);
1389
+ resolve(outputPath);
1390
+ })
1226
1391
  .on('error', (err) => reject(new Error(`Error during video rotation: ${err.message}`)))
1227
1392
  .run();
1228
1393
  });
@@ -1253,26 +1418,55 @@ async function deleteTempVideos() {
1253
1418
 
1254
1419
  // Enhanced cleanup function for all temporary files
1255
1420
  async function cleanupTempFiles() {
1421
+ // Clean TEMP_DIR (_compressed files)
1256
1422
  try {
1257
- if (!fs.existsSync(TEMP_DIR)) return;
1258
- const files = fs.readdirSync(TEMP_DIR);
1259
- const tempFiles = files.filter(file =>
1260
- /^temp_video.*\.mp4$/.test(file) ||
1261
- /_audio\.mp3$/.test(file) ||
1262
- /_rotated\.mp4$/.test(file) ||
1263
- /_cropped\.mp4$/.test(file) ||
1264
- /_compressed\.mp4$/.test(file)
1265
- );
1266
- for (const file of tempFiles) {
1267
- await safeUnlinkWithRetry(path.join(TEMP_DIR, file));
1423
+ if (fs.existsSync(TEMP_DIR)) {
1424
+ const files = fs.readdirSync(TEMP_DIR);
1425
+ const tempFiles = files.filter(file => /_compressed\.mp4$/.test(file));
1426
+ const now = Date.now();
1427
+ const GRACE_PERIOD_MS = 5 * 60 * 1000; // 5 minutes
1428
+
1429
+ for (const file of tempFiles) {
1430
+ const full = path.join(TEMP_DIR, file);
1431
+ try {
1432
+ const st = fs.statSync(full);
1433
+ if (now - st.mtimeMs > GRACE_PERIOD_MS) {
1434
+ await safeUnlinkWithRetry(full);
1435
+ }
1436
+ } catch (_) { }
1437
+ }
1268
1438
  }
1269
- triggerGC();
1270
1439
  } catch (error) {
1440
+ if (error && error.code !== 'ENOENT') {
1441
+ console.error(`Error during temp file cleanup (TEMP_DIR): ${error.message}`);
1442
+ }
1443
+ }
1271
1444
 
1445
+ // Also clean OUTPUT_DIR orphan files (v_, vr_, vc_, vx_, vf_, a_ prefixed)
1446
+ try {
1447
+ if (fs.existsSync(OUTPUT_DIR)) {
1448
+ const files = fs.readdirSync(OUTPUT_DIR);
1449
+ const now = Date.now();
1450
+ const GRACE_PERIOD_MS = 5 * 60 * 1000; // 5 minutes
1451
+
1452
+ for (const file of files) {
1453
+ if (!/^(v_|vr_|vc_|vx_|vf_|a_)[0-9a-f]{12}/i.test(file)) continue;
1454
+ const full = path.join(OUTPUT_DIR, file);
1455
+ try {
1456
+ const st = fs.statSync(full);
1457
+ if (now - st.mtimeMs > GRACE_PERIOD_MS) {
1458
+ await safeUnlinkWithRetry(full);
1459
+ }
1460
+ } catch (_) { }
1461
+ }
1462
+ }
1463
+ } catch (error) {
1272
1464
  if (error && error.code !== 'ENOENT') {
1273
- console.error(`Error during temp file cleanup: ${error.message}`);
1465
+ console.error(`Error during temp file cleanup (OUTPUT_DIR): ${error.message}`);
1274
1466
  }
1275
1467
  }
1468
+
1469
+ triggerGC();
1276
1470
  }
1277
1471
 
1278
1472
  // Cleanup files in output folder older than OUTPUT_RETENTION_MIN
@@ -1283,12 +1477,14 @@ async function cleanupOutputFiles() {
1283
1477
  const now = Date.now();
1284
1478
  const maxAgeMs = OUTPUT_RETENTION_MIN * 60 * 1000;
1285
1479
  for (const file of files) {
1286
- // Only consider our generated short names
1287
- if (!/^(v_|vr_|vc_|vx_|vf_|a_)[0-9a-f]{4}/i.test(file)) continue;
1480
+ // Only consider our generated names with exactly 12 char hex suffix
1481
+ if (!/^(v_|vr_|vc_|vx_|vf_|a_)[0-9a-f]{12}/i.test(file)) continue;
1288
1482
  const full = path.join(OUTPUT_DIR, file);
1289
1483
  try {
1290
1484
  const st = fs.statSync(full);
1291
- if (now - st.mtimeMs > maxAgeMs) {
1485
+ const GRACE_PERIOD_MS = 10 * 60 * 1000; // 10 minutes
1486
+ // Only delete if matches retention AND is not extremely new (grace period)
1487
+ if (now - st.mtimeMs > maxAgeMs && now - st.mtimeMs > GRACE_PERIOD_MS) {
1292
1488
  await safeUnlinkWithRetry(full);
1293
1489
  }
1294
1490
  } catch (_) { }
@@ -1298,52 +1494,74 @@ async function cleanupOutputFiles() {
1298
1494
  }
1299
1495
 
1300
1496
 
1301
- // Schedule periodic cleanup (every 10 minutes)
1497
+ // Schedule periodic cleanup (every 5 minutes)
1302
1498
  setInterval(() => {
1303
1499
  cleanupTempFiles();
1304
1500
  cleanupOutputFiles();
1305
- }, 60 * 1000);
1501
+ }, 5 * 60 * 1000);
1306
1502
  // Run once at startup
1307
1503
  cleanupOutputFiles();
1308
1504
 
1309
1505
  // Function to auto-crop video
1310
- async function autoCrop(fileName) {
1506
+ async function autoCrop(fileName, signal = null) {
1311
1507
  const inputPath = fileName;
1312
- const outputPath = path.join(OUTPUT_DIR, `vc_${uuidv4().slice(0, 4)}.mp4`);
1508
+ const outputPath = path.join(OUTPUT_DIR, `vc_${uuidv4().replace(/-/g, '').slice(0, 12)}.mp4`);
1313
1509
 
1314
1510
  return new Promise((resolve, reject) => {
1315
1511
  let cropValues = null;
1316
- ffmpeg(inputPath)
1512
+ const ffDetect = ffmpeg(inputPath)
1317
1513
  .outputOptions('-vf', 'cropdetect=24:16:0')
1318
1514
  .outputFormat('null')
1319
- .output('-')
1320
- .on('stderr', function (stderrLine) {
1321
- const cropMatch = stderrLine.match(/crop=([0-9]+):([0-9]+):([0-9]+):([0-9]+)/);
1322
- if (cropMatch) {
1323
- cropValues = `crop=${cropMatch[1]}:${cropMatch[2]}:${cropMatch[3]}:${cropMatch[4]}`;
1324
- }
1325
- })
1515
+ .output('-');
1516
+
1517
+ if (signal) {
1518
+ if (signal.aborted) {
1519
+ reject(new Error("AbortError"));
1520
+ return;
1521
+ }
1522
+ signal.addEventListener('abort', () => {
1523
+ ffDetect.kill('SIGKILL');
1524
+ reject(new Error("AbortError"));
1525
+ }, { once: true });
1526
+ }
1527
+
1528
+ ffDetect.on('stderr', function (stderrLine) {
1529
+ const cropMatch = stderrLine.match(/crop=([0-9]+):([0-9]+):([0-9]+):([0-9]+)/);
1530
+ if (cropMatch) {
1531
+ cropValues = `crop=${cropMatch[1]}:${cropMatch[2]}:${cropMatch[3]}:${cropMatch[4]}`;
1532
+ }
1533
+ })
1326
1534
  .on('end', function () {
1327
1535
  if (!cropValues) {
1328
1536
  resolve(inputPath);
1329
1537
  return;
1330
1538
  }
1331
- ffmpeg(inputPath)
1539
+ const ffCrop = ffmpeg(inputPath)
1332
1540
  .outputOptions('-vf', cropValues)
1333
1541
  .audioCodec('copy')
1334
- .output(outputPath)
1335
- .on('end', async () => {
1336
- await safeUnlinkWithRetry(inputPath);
1337
- resolve(outputPath);
1338
- })
1542
+ .output(outputPath);
1543
+
1544
+ if (signal) {
1545
+ signal.addEventListener('abort', () => {
1546
+ ffCrop.kill('SIGKILL');
1547
+ safeUnlink(outputPath);
1548
+ reject(new Error("AbortError"));
1549
+ }, { once: true });
1550
+ }
1551
+
1552
+ ffCrop.on('end', async () => {
1553
+ await safeUnlinkWithRetry(inputPath);
1554
+ resolve(outputPath);
1555
+ })
1339
1556
  .on('error', (err) => reject(new Error(`Error during auto-cropping: ${err.message}`)))
1340
1557
  .run();
1341
- });
1558
+ })
1559
+ .run();
1342
1560
  });
1343
1561
  }
1344
1562
 
1345
1563
  // Function to check and compress video
1346
- async function checkAndCompressVideo(filePath, limitSizeMB, platform = null) {
1564
+ async function checkAndCompressVideo(filePath, limitSizeMB, platform = null, signal = null) {
1347
1565
  // Não comprimir vídeos do YouTube
1348
1566
  if (platform === "youtube") return filePath;
1349
1567
 
@@ -1357,21 +1575,34 @@ async function checkAndCompressVideo(filePath, limitSizeMB, platform = null) {
1357
1575
  return filePath;
1358
1576
  }
1359
1577
 
1360
- const outputPath = path.join(OUTPUT_DIR, `vx_${uuidv4().slice(0, 4)}.mp4`);
1578
+ const outputPath = path.join(OUTPUT_DIR, `vx_${uuidv4().replace(/-/g, '').slice(0, 12)}.mp4`);
1361
1579
 
1362
1580
  return new Promise((resolve, reject) => {
1363
- ffmpeg(filePath)
1581
+ const ff = ffmpeg(filePath)
1364
1582
  .outputOptions(
1365
1583
  '-vf', 'scale=trunc(iw/2)*2:trunc(ih/2)*2',
1366
1584
  '-crf', '28',
1367
1585
  '-preset', 'slow'
1368
1586
  )
1369
1587
  .audioCodec('aac')
1370
- .output(outputPath)
1371
- .on('end', async () => {
1372
- await safeUnlinkWithRetry(filePath);
1373
- resolve(outputPath);
1374
- })
1588
+ .output(outputPath);
1589
+
1590
+ if (signal) {
1591
+ if (signal.aborted) {
1592
+ reject(new Error("AbortError"));
1593
+ return;
1594
+ }
1595
+ signal.addEventListener('abort', () => {
1596
+ ff.kill('SIGKILL');
1597
+ safeUnlink(outputPath);
1598
+ reject(new Error("AbortError"));
1599
+ }, { once: true });
1600
+ }
1601
+
1602
+ ff.on('end', async () => {
1603
+ await safeUnlinkWithRetry(filePath);
1604
+ resolve(outputPath);
1605
+ })
1375
1606
  .on('error', (err) => {
1376
1607
  resolve(filePath);
1377
1608
  })
@@ -1380,12 +1611,12 @@ async function checkAndCompressVideo(filePath, limitSizeMB, platform = null) {
1380
1611
  }
1381
1612
 
1382
1613
  // Video format conversion
1383
- async function convertVideoFormat(inputPath, targetFormat) {
1614
+ async function convertVideoFormat(inputPath, targetFormat, signal = null) {
1384
1615
  try {
1385
1616
  const fmt = String(targetFormat).toLowerCase();
1386
1617
 
1387
1618
  if (fmt === "mp3") {
1388
- const audioPath = await extractAudioMp3(inputPath);
1619
+ const audioPath = await extractAudioMp3(inputPath, signal);
1389
1620
  await safeUnlinkWithRetry(inputPath);
1390
1621
  return audioPath;
1391
1622
  }
@@ -1393,9 +1624,17 @@ async function convertVideoFormat(inputPath, targetFormat) {
1393
1624
  const supported = ["mp4", "mov", "webm", "mkv"];
1394
1625
  if (!supported.includes(fmt)) return inputPath;
1395
1626
 
1396
- const outputPath = path.join(OUTPUT_DIR, `vf_${uuidv4().slice(0, 4)}.${fmt}`);
1627
+ const outputPath = path.join(OUTPUT_DIR, `vf_${uuidv4().replace(/-/g, '').slice(0, 12)}.${fmt}`);
1397
1628
 
1398
1629
  const ff = ffmpeg(inputPath);
1630
+ if (signal) {
1631
+ if (signal.aborted) throw new Error("AbortError");
1632
+ signal.addEventListener('abort', () => {
1633
+ ff.kill('SIGKILL');
1634
+ safeUnlink(outputPath);
1635
+ });
1636
+ }
1637
+
1399
1638
  switch (fmt) {
1400
1639
  case "mp4":
1401
1640
  case "mov":
@@ -1484,6 +1723,9 @@ async function unshortenUrl(url) {
1484
1723
  const AudioDownloader = async (url, options = {}) => {
1485
1724
  assertAuthorized();
1486
1725
  const config = { ...defaultConfig, ...options };
1726
+ const signal = config.signal;
1727
+
1728
+ if (signal?.aborted) throw new Error("AbortError");
1487
1729
 
1488
1730
  if (!url || !url.includes("http")) {
1489
1731
  throw new Error("Please specify a valid video URL.");
@@ -1501,7 +1743,8 @@ const AudioDownloader = async (url, options = {}) => {
1501
1743
  throw new Error("This URL is not from a supported platform. Supported platforms: Instagram, X(Twitter), TikTok, Facebook, and YouTube");
1502
1744
  }
1503
1745
 
1504
- await cleanupTempFiles(); // Clean up previous temp files
1746
+ // No longer cleanup at start to avoid race conditions during concurrent downloads
1747
+ // await cleanupTempFiles(); // Clean up previous temp files
1505
1748
 
1506
1749
  let downloadedFilePath = null;
1507
1750
  let audioFilePath = null;
@@ -1518,16 +1761,16 @@ const AudioDownloader = async (url, options = {}) => {
1518
1761
  }
1519
1762
  await downloadYoutubeAudio(url, fileName);
1520
1763
  audioFilePath = fileName;
1521
- const result = await uploadToGoFileIfNeeded(audioFilePath);
1764
+ const result = await uploadToGoFileIfNeeded(audioFilePath, signal);
1522
1765
  return result;
1523
1766
  }
1524
1767
  // Baixar vídeo normalmente
1525
1768
  try {
1526
- downloadedFilePath = await downloadSmartVideo(url, config);
1769
+ downloadedFilePath = await downloadSmartVideo(url, config, signal);
1527
1770
  } catch (error) {
1528
1771
  try {
1529
1772
  const fallbackUrl = await tryFallbackDownload(url);
1530
- downloadedFilePath = await downloadDirectVideo(fallbackUrl, config);
1773
+ downloadedFilePath = await downloadDirectVideo(fallbackUrl, config, signal);
1531
1774
  } catch (fallbackError) {
1532
1775
  throw new Error(`Failed to download video with both methods: ${fallbackError.message}`);
1533
1776
  }
@@ -1535,12 +1778,12 @@ const AudioDownloader = async (url, options = {}) => {
1535
1778
 
1536
1779
  if (downloadedFilePath) {
1537
1780
  // Extrair áudio em mp3
1538
- audioFilePath = await extractAudioMp3(downloadedFilePath);
1781
+ audioFilePath = await extractAudioMp3(downloadedFilePath, signal);
1539
1782
 
1540
1783
  // Remove o arquivo de vídeo temporário após extrair o áudio
1541
1784
  await safeUnlinkWithRetry(downloadedFilePath);
1542
1785
 
1543
- const result = await uploadToGoFileIfNeeded(audioFilePath);
1786
+ const result = await uploadToGoFileIfNeeded(audioFilePath, signal);
1544
1787
  triggerGC();
1545
1788
  return result;
1546
1789
 
@@ -1554,24 +1797,42 @@ const AudioDownloader = async (url, options = {}) => {
1554
1797
  }
1555
1798
  };
1556
1799
 
1557
- // Função para extrair áudio em mp3 usando ffmpeg
1558
- async function extractAudioMp3(videoPath) {
1800
+ async function extractAudioMp3(videoPath, signal = null) {
1559
1801
  return new Promise((resolve, reject) => {
1560
- const audioPath = path.join(OUTPUT_DIR, `a_${uuidv4().slice(0, 4)}.mp3`);
1561
- ffmpeg(videoPath)
1562
- .noVideo()
1563
- .audioCodec('libmp3lame')
1564
- .audioBitrate(192)
1565
- .format('mp3')
1566
- .save(audioPath)
1567
- .on('end', () => {
1568
- triggerGC();
1569
- resolve(audioPath);
1570
- })
1802
+ const fileName = `a_${uuidv4().replace(/-/g, '').slice(0, 12)}.mp3`;
1803
+ const audioPath = path.join(OUTPUT_DIR, fileName);
1804
+
1805
+ // We remove the hardcoded 'libmp3lame' codec which can cause "Invalid argument"
1806
+ // on some statically compiled ffmpeg versions and let it auto-select based on format.
1807
+ const ff = ffmpeg(videoPath)
1808
+ .outputOptions([
1809
+ '-vn', // No video
1810
+ '-b:a', '192k' // Audio bitrate
1811
+ ])
1812
+ .format('mp3');
1813
+
1814
+ if (signal) {
1815
+ if (signal.aborted) {
1816
+ reject(new Error("AbortError"));
1817
+ return;
1818
+ }
1819
+ signal.addEventListener('abort', () => {
1820
+ ff.kill('SIGKILL');
1821
+ safeUnlink(audioPath);
1822
+ reject(new Error("AbortError"));
1823
+ }, { once: true });
1824
+ }
1571
1825
 
1826
+ // Attach events BEFORE calling run or save
1827
+ ff.on('end', () => {
1828
+ triggerGC();
1829
+ resolve(audioPath);
1830
+ })
1572
1831
  .on('error', (err) => {
1573
1832
  reject(new Error(`Error extracting audio: ${err.message}`));
1574
- });
1833
+ })
1834
+ .output(audioPath)
1835
+ .run();
1575
1836
  });
1576
1837
  }
1577
1838
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frostpv",
3
- "version": "1.0.24",
3
+ "version": "1.0.26",
4
4
  "description": "downloads",
5
5
  "main": "index.js",
6
6
  "scripts": {