first commit
This commit is contained in:
54
.gitignore
vendored
Normal file
54
.gitignore
vendored
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# Use yarn.lock instead
|
||||||
|
package-lock.json
|
||||||
|
|
||||||
|
*.mp4
|
||||||
|
*.mkv
|
||||||
|
*.m4a
|
||||||
|
*.srt
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
#.env
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
|
||||||
|
# Documentation
|
||||||
|
docs
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules
|
||||||
|
jspm_packages
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
[Uu]ploads/
|
||||||
|
dist/
|
||||||
254
index.js
Normal file
254
index.js
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const Queue = require('bull');
|
||||||
|
const { exec, spawn } = require('child_process');
|
||||||
|
const fs = require('fs').promises;
|
||||||
|
const path = require('path');
|
||||||
|
const cors = require('cors');
|
||||||
|
const EventEmitter = require('events');
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const port = 3000;
|
||||||
|
|
||||||
|
app.use(cors());
|
||||||
|
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
// Configuration de la file d'attente Bull
|
||||||
|
const videoQueue = new Queue('crawl', 'redis://192.168.1.222:32768');
|
||||||
|
|
||||||
|
videoQueue.on('error', (e) => {
|
||||||
|
console.log('An error occured', e);
|
||||||
|
});
|
||||||
|
|
||||||
|
const runCommand = (command) => {
|
||||||
|
console.log('⚙️ Will execute: ' + command);
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const cmd = spawn(command, { shell: true });
|
||||||
|
let lastLog = '';
|
||||||
|
|
||||||
|
cmd.stdout.on('data', (data) => {
|
||||||
|
lastLog = data.toString();
|
||||||
|
process.stdout.write(data.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
cmd.stderr.on('data', (data) => {
|
||||||
|
lastLog = data.toString();
|
||||||
|
process.stderr.write(`stderr: ${data.toString()}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
cmd.on('close', (code) => {
|
||||||
|
if (code === 0) {
|
||||||
|
resolve('Command executed successfully.');
|
||||||
|
} else {
|
||||||
|
reject(lastLog || 'Command failed with status code ' + code);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const runProgressCommand = (command) => {
|
||||||
|
console.log('⚙️ Will execute: ' + command);
|
||||||
|
const emitter = new EventEmitter();
|
||||||
|
|
||||||
|
const executeCommand = new Promise((resolve, reject) => {
|
||||||
|
const cmd = spawn(command, { shell: true });
|
||||||
|
let lastLog = '';
|
||||||
|
|
||||||
|
cmd.stdout.on('data', (data) => {
|
||||||
|
lastLog = data.toString();
|
||||||
|
const perc = extractPercentage(data.toString());
|
||||||
|
if (!perc) {
|
||||||
|
process.stdout.write(data.toString());
|
||||||
|
} else {
|
||||||
|
//console.log('🐳 Percentage ====> ' + perc);
|
||||||
|
emitter.emit('percentage', perc);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
cmd.stderr.on('data', (data) => {
|
||||||
|
lastLog = data.toString();
|
||||||
|
process.stderr.write(`stderr: ${data.toString()}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
cmd.on('close', (code) => {
|
||||||
|
if (code === 0) {
|
||||||
|
resolve('Command executed successfully.');
|
||||||
|
} else {
|
||||||
|
reject(lastLog || 'Command failed with status code ' + code);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return { executeCommand, emitter };
|
||||||
|
};
|
||||||
|
|
||||||
|
// Route pour démarrer le processus
|
||||||
|
app.post('/start-process', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const { mp4Filename, mpdUrl, keys, wantedResolution } = req.body;
|
||||||
|
console.log(req.body);
|
||||||
|
const job = await videoQueue.add({
|
||||||
|
mp4Filename,
|
||||||
|
mpdUrl,
|
||||||
|
keys,
|
||||||
|
wantedResolution
|
||||||
|
});
|
||||||
|
res.json({ jobId: job.id });
|
||||||
|
} catch(e) {
|
||||||
|
console.log(e);
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Route pour vérifier le statut du job
|
||||||
|
app.get('/job-status/:jobId', async (req, res) => {
|
||||||
|
const job = await videoQueue.getJob(req.params.jobId);
|
||||||
|
if (job === null) {
|
||||||
|
res.status(404).json({ error: 'Job not found' });
|
||||||
|
} else {
|
||||||
|
const state = await job.getState();
|
||||||
|
const progress = job._progress;
|
||||||
|
res.json({ jobId: job.id, state, progress });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/jobs-status', async (req, res) => {
|
||||||
|
const jobs = await videoQueue.getJobs();
|
||||||
|
res.json(await Promise.all(jobs.splice(0, 25).map(async job => ({
|
||||||
|
id: job.id,
|
||||||
|
state: await job.getState(),
|
||||||
|
progress: job._progress,
|
||||||
|
addedOn: job.timestamp,
|
||||||
|
processedOn: job.processedOn,
|
||||||
|
finishedOn: job.finishedOn,
|
||||||
|
returnValue: job.returnvalue,
|
||||||
|
failedReason: job.failedReason,
|
||||||
|
data: job.data
|
||||||
|
}))));
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/download/:filename', async (req, res) => {
|
||||||
|
const { filename } = req.params;
|
||||||
|
const file = path.join(process.cwd(), filename);
|
||||||
|
res.download(file);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const checkFilesExistance = (pattern) => {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
const files = await fs.readdir(process.cwd());
|
||||||
|
resolve(files.filter(file => file.includes(pattern)));
|
||||||
|
} catch(e) {
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const extractPercentage = (line) => {
|
||||||
|
const s = line.split('%');
|
||||||
|
const percentages = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < s.length; i++) {
|
||||||
|
if (s[i].length === 0) continue;
|
||||||
|
const ss = s[i].split(' ');
|
||||||
|
if (ss.length > 0 && !isNaN(ss[ss.length - 1]))
|
||||||
|
percentages.push(parseFloat(ss[ss.length - 1]));
|
||||||
|
}
|
||||||
|
if (percentages.length === 0) return null;
|
||||||
|
return Math.max(...percentages.filter((p) => p < 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Processus de la file d'attente
|
||||||
|
videoQueue.process((job) => {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
const { mp4Filename, mpdUrl, keys, wantedResolution } = job.data;
|
||||||
|
const downloaderPath = path.join(process.env.HOME, 'Downloads/N_m3u8DL-RE_Beta_osx-arm64/N_m3u8DL-RE');
|
||||||
|
const mp4decryptPath = path.join(process.env.HOME, 'Downloads/Bento4-SDK-1-6-0-641.universal-apple-macosx/bin/mp4decrypt');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const filesExist = await checkFilesExistance('encrypted');
|
||||||
|
|
||||||
|
if (filesExist.length === 0) {
|
||||||
|
const resPattern = {
|
||||||
|
'4k': [3840, 2160],
|
||||||
|
'1080p': [1920, 1080],
|
||||||
|
'720p': [1280, 720],
|
||||||
|
'480p': [854, 480],
|
||||||
|
'360p': [640, 360],
|
||||||
|
'240p': [426, 240]
|
||||||
|
}[wantedResolution] || [1920, 1080];
|
||||||
|
console.log('Encrypted files not found, downloading...');
|
||||||
|
job.progress(10);
|
||||||
|
const { executeCommand, emitter } = runProgressCommand(`${downloaderPath} \"${mpdUrl}\" --save-name ${mp4Filename}_encrypted --select-video \"(?=.*${resPattern[0]})(?=.*${resPattern[1]})\" --select-audio lang=\"fr|en\":for=best2 --select-subtitle all`, true);
|
||||||
|
emitter.on('percentage', (percentage) => {
|
||||||
|
console.log(`Download Progression : ${percentage}%`);
|
||||||
|
job.progress(Math.round(10 + (percentage / 5)));
|
||||||
|
});
|
||||||
|
await executeCommand;
|
||||||
|
} else {
|
||||||
|
console.log('Encrypted files already exist, bypassing download...')
|
||||||
|
}
|
||||||
|
|
||||||
|
job.progress(30);
|
||||||
|
|
||||||
|
// Décryptage vidéo
|
||||||
|
await runCommand(`${mp4decryptPath} ${keys.map(k => `--key ${k.key}:${k.value}`).join(' ')} "${mp4Filename}_encrypted.mp4" "${mp4Filename}_decrypted.mp4"`);
|
||||||
|
|
||||||
|
job.progress(50);
|
||||||
|
|
||||||
|
// Décryptage audio
|
||||||
|
const audioFiles = await fs.readdir('.');
|
||||||
|
const finalAudio = [];
|
||||||
|
for (const file of audioFiles) {
|
||||||
|
if (file.startsWith(`${mp4Filename}_encrypted`) && file.endsWith('.m4a')) {
|
||||||
|
const baseName = path.basename(file, '.m4a');
|
||||||
|
await runCommand(`${mp4decryptPath} ${keys.map(k => `--key ${k.key}:${k.value}`).join(' ')} "${file}" "${baseName}_decrypted.m4a"`);
|
||||||
|
finalAudio.push(`${baseName}_decrypted.m4a`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
job.progress(70);
|
||||||
|
|
||||||
|
// Combinaison avec ffmpeg
|
||||||
|
let ffmpegCommand = `ffmpeg -y -i ${mp4Filename}_decrypted.mp4`;
|
||||||
|
let mapCommand = ' -map 0:v';
|
||||||
|
let inputIndex = 1;
|
||||||
|
|
||||||
|
for (const file of finalAudio) {
|
||||||
|
//if (file.startsWith(`${mp4Filename}_encrypted`) && file.endsWith('_decrypted.m4a')) {
|
||||||
|
ffmpegCommand += ` -i ${file}`;
|
||||||
|
mapCommand += ` -map ${inputIndex}:a`;
|
||||||
|
inputIndex++;
|
||||||
|
//}
|
||||||
|
}
|
||||||
|
|
||||||
|
ffmpegCommand += `${mapCommand} -c copy ${mp4Filename}.mp4`;
|
||||||
|
await runCommand(ffmpegCommand);
|
||||||
|
|
||||||
|
job.progress(90);
|
||||||
|
|
||||||
|
// Renommage des fichiers SRT
|
||||||
|
let counter = 1;
|
||||||
|
for (const file of audioFiles) {
|
||||||
|
if (file.startsWith(`${mp4Filename}_encrypted`) && file.endsWith('.srt')) {
|
||||||
|
await fs.rename(file, `${mp4Filename}_${counter}.srt`);
|
||||||
|
counter++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nettoyage (commenté pour correspondre au script original)
|
||||||
|
await runCommand(`rm ${mp4Filename}_encrypted* && rm ${mp4Filename}_decrypted*`);
|
||||||
|
|
||||||
|
job.progress(100);
|
||||||
|
resolve({ message: `File fetched and decrypted with success: ${mp4Filename}.mp4`, filePath: `${mp4Filename}.mp4`, fileName: `${mp4Filename}.mp4` });
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Error while processing task', error)
|
||||||
|
reject(new Error(`${error.toString() || error}`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(port, () => {
|
||||||
|
console.log(`Server running at http://localhost:${port}`);
|
||||||
|
});
|
||||||
18
package.json
Normal file
18
package.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "crawlflix-api",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"bull": "^4.16.3",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"express": "^4.21.0",
|
||||||
|
"fs": "^0.0.1-security",
|
||||||
|
"path": "^0.12.7"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user