frostpv 1.0.25 → 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 +111 -76
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -798,7 +798,7 @@ const MediaDownloader = async (url, options = {}) => {
798
798
  // Verificar se é YouTube e lançar erro customizado
799
799
  const platform = getPlatformType(url);
800
800
  if (platform === "youtube") {
801
- 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");
802
802
  }
803
803
 
804
804
  // No longer cleanup at start to avoid race conditions during concurrent downloads
@@ -811,17 +811,17 @@ const MediaDownloader = async (url, options = {}) => {
811
811
  // 1. Try primary download method
812
812
  try {
813
813
  downloadedFilePath = await downloadSmartVideo(url, config, signal);
814
-
814
+
815
815
  // Verify integrity of primary download
816
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
- }
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
825
  }
826
826
  } catch (error) {
827
827
  if (signal?.aborted || error.message?.includes("AbortError")) throw error;
@@ -831,13 +831,13 @@ const MediaDownloader = async (url, options = {}) => {
831
831
 
832
832
  // 2. If primary failed OR file was invalid, try fallback
833
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
- }
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
+ }
839
839
 
840
- try {
840
+ try {
841
841
  const fallbackUrl = await tryFallbackDownload(url);
842
842
 
843
843
  // Validate fallback URL
@@ -847,14 +847,14 @@ const MediaDownloader = async (url, options = {}) => {
847
847
  }
848
848
 
849
849
  downloadedFilePath = await downloadDirectVideo(fallbackUrl, config, signal);
850
-
850
+
851
851
  // Verify integrity of fallback download
852
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
- }
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
858
  }
859
859
 
860
860
  } catch (fallbackError) {
@@ -1139,7 +1139,7 @@ async function downloadSmartVideo(url, config, signal = null) {
1139
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'
1140
1140
  }
1141
1141
  });
1142
-
1142
+
1143
1143
  // Check content-length early if available
1144
1144
  const contentLength = parseInt(response.headers['content-length']);
1145
1145
  if (!isNaN(contentLength) && contentLength > ABSOLUTE_MAX_BYTES) {
@@ -1153,7 +1153,7 @@ async function downloadSmartVideo(url, config, signal = null) {
1153
1153
  let fileName = path.join(OUTPUT_DIR, `v_${uuidv4().replace(/-/g, '').slice(0, 12)}.mp4`);
1154
1154
 
1155
1155
  const videoWriter = fs.createWriteStream(fileName);
1156
-
1156
+
1157
1157
  // Byte counter to abort if stream exceeds limit
1158
1158
  let downloadedBytes = 0;
1159
1159
  response.data.on('data', (chunk) => {
@@ -1179,23 +1179,25 @@ async function downloadSmartVideo(url, config, signal = null) {
1179
1179
  response.data.destroy();
1180
1180
  videoWriter.destroy();
1181
1181
  safeUnlink(fileName);
1182
- reject(new Error("AbortError"));
1183
- return;
1182
+ throw new Error("AbortError");
1184
1183
  }
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
1184
  }
1193
1185
 
1194
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
+
1195
1197
  videoWriter.on("finish", async () => {
1196
1198
  try {
1197
1199
  if (downloadedBytes > ABSOLUTE_MAX_BYTES) {
1198
- throw new Error("Video exceeded 1GB limit.");
1200
+ throw new Error("Video exceeded 1GB limit.");
1199
1201
  }
1200
1202
 
1201
1203
  // Check if file is empty
@@ -1235,7 +1237,7 @@ async function downloadDirectVideo(url, config, signal = null) {
1235
1237
  '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'
1236
1238
  }
1237
1239
  });
1238
-
1240
+
1239
1241
  // Check content-length early if available
1240
1242
  const contentLength = parseInt(response.headers['content-length']);
1241
1243
  if (!isNaN(contentLength) && contentLength > ABSOLUTE_MAX_BYTES) {
@@ -1269,14 +1271,17 @@ async function downloadDirectVideo(url, config, signal = null) {
1269
1271
 
1270
1272
  response.data.pipe(videoWriter);
1271
1273
 
1272
- if (signal) {
1273
- signal.addEventListener('abort', () => {
1274
- videoWriter.destroy();
1275
- safeUnlink(fileName);
1276
- });
1277
- }
1278
-
1279
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
+
1280
1285
  videoWriter.on("finish", async () => {
1281
1286
  try {
1282
1287
  if (downloadedBytes > ABSOLUTE_MAX_BYTES) {
@@ -1376,7 +1381,7 @@ async function rotateVideo(fileName, rotation, signal = null) {
1376
1381
  ff.kill('SIGKILL');
1377
1382
  safeUnlink(outputPath);
1378
1383
  reject(new Error("AbortError"));
1379
- });
1384
+ }, { once: true });
1380
1385
  }
1381
1386
 
1382
1387
  ff.on('end', async () => {
@@ -1413,31 +1418,55 @@ async function deleteTempVideos() {
1413
1418
 
1414
1419
  // Enhanced cleanup function for all temporary files
1415
1420
  async function cleanupTempFiles() {
1421
+ // Clean TEMP_DIR (_compressed files)
1416
1422
  try {
1417
- if (!fs.existsSync(TEMP_DIR)) return;
1418
- const files = fs.readdirSync(TEMP_DIR);
1419
- const tempFiles = files.filter(file =>
1420
- /_compressed\.mp4$/.test(file)
1421
- );
1422
- const now = Date.now();
1423
- const GRACE_PERIOD_MS = 10 * 60 * 1000; // 10 minutes
1424
-
1425
- for (const file of tempFiles) {
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 (_) { }
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
+ }
1433
1438
  }
1434
- triggerGC();
1435
1439
  } catch (error) {
1440
+ if (error && error.code !== 'ENOENT') {
1441
+ console.error(`Error during temp file cleanup (TEMP_DIR): ${error.message}`);
1442
+ }
1443
+ }
1436
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) {
1437
1464
  if (error && error.code !== 'ENOENT') {
1438
- console.error(`Error during temp file cleanup: ${error.message}`);
1465
+ console.error(`Error during temp file cleanup (OUTPUT_DIR): ${error.message}`);
1439
1466
  }
1440
1467
  }
1468
+
1469
+ triggerGC();
1441
1470
  }
1442
1471
 
1443
1472
  // Cleanup files in output folder older than OUTPUT_RETENTION_MIN
@@ -1465,11 +1494,11 @@ async function cleanupOutputFiles() {
1465
1494
  }
1466
1495
 
1467
1496
 
1468
- // Schedule periodic cleanup (every 10 minutes)
1497
+ // Schedule periodic cleanup (every 5 minutes)
1469
1498
  setInterval(() => {
1470
1499
  cleanupTempFiles();
1471
1500
  cleanupOutputFiles();
1472
- }, 60 * 1000);
1501
+ }, 5 * 60 * 1000);
1473
1502
  // Run once at startup
1474
1503
  cleanupOutputFiles();
1475
1504
 
@@ -1493,7 +1522,7 @@ async function autoCrop(fileName, signal = null) {
1493
1522
  signal.addEventListener('abort', () => {
1494
1523
  ffDetect.kill('SIGKILL');
1495
1524
  reject(new Error("AbortError"));
1496
- });
1525
+ }, { once: true });
1497
1526
  }
1498
1527
 
1499
1528
  ffDetect.on('stderr', function (stderrLine) {
@@ -1517,7 +1546,7 @@ async function autoCrop(fileName, signal = null) {
1517
1546
  ffCrop.kill('SIGKILL');
1518
1547
  safeUnlink(outputPath);
1519
1548
  reject(new Error("AbortError"));
1520
- });
1549
+ }, { once: true });
1521
1550
  }
1522
1551
 
1523
1552
  ffCrop.on('end', async () => {
@@ -1567,7 +1596,7 @@ async function checkAndCompressVideo(filePath, limitSizeMB, platform = null, sig
1567
1596
  ff.kill('SIGKILL');
1568
1597
  safeUnlink(outputPath);
1569
1598
  reject(new Error("AbortError"));
1570
- });
1599
+ }, { once: true });
1571
1600
  }
