diff --git a/index.js b/index.js index e22c6c3..6f3d4fd 100644 --- a/index.js +++ b/index.js @@ -6,7 +6,7 @@ const path = require('path'); const cors = require('cors'); const EventEmitter = require('events'); const axios = require('axios'); -var mpdParser = require('mpd-parser'); +const mpdParser = require('mpd-parser'); const softwareService = require('./services/softwares'); const BASE_PATH = process.env.DATA_PATH || `./data`; @@ -86,7 +86,12 @@ const runProgressCommand = (command) => { return { executeCommand, emitter }; }; -// Route pour démarrer le processus +app.use((req, res, next) => { + const timestamp = new Date().toISOString(); + console.log(`[${timestamp}] ${req.method} ${req.url} - ${req.ip}`); + next(); +}); + app.post('/start-process', async (req, res, next) => { try { const { mp4Filename, mpdUrl, keys, wantedResolution, wantedAudioTracks, wantedSubtitles } = req.body; @@ -106,7 +111,6 @@ app.post('/start-process', async (req, res, 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) { @@ -118,6 +122,126 @@ app.get('/job-status/:jobId', async (req, res) => { } }); +app.delete('/job/:jobId', async (req, res) => { + try { + const job = await videoQueue.getJob(req.params.jobId); + + if (job === null) { + return res.status(404).json({ error: 'Job not found' }); + } + + const state = await job.getState(); + + if (state === 'active' || state === 'waiting') { + const { mp4Filename } = job.data; + const workdir = path.join(TMP_PATH, mp4Filename); + + if (fs.existsSync(workdir)) { + await runCommand(`rm -rf ${workdir}`); + console.log(`Cleaned up temp files for cancelled job: ${workdir}`); + } + } + + await job.remove(); + res.json({ + message: 'Job deleted successfully', + jobId: req.params.jobId, + previousState: state + }); + + } catch (error) { + console.error('Error deleting job:', error); + res.status(500).json({ error: error.message || 'Failed to delete job' }); + } +}); + +app.delete('/jobs/completed', async (req, res) => { + try { + const completedJobs = await videoQueue.getJobs(['completed', 'failed'], 0, -1); + + if (completedJobs.length === 0) { + return res.json({ + message: 'No completed jobs to delete', + deletedCount: 0 + }); + } + + const deletePromises = completedJobs.map(job => job.remove()); + await Promise.all(deletePromises); + + res.json({ + message: `Successfully deleted ${completedJobs.length} completed jobs`, + deletedCount: completedJobs.length + }); + + } catch (error) { + console.error('Error deleting completed jobs:', error); + res.status(500).json({ error: error.message || 'Failed to delete completed jobs' }); + } +}); + +app.delete('/jobs/all', async (req, res) => { + try { + const allJobs = await videoQueue.getJobs(['waiting', 'active', 'completed', 'failed', 'delayed'], 0, -1); + + if (allJobs.length === 0) { + return res.json({ + message: 'Queue is already empty', + deletedCount: 0 + }); + } + + let cleanedDirs = 0; + + for (const job of allJobs) { + const state = await job.getState(); + if (state === 'active' || state === 'waiting') { + const { mp4Filename } = job.data; + const workdir = path.join(TMP_PATH, mp4Filename); + + if (fs.existsSync(workdir)) { + await runCommand(`rm -rf ${workdir}`); + cleanedDirs++; + } + } + } + + const deletePromises = allJobs.map(job => job.remove()); + await Promise.all(deletePromises); + + res.json({ + message: `Successfully deleted all ${allJobs.length} jobs`, + deletedCount: allJobs.length, + cleanedTempDirs: cleanedDirs + }); + + } catch (error) { + console.error('Error deleting all jobs:', error); + res.status(500).json({ error: error.message || 'Failed to delete all jobs' }); + } +}); + +app.get('/jobs/stats', async (req, res) => { + try { + const waiting = await videoQueue.getWaiting(); + const active = await videoQueue.getActive(); + const completed = await videoQueue.getCompleted(); + const failed = await videoQueue.getFailed(); + + res.json({ + waiting: waiting.length, + active: active.length, + completed: completed.length, + failed: failed.length, + total: waiting.length + active.length + completed.length + failed.length + }); + + } catch (error) { + console.error('Error getting queue stats:', error); + res.status(500).json({ error: error.message || 'Failed to get queue stats' }); + } +}); + app.get('/jobs-status', async (req, res) => { const jobs = await videoQueue.getJobs(); res.json(await Promise.all(jobs.splice(0, 25).map(async job => ({ @@ -248,6 +372,10 @@ app.post('/processMPD', async (req, res, next) => { } }); +app.use((err, req, res, next) => { + res.status(500).json({ error: err.message || err.toString() || 'An error occured' }); +}) + // Processus de la file d'attente videoQueue.process((job) => { return new Promise(async (resolve, reject) => { diff --git a/services/softwares.js b/services/softwares.js index 58c408f..a90b941 100644 --- a/services/softwares.js +++ b/services/softwares.js @@ -186,30 +186,72 @@ const processUpdate = async (data) => { try { let downloadURL = data.binType === 'downloader' ? data.details.remoteInfos.browser_download_url : data.details.remoteInfos.downloadUrl; let filename = data.binType === 'downloader' ? data.details.remoteInfos.name : data.details.remoteInfos.filename; - fs.mkdirSync(tmpUpdatePath); + fs.mkdirSync(tmpUpdatePath, { recursive: true }); const zipDest = `${tmpUpdatePath}/${filename}`; console.log('Will download file from ' + downloadURL); await downloadFile(downloadURL, zipDest); console.log('File downloaded to ' + zipDest) console.log('Will decompress downloaded file'); const unzippedFolderDest = `${tmpUpdatePath}/${data.binType}_tmp` - fs.mkdirSync(unzippedFolderDest); + fs.mkdirSync(unzippedFolderDest, { recursive: true }); await extractFile(zipDest, unzippedFolderDest); console.log('Unzipped !'); - const uncompressedFoldersS = fs.readdirSync(unzippedFolderDest); - console.log(uncompressedFoldersS) - if (uncompressedFoldersS.length !== 1) - throw new Error('Unable to retrieve decompressed folder'); - const uncompressedFolders = fs.readdirSync(`${unzippedFolderDest}/${uncompressedFoldersS[0]}`); - if (uncompressedFolders.length === 0) - throw new Error('Unable to retrieve archive content'); - if (data.binType === 'downloader') - fs.renameSync(`${unzippedFolderDest}/${uncompressedFoldersS[0]}/${uncompressedFolders[0]}`, data.details.localInfos.path); - else if (data.binType === 'mp4decrypt') - fs.renameSync(`${unzippedFolderDest}/${uncompressedFoldersS[0]}/bin/mp4decrypt`, data.details.localInfos.path); + + const extractedItems = fs.readdirSync(unzippedFolderDest); + console.log('Extracted items:', extractedItems); + + if (extractedItems.length === 0) { + throw new Error('No files found after extraction'); + } + + if (data.binType === 'downloader') { + let binaryPath = null; + + for (const item of extractedItems) { + const itemPath = `${unzippedFolderDest}/${item}`; + const stat = fs.statSync(itemPath); + + if (stat.isFile() && (item === 'N_m3u8DL-RE' || item.includes('N_m3u8DL-RE'))) { + binaryPath = itemPath; + break; + } else if (stat.isDirectory()) { + const subItems = fs.readdirSync(itemPath); + const binary = subItems.find(subItem => + subItem === 'N_m3u8DL-RE' || subItem.includes('N_m3u8DL-RE') + ); + if (binary) { + binaryPath = `${itemPath}/${binary}`; + break; + } + } + } + + if (!binaryPath) { + throw new Error('N_m3u8DL-RE binary not found in archive'); + } + + fs.renameSync(binaryPath, data.details.localInfos.path); + + } else if (data.binType === 'mp4decrypt') { + if (extractedItems.length !== 1) { + throw new Error('Unable to retrieve decompressed folder for mp4decrypt'); + } + + const mainFolder = `${unzippedFolderDest}/${extractedItems[0]}`; + const mp4decryptPath = `${mainFolder}/bin/mp4decrypt`; + + if (!fs.existsSync(mp4decryptPath)) { + throw new Error('mp4decrypt binary not found at expected path: bin/mp4decrypt'); + } + + fs.renameSync(mp4decryptPath, data.details.localInfos.path); + } + fs.chmodSync(data.details.localInfos.path, 0o755); writeBinVersion(`${BIN_PATH}/.${data.binType}.version`, data.details.remoteInfos[data.binType === 'downloader' ? 'id' : 'version']) + } catch(e) { + console.error('Error during update process:', e); throw e; } finally { fs.rmSync(tmpUpdatePath, { recursive: true, force: true });