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.
- package/index.js +111 -76
- 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
|
-
|
|
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
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
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
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
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
|
-
|
|
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
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
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
|
|
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
|
|
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
|
-
.
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
.
|
|
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
|
|