Compare commits

...

2 Commits

Author SHA1 Message Date
Joris Bertomeu
345af9d759 Add alot of new cool feats
All checks were successful
ci / Image build (push) Successful in 51s
ci / Deployment (push) Successful in 9s
2024-10-02 16:09:24 +02:00
Joris Bertomeu
4dfacbf7e1 Alot of feats 2024-10-02 11:37:15 +02:00
2 changed files with 129 additions and 53 deletions

181
index.js
View File

@@ -5,6 +5,8 @@ const fs = require('fs');
const path = require('path'); const path = require('path');
const cors = require('cors'); const cors = require('cors');
const EventEmitter = require('events'); const EventEmitter = require('events');
const axios = require('axios');
var mpdParser = require('mpd-parser');
const softwareService = require('./services/softwares'); const softwareService = require('./services/softwares');
const BASE_PATH = process.env.DATA_PATH || `./data`; const BASE_PATH = process.env.DATA_PATH || `./data`;
@@ -87,13 +89,15 @@ const runProgressCommand = (command) => {
// Route pour démarrer le processus // Route pour démarrer le processus
app.post('/start-process', async (req, res, next) => { app.post('/start-process', async (req, res, next) => {
try { try {
const { mp4Filename, mpdUrl, keys, wantedResolution } = req.body; const { mp4Filename, mpdUrl, keys, wantedResolution, wantedAudioTracks, wantedSubtitles } = req.body;
console.log(req.body); console.log(JSON.stringify(req.body, null, 2));
const job = await videoQueue.add({ const job = await videoQueue.add({
mp4Filename, mp4Filename,
mpdUrl, mpdUrl,
keys, keys,
wantedResolution wantedResolution,
wantedAudioTracks,
wantedSubtitles
}); });
res.json({ jobId: job.id }); res.json({ jobId: job.id });
} catch(e) { } catch(e) {
@@ -183,91 +187,162 @@ const extractPercentage = (line) => {
return Math.max(...percentages.filter((p) => p < 100)); return Math.max(...percentages.filter((p) => p < 100));
} }
const parseMPDStream = async (mpdUrl) => {
const mpdResponse = await axios({
url: mpdUrl,
method: 'GET',
responseType: 'text'
});
const eventHandler = ({ type, message }) => console.log(`${type}: ${message}`);
const parsedManifest = mpdParser.parse(mpdResponse.data , { mpdUrl, eventHandler });
const obj = {
audioTracks: [],
videoTracks: [],
subtitles: []
};
const toParse = [{
rootProp: 'AUDIO',
subProp: 'audio',
targetProp: 'audioTracks'
}, {
rootProp: 'SUBTITLES',
subProp: 'subs',
targetProp: 'subtitles'
}]
toParse.forEach(({ rootProp, subProp, targetProp }) => {
for (const [key, value] of Object.entries(parsedManifest?.mediaGroups?.[rootProp]?.[subProp])) {
for (let i = 0; i < value.playlists.length; i++) {
obj[targetProp].push({
name: key,
language: value.language,
attributes: value.playlists[i].attributes
});
}
}
});
for (let i = 0; i < parsedManifest.playlists.length; i++) {
obj.videoTracks.push({
name: `${parsedManifest.playlists?.[i]?.attributes?.RESOLUTION?.width || 'N/C'}x${parsedManifest.playlists?.[i]?.attributes?.RESOLUTION?.height || 'N/C'}`,
codec: parsedManifest.playlists?.[i]?.attributes?.CODECS,
bandwidth: parsedManifest.playlists?.[i]?.attributes?.BANDWIDTH,
fps: parsedManifest.playlists?.[i]?.attributes?.['FRAME-RATE'],
resolution: {
width: parsedManifest.playlists?.[i]?.attributes?.RESOLUTION?.width,
height: parsedManifest.playlists?.[i]?.attributes?.RESOLUTION?.height
}
});
}
obj.videoTracks = obj.videoTracks.sort((a, b) => b.resolution.width - a.resolution.width);
return obj;
};
app.post('/processMPD', async (req, res, next) => {
try {
res.json(await parseMPDStream(req.body.mpdUrl));
} catch(e) {
console.log(e);
next(e);
}
});
// Processus de la file d'attente // Processus de la file d'attente
videoQueue.process((job) => { videoQueue.process((job) => {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
try { try {
const { mp4Filename, mpdUrl, keys, wantedResolution } = job.data; console.log('Will launch job')
const { mp4Filename, mpdUrl, keys, wantedResolution, wantedAudioTracks, wantedSubtitles } = job.data;
const downloaderPath = softwareService.getLocalBinFileInfo('downloader').path; const downloaderPath = softwareService.getLocalBinFileInfo('downloader').path;
const mp4decryptPath = softwareService.getLocalBinFileInfo('mp4decrypt').path; const mp4decryptPath = softwareService.getLocalBinFileInfo('mp4decrypt').path;
const mp4TmpFilepath = path.join(TMP_PATH, `${mp4Filename}.mp4`);
const mp4FinalFilepath = path.join(OUTPUT_PATH, `${mp4Filename}.mp4`); const workdir = path.join(TMP_PATH, mp4Filename);
console.log('1') if (!fs.existsSync(workdir))
const filesExist = await checkFilesExistance('encrypted', TMP_PATH); fs.mkdirSync(workdir);
console.log('2') const mp4FilenameWithExt= `${mp4Filename}.mp4`;
const finalPath = path.join(OUTPUT_PATH, mp4Filename);
const filesExist = await checkFilesExistance('encrypted', workdir);
if (filesExist.length === 0) { 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...'); console.log('Encrypted files not found, downloading...');
job.progress(10); let objectsDownloaded = -1, previousPercentage = -1;
const { executeCommand, emitter } = runProgressCommand(`${downloaderPath} \"${mpdUrl}\" --save-name ${mp4TmpFilepath}_encrypted --select-video \"(?=.*${resPattern[0]})(?=.*${resPattern[1]})\" --select-audio lang=\"fr|en\":for=best2 --select-subtitle all`, true); const objectsToDownload = 1 + wantedAudioTracks.length + wantedSubtitles.length;
job.progress(10); // Début à 10%
const subPart = wantedSubtitles.length > 0 ? `--select-subtitle lang=\"${wantedSubtitles.map(elem => elem.language).join('|')}\":bwMin=\"${wantedSubtitles.map(elem => Math.floor(elem.attributes.BANDWIDTH / 1000 -1)).join('|')}\":bwMax=\"${wantedSubtitles.map(elem => Math.round(elem.attributes.BANDWIDTH / 1000 + 1)).join('|')}\"` : '--drop-subtitle lang=\".*\"';
const audioPart = wantedAudioTracks.length > 0 ? `--select-audio lang=\"${wantedAudioTracks.map(elem => elem.language).join('|')}\":codecs=\"${wantedAudioTracks.map(elem => elem.attributes.CODECS).join('|')}\":bwMin=\"${wantedAudioTracks.map(elem => Math.floor(elem.attributes.BANDWIDTH / 1000 -1)).join('|')}\":bwMax=\"${wantedAudioTracks.map(elem => Math.round(elem.attributes.BANDWIDTH / 1000 + 1)).join('|')}\"` : '--drop-audio lang=\".*\"';
const { executeCommand, emitter } = runProgressCommand(`${downloaderPath} \"${mpdUrl}\" --save-dir ${workdir} --save-name ${mp4Filename}_encrypted --select-video res=\"${wantedResolution.resolution.width}*\" ${audioPart} ${subPart}`, true);
emitter.on('percentage', (percentage) => { emitter.on('percentage', (percentage) => {
console.log(`Download Progression : ${percentage}%`); if (percentage < previousPercentage) {
job.progress(Math.round(10 + (percentage / 5))); objectsDownloaded++;
}
previousPercentage = percentage;
const subPercMax = 50 / objectsToDownload;
job.progress(Math.round((10 + (objectsDownloaded * subPercMax)) + (percentage * subPercMax / 100)));
}); });
await executeCommand; await executeCommand;
} else { } else {
console.log('Encrypted files already exist, bypassing download...') console.log('Encrypted files already exist, bypassing download...')
} }
job.progress(60);
job.progress(30); // Decrypt video stream
await runCommand(`${mp4decryptPath} ${keys.map(k => `--key ${k.key}:${k.value}`).join(' ')} "${workdir}/${mp4Filename}_encrypted.mp4" "${workdir}/${mp4Filename}_decrypted.mp4"`);
// Décryptage vidéo
await runCommand(`${mp4decryptPath} ${keys.map(k => `--key ${k.key}:${k.value}`).join(' ')} "${mp4TmpFilepath}_encrypted.mp4" "${mp4TmpFilepath}_decrypted.mp4"`);
job.progress(50);
// Décryptage audio
const audioFiles = fs.readdirSync(mp4TmpFilepath);
const finalAudio = [];
for (const file of audioFiles) {
if (file.startsWith(`${mp4TmpFilepath}_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); job.progress(70);
// Combinaison avec ffmpeg // Decrypt audio streams
let ffmpegCommand = `ffmpeg -y -i ${mp4TmpFilepath}_decrypted.mp4`; const audioFiles = fs.readdirSync(workdir);
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(' ')} "${workdir}/${file}" "${workdir}/${baseName}_decrypted.m4a"`);
finalAudio.push(`${workdir}/${baseName}_decrypted.m4a`);
}
}
job.progress(80);
// Combinaison avec ffmpeg
let ffmpegCommand = `ffmpeg -y -i ${workdir}/${mp4Filename}_decrypted.mp4`;
let mapCommand = ' -map 0:v'; let mapCommand = ' -map 0:v';
let inputIndex = 1; let inputIndex = 1;
for (const file of finalAudio) { for (const file of finalAudio) {
//if (file.startsWith(`${mp4Filename}_encrypted`) && file.endsWith('_decrypted.m4a')) { ffmpegCommand += ` -i ${file}`;
ffmpegCommand += ` -i ${file}`; mapCommand += ` -map ${inputIndex}:a`;
mapCommand += ` -map ${inputIndex}:a`; inputIndex++;
inputIndex++;
//}
} }
ffmpegCommand += `${mapCommand} -c copy ${mp4FinalFilepath}.mp4`; if (!fs.existsSync(finalPath))
fs.mkdirSync(finalPath);
ffmpegCommand += `${mapCommand} -c copy ${finalPath}/${mp4FilenameWithExt}`;
await runCommand(ffmpegCommand); await runCommand(ffmpegCommand);
job.progress(90); job.progress(90);
// Renommage des fichiers SRT // Renommage des fichiers SRT
const subFiles = fs.readdirSync(workdir);
let counter = 1; let counter = 1;
for (const file of audioFiles) { for (const file of subFiles) {
if (file.startsWith(`${mp4TmpFilepath}_encrypted`) && file.endsWith('.srt')) { if (file.startsWith(`${mp4Filename}_encrypted`) && file.endsWith('.srt')) {
fs.renameSync(file, `${mp4FinalFilepath}_${counter}.srt`); fs.renameSync(`${workdir}/${file}`, `${finalPath}/${mp4Filename}_${counter}.srt`);
counter++; counter++;
} }
} }
// Nettoyage (commenté pour correspondre au script original) // Nettoyage (commenté pour correspondre au script original)
await runCommand(`rm ${mp4TmpFilepath}_encrypted* && rm ${mp4TmpFilepath}_decrypted*`); await runCommand(`rm -fr ${workdir}`);
job.progress(100); job.progress(100);
resolve({ message: `File fetched and decrypted with success: ${mp4Filename}.mp4`, filePath: `${mp4FinalFilepath}.mp4`, fileName: `${mp4Filename}.mp4` }); resolve({ message: `File fetched and decrypted with success: ${mp4Filename}.mp4`, filePath: `${OUTPUT_PATH}/${mp4Filename}.mp4`, fileName: `${mp4Filename}.mp4` });
} catch (error) { } catch (error) {
console.log('Error while processing task', error) console.log('Error while processing task', error)
reject(new Error(`${error.toString() || error}`)); reject(new Error(`${error.toString() || error}`));

View File

@@ -15,6 +15,7 @@
"cors": "^2.8.5", "cors": "^2.8.5",
"express": "^4.21.0", "express": "^4.21.0",
"fs": "^0.0.1-security", "fs": "^0.0.1-security",
"mpd-parser": "^1.3.0",
"path": "^0.12.7", "path": "^0.12.7",
"tar": "^7.4.3", "tar": "^7.4.3",
"unzipper": "^0.12.3" "unzipper": "^0.12.3"