1572
1601
 
1573
1602
  ff.on('end', async () => {
@@ -1770,13 +1799,17 @@ const AudioDownloader = async (url, options = {}) => {
1770
1799
 
1771
1800
  async function extractAudioMp3(videoPath, signal = null) {
1772
1801
  return new Promise((resolve, reject) => {
1773
- const audioPath = path.join(OUTPUT_DIR, `a_${uuidv4().replace(/-/g, '').slice(0, 12)}.mp3`);
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.
1774
1807
  const ff = ffmpeg(videoPath)
1775
- .noVideo()
1776
- .audioCodec('libmp3lame')
1777
- .audioBitrate(192)
1778
- .format('mp3')
1779
- .save(audioPath);
1808
+ .outputOptions([
1809
+ '-vn', // No video
1810
+ '-b:a', '192k' // Audio bitrate
1811
+ ])
1812
+ .format('mp3');
1780
1813
 
1781
1814
  if (signal) {
1782
1815
  if (signal.aborted) {
@@ -1784,20 +1817,22 @@ async function extractAudioMp3(videoPath, signal = null) {
1784
1817
  return;
1785
1818
  }
1786
1819
  signal.addEventListener('abort', () => {
1787
- ff.kill();
1820
+ ff.kill('SIGKILL');
1788
1821
  safeUnlink(audioPath);
1789
1822
  reject(new Error("AbortError"));
1790
- });
1823
+ }, { once: true });
1791
1824
  }
1792
1825
 
1826
+ // Attach events BEFORE calling run or save
1793
1827
  ff.on('end', () => {
1794
1828
  triggerGC();
1795
1829
  resolve(audioPath);
1796
1830
  })
1797
-
1798
1831
  .on('error', (err) => {
1799
1832
  reject(new Error(`Error extracting audio: ${err.message}`));
1800
- });
1833
+ })
1834
+ .output(audioPath)
1835
+ .run();
1801
1836
  });
1802
1837
  }
1803
1838
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frostpv",
3
- "version": "1.0.25",
3
+ "version": "1.0.26",
4
4
  "description": "downloads",
5
5
  "main": "index.js",
6
6
  "scripts": {