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.
- package/index.js +319 -90
- 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 = "
|
|
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
|
-
|
|
801
|
+
throw new Error("This URL is not from a supported platform. Supported platforms: Instagram, X(Twitter), TikTok, Facebook");
|
|
797
802
|
}
|
|
798
803
|
|
|
799
|
-
|
|
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
|
-
|
|
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
|
-
|
|
811
|
-
|
|
812
|
-
|
|
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
|
-
|
|
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
|
-
|
|
886
|
+
if (signal?.aborted) throw new Error("AbortError");
|
|
887
|
+
processedFile = await rotateVideo(processedFile, config.rotation, signal);
|
|
851
888
|
}
|
|
852
889
|
if (config.autocrop) {
|
|
853
|
-
|
|
890
|
+
if (signal?.aborted) throw new Error("AbortError");
|
|
891
|
+
processedFile = await autoCrop(processedFile, signal);
|
|
854
892
|
}
|
|
855
|
-
|
|
893
|
+
if (signal?.aborted) throw new Error("AbortError");
|
|
894
|
+
processedFile = await checkAndCompressVideo(processedFile, config.limitSizeMB, platform, signal);
|
|
856
895
|
if (config.outputFormat) {
|
|
857
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
-
.
|
|
1221
|
-
.
|
|
1222
|
-
|
|
1223
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1286
|
-
if (!/^(v_|vr_|vc_|vx_|vf_|a_)[0-9a-f]{
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
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
|
-
.
|
|
1333
|
-
.
|
|
1334
|
-
|
|
1335
|
-
|
|
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,
|
|
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
|
-
.
|
|
1368
|
-
.
|
|
1369
|
-
|
|
1370
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
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}`));
|