diff --git a/index.js b/index.js index 0f14ccf..19cbb74 100644 --- a/index.js +++ b/index.js @@ -258,9 +258,50 @@ app.get('/jobs-status', async (req, res) => { }); app.get('/download/:filename', async (req, res) => { - const { filename } = req.params; - const file = path.join(process.cwd(), filename); - res.download(file); + try { + const { filename } = req.params; + + if (filename.includes('..') || filename.includes('/') || filename.includes('\\')) { + return res.status(400).json({ error: 'Invalid filename' }); + } + + const filePath = path.join(OUTPUT_PATH, filename); + + if (!fs.existsSync(filePath)) { + return res.status(404).json({ error: 'File not found' }); + } + + const stat = fs.statSync(filePath); + if (!stat.isFile()) { + return res.status(400).json({ error: 'Not a file' }); + } + + const allowedExtensions = ['.mp4', '.mkv', '.srt', '.m4a']; + const fileExt = path.extname(filename).toLowerCase(); + + if (!allowedExtensions.includes(fileExt)) { + return res.status(403).json({ error: 'File type not allowed' }); + } + + console.log(`📥 Download requested: ${filename} (${stat.size} bytes)`); + + res.download(filePath, filename, (err) => { + if (err) { + console.error('Download error:', err); + if (!res.headersSent) { + res.status(500).json({ error: 'Download failed' }); + } + } else { + console.log(`✅ Download completed: ${filename}`); + } + }); + + } catch (error) { + console.error('Download route error:', error); + if (!res.headersSent) { + res.status(500).json({ error: 'Internal server error' }); + } + } }); app.get('/hello', async (req, res, next) => { @@ -409,12 +450,90 @@ const safeMove = async (source, destination) => { } }; +const remuxToMKV = async (options) => { + const { mp4FilePath, outputPath, filename, wantedAudioTracks, wantedSubtitles, videoInfo } = options; + + try { + console.log('🎬 Starting MKV remux...'); + + const mkvFilePath = `${outputPath}/${filename}.mkv`; + + let mkvCommand = `mkvmerge -o "${mkvFilePath}" --verbose`; + + mkvCommand += ` --title "${filename}"`; + + const resolution = videoInfo?.resolution || { width: 'Unknown', height: 'Unknown' }; + mkvCommand += ` --language 0:und`; + mkvCommand += ` --track-name "0:Video ${resolution.width}x${resolution.height}"`; + mkvCommand += ` "${mp4FilePath}"`; + + for (let i = 0; i < wantedAudioTracks.length; i++) { + const audioTrack = wantedAudioTracks[i]; + const codec = audioTrack.attributes.CODECS?.split('.')[0] || 'Unknown'; + const bitrate = Math.round(audioTrack.attributes.BANDWIDTH / 1000); + + const trackIndex = i + 1; + + mkvCommand += ` --language ${trackIndex}:${audioTrack.language}`; + mkvCommand += ` --track-name "${trackIndex}:${audioTrack.language.toUpperCase()} ${codec} ${bitrate}kbps"`; + + mkvCommand += ` --default-track ${trackIndex}:${i === 0 ? 'yes' : 'no'}`; + } + + const srtFiles = fs.readdirSync(outputPath).filter(file => + file.startsWith(filename) && file.endsWith('.srt') + ); + + for (let i = 0; i < srtFiles.length; i++) { + const srtFile = srtFiles[i]; + const srtPath = `${outputPath}/${srtFile}`; + + const srtIndex = i; + const correspondingSubtitle = wantedSubtitles[srtIndex]; + + if (correspondingSubtitle) { + const language = correspondingSubtitle.language; + + const isForced = srtFile.toLowerCase().includes('forced') || srtFile.toLowerCase().includes('sdh') === false; + const trackName = isForced ? `${language.toUpperCase()} Forced` : `${language.toUpperCase()}`; + + mkvCommand += ` --language 0:${language}`; + mkvCommand += ` --track-name "0:${trackName}"`; + + if (isForced) { + mkvCommand += ` --forced-track 0:yes`; + } + + const defaultSub = (i === 0 && wantedAudioTracks.length > 0 && language === wantedAudioTracks[0].language) ? 'yes' : 'no'; + mkvCommand += ` --default-track 0:${defaultSub}`; + + mkvCommand += ` "${srtPath}"`; + } + } + + console.log('🔧 MKV Command:', mkvCommand); + + await runCommand(mkvCommand); + + console.log(`✅ MKV remux completed: ${mkvFilePath}`); + + fs.unlinkSync(mp4FilePath); + srtFiles.forEach(srt => fs.unlinkSync(`${outputPath}/${srt}`)); + + return mkvFilePath; + + } catch (error) { + console.error('❌ MKV remux failed:', error); + throw new Error(`MKV remux failed: ${error.message}`); + } +}; + // Processus de la file d'attente videoQueue.process((job) => { return new Promise(async (resolve, reject) => { try { console.log('Will launch job') - const { mp4Filename, mpdUrl, keys, wantedResolution, wantedAudioTracks, wantedSubtitles } = job.data; + const { mp4Filename, mpdUrl, keys, wantedResolution, wantedAudioTracks, wantedSubtitles, wantedRemux } = job.data; const downloaderPath = softwareService.getLocalBinFileInfo('downloader').path; const mp4decryptPath = softwareService.getLocalBinFileInfo('mp4decrypt').path; @@ -492,6 +611,27 @@ videoQueue.process((job) => { job.progress(90); + if (wantedRemux) { + console.log('🎬 Starting optional MKV remux...'); + + try { + await remuxToMKV({ + mp4FilePath: `${finalPath}/${mp4FilenameWithExt}`, + outputPath: finalPath, + filename: mp4Filename, + wantedAudioTracks, + wantedSubtitles, + videoInfo: wantedResolution + }); + + job.progress(95); + console.log('✅ MKV remux completed successfully'); + + } catch (remuxError) { + console.error('⚠️ MKV remux failed, keeping MP4:', remuxError); + } + } + // Renommage des fichiers SRT const subFiles = fs.readdirSync(workdir); let counter = 1;