diff --git a/index.js b/index.js index c069f27..ca5332f 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,6 @@ const express = require('express'); const Queue = require('bull'); -const { spawn } = require('child_process'); +const { spawn, exec } = require('child_process'); const fs = require('fs'); const path = require('path'); const cors = require('cors'); @@ -10,6 +10,10 @@ const mpdParser = require('mpd-parser'); const softwareService = require('./services/softwares'); const ffprobe = require('ffprobe'); const ffprobeStatic = require('ffprobe-static'); +const { stringify } = require('querystring'); +const xml2js = require('xml2js'); +const { promisify } = require('util'); +const execAsync = promisify(exec); const BASE_PATH = process.env.DATA_PATH || `./data`; const OUTPUT_PATH = process.env.OUTPUT_PATH || `${BASE_PATH}/output`; @@ -380,65 +384,457 @@ const formatDuration = (seconds) => { } 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 }); + // Télécharger le manifest d'abord + await downloadMPDManifest(mpdUrl, `./data/tmp/manifest.mpd`); + + // 1. Parser le XML pour récupérer les KID + const mpdContent = fs.readFileSync('./data/tmp/manifest.mpd', 'utf8'); + const parser = new xml2js.Parser({ explicitArray: false, mergeAttrs: true }); + const parsedXML = await parser.parseStringPromise(mpdContent); + + // Extraire les KID du XML par contentType et bandwidth + const kidMapping = {}; + if (parsedXML.MPD?.Period) { + const periods = Array.isArray(parsedXML.MPD.Period) ? parsedXML.MPD.Period : [parsedXML.MPD.Period]; + + periods.forEach(period => { + if (period.AdaptationSet) { + const adaptationSets = Array.isArray(period.AdaptationSet) ? period.AdaptationSet : [period.AdaptationSet]; + + adaptationSets.forEach(adaptSet => { + let defaultKID = null; + + // Chercher ContentProtection + if (adaptSet.ContentProtection) { + if (Array.isArray(adaptSet.ContentProtection)) { + defaultKID = adaptSet.ContentProtection[0]['cenc:default_KID']; + } else { + defaultKID = adaptSet.ContentProtection['cenc:default_KID']; + } + } + + if (defaultKID && adaptSet.Representation) { + const representations = Array.isArray(adaptSet.Representation) ? adaptSet.Representation : [adaptSet.Representation]; + + representations.forEach(rep => { + const key = `${adaptSet.contentType}_${rep.bandwidth}`; + kidMapping[key] = defaultKID.replace(/-/g, ''); + }); + } + }); + } + }); + } + + + // 2. Parser avec N_m3u8DL-RE pour avoir les bonnes données + const baseUrl = mpdUrl.substring(0, mpdUrl.lastIndexOf('/') + 1); + const downloaderPath = softwareService.getLocalBinFileInfo('downloader').path; + const parseCommand = `${downloaderPath} "./data/tmp/manifest.mpd" --base-url "${baseUrl}" --skip-download -H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:142.0) Gecko/20100101 Firefox/142.0"`; + + let stdout; + try { + const result = await execAsync(parseCommand); + stdout = result.stdout; + } catch (error) { + if (error.stdout) { + stdout = error.stdout; + } else { + throw error; + } + } + + // 3. Parser N_m3u8DL-RE et associer les KID + const isValidDuration = (durationStr) => { + const timeMatch = durationStr.match(/~(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?/); + if (!timeMatch) return false; + + const hours = parseInt(timeMatch[1]) || 0; + const minutes = parseInt(timeMatch[2]) || 0; + const totalMinutes = hours * 60 + minutes; + + return totalMinutes >= 10 && totalMinutes <= 240; + }; const obj = { audioTracks: [], videoTracks: [], subtitles: [] }; - const toParse = [{ - rootProp: 'AUDIO', - subProp: 'audio', - targetProp: 'audioTracks' - }, { - rootProp: 'SUBTITLES', - subProp: 'subs', - targetProp: 'subtitles' - }]; - - toParse.forEach(({ rootProp, subProp, targetProp }) => { - try { - 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 - }); - } + + const lines = stdout.split('\n'); + lines.forEach(line => { + if (line.includes('INFO : Vid')) { + if (!/\d+x\d+/.test(line)) { + console.log('Skipping non-video line:', line); + return; + } + + const parts = line.split(' | '); + if (parts.length >= 7) { + const encrypted = parts[0].includes('*CENC'); + const resolution = parts[0].match(/(\d+x\d+)/)[1]; + const [width, height] = resolution.split('x').map(Number); + const kbps = parseInt(parts[1].match(/(\d+) Kbps/)[1]); + const videoId = parts[2].trim(); + const fps = parseFloat(parts[3]); + const codec = parts[4].trim(); + const duration = parts[7].trim(); + + if (!isValidDuration(duration)) { + console.log(`Skipping video track with suspicious duration (${duration}):`, line); + return; + } + + const bandwidth = kbps * 1000; + const mappingKey1 = `video_${bandwidth}`; + const mappingKey2 = videoId.startsWith('video=') ? videoId : `video=${bandwidth}`; + const defaultKID = kidMapping[mappingKey1] || kidMapping[mappingKey2] || null; + + obj.videoTracks.push({ + id: videoId, + name: resolution, + resolution: { width, height }, + bandwidth, + fps, + codec, + encrypted, + formatedDuration: duration, + defaultKID + }); + } + } else if (line.includes('INFO : Aud')) { + const audioMatch = line.match(/Aud (\*CENC )?(.+?) \| (\d+) Kbps \| ([^|]+) \| ([^|]+) \| ([^|]+) \| \d+ Segments? \| [^|]+ \| (~.+)$/); + if (audioMatch) { + const [, encrypted, audioId, kbps, codec, lang, channels, duration] = audioMatch; + + if (!isValidDuration(duration)) return; + + const bandwidth = parseInt(kbps) * 1000; + + // Chercher le KID correspondant + const kidKey = `audio_${bandwidth}`; + const defaultKID = kidMapping[kidKey] || null; + + obj.audioTracks.push({ + id: audioId.trim(), + name: audioId.trim(), + language: lang.trim(), + bandwidth, + codec: codec.trim(), + channels: channels.trim(), + encrypted: !!encrypted, + formatedDuration: duration.trim(), + defaultKID + }); + } + } else if (line.includes('INFO : Sub')) { + const parts = line.split(' | '); + if (parts.length >= 6) { + const subId = parts[0].replace(/.*INFO : Sub /, '').trim(); + const lang = parts[1].trim(); + const codec = parts[2].trim(); + const duration = parts[5].trim(); + + if (!isValidDuration(duration)) return; + + const defaultKID = kidMapping[subId] || kidMapping[`text_${subId}`] || null; + + obj.subtitles.push({ + id: subId, + name: subId, + language: lang, + codec, + encrypted: false, + formatedDuration: duration, + defaultKID + }); } - } catch(e) { - console.log(`No ${targetProp} found in manifest`); } }); - for (let i = 0; i < parsedManifest.playlists.length; i++) { - obj.videoTracks.push({ - id: parsedManifest.playlists?.[i]?.attributes?.NAME, - 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, - defaultKID: parsedManifest.playlists?.[i]?.contentProtection?.mp4protection?.attributes['cenc:default_KID']?.replaceAll('-', '') || null, - fps: parsedManifest.playlists?.[i]?.attributes?.['FRAME-RATE'], - resolution: { - width: parsedManifest.playlists?.[i]?.attributes?.RESOLUTION?.width, - height: parsedManifest.playlists?.[i]?.attributes?.RESOLUTION?.height - }, - duration: parsedManifest.playlists?.[i]?.targetDuration, - formatedDuration: formatDuration(parsedManifest.playlists?.[i]?.targetDuration || 0) - }); - } - obj.videoTracks = obj.videoTracks.sort((a, b) => b.resolution.width - a.resolution.width); + + // Déduplication et tri + obj.videoTracks = obj.videoTracks + .filter((track, index, self) => index === self.findIndex(t => t.id === track.id)) + .sort((a, b) => b.resolution.width - a.resolution.width); + + obj.audioTracks = obj.audioTracks + .filter((track, index, self) => index === self.findIndex(t => t.id === track.id)) + .sort((a, b) => b.bandwidth - a.bandwidth); + + console.log(`Parsed ${obj.videoTracks.length} video tracks, ${obj.audioTracks.length} audio tracks`); + return obj; }; +// const parseMPDStream = async (mpdUrl) => { +// const mpdResponse = await axios({ +// url: mpdUrl, +// method: 'GET', +// responseType: 'text' +// }); + +// const parser = new xml2js.Parser({ explicitArray: false, mergeAttrs: true }); +// const parsedXML = await parser.parseStringPromise(mpdResponse.data); + +// const obj = { +// audioTracks: [], +// videoTracks: [], +// subtitles: [] +// }; + +// // Fonction pour parser la durée PT format vers secondes +// const parseDuration = (duration) => { +// if (!duration) return 0; +// // PT34M11.34S -> 34*60 + 11.34 = 2051.34 secondes +// const match = duration.match(/PT(?:(\d+)H)?(?:(\d+)M)?(?:([\d.]+)S)?/); +// if (!match) return 0; +// const hours = parseInt(match[1]) || 0; +// const minutes = parseInt(match[2]) || 0; +// const seconds = parseFloat(match[3]) || 0; +// return hours * 3600 + minutes * 60 + seconds; +// }; + +// // Fonction pour filtrer les periods avec contenu principal (> 10 minutes) +// const isMainContent = (duration) => { +// const durationInSeconds = parseDuration(duration); +// return durationInSeconds > 600; // Plus de 10 minutes +// }; + +// if (parsedXML.MPD && parsedXML.MPD.Period) { +// const periods = Array.isArray(parsedXML.MPD.Period) ? parsedXML.MPD.Period : [parsedXML.MPD.Period]; + +// // Filtrer seulement les periods de contenu principal +// const mainPeriods = periods.filter(period => isMainContent(period.duration)); + +// console.log(`Found ${periods.length} total periods, ${mainPeriods.length} main content periods`); + +// mainPeriods.forEach((period, periodIndex) => { +// console.log(`Processing period: ${period.duration} (${parseDuration(period.duration)}s)`); + +// if (period.AdaptationSet) { +// const adaptationSets = Array.isArray(period.AdaptationSet) ? period.AdaptationSet : [period.AdaptationSet]; + +// adaptationSets.forEach(adaptSet => { +// const contentType = adaptSet.contentType; +// let defaultKID = null; +// if (adaptSet.ContentProtection) { +// if (Array.isArray(adaptSet.ContentProtection)) { +// defaultKID = adaptSet.ContentProtection[0]['cenc:default_KID']; +// } else { +// defaultKID = adaptSet.ContentProtection['cenc:default_KID']; +// } + +// if (defaultKID) { +// defaultKID = defaultKID.replace(/-/g, ''); +// } +// } +// if (adaptSet.Representation) { +// const representations = Array.isArray(adaptSet.Representation) ? adaptSet.Representation : [adaptSet.Representation]; + +// representations.forEach(rep => { +// const baseTrack = { +// id: rep.id, +// codec: rep.codecs, +// bandwidth: parseInt(rep.bandwidth), +// defaultKID: defaultKID ? defaultKID.replace(/-/g, '') : null, +// duration: parseDuration(period.duration), +// formatedDuration: formatDuration(parseDuration(period.duration)) +// }; + +// if (contentType === 'video') { +// obj.videoTracks.push({ +// ...baseTrack, +// name: `${rep.width}x${rep.height}`, +// fps: parseFloat(rep.frameRate), +// resolution: { +// width: parseInt(rep.width), +// height: parseInt(rep.height) +// } +// }); +// } else if (contentType === 'audio') { +// obj.audioTracks.push({ +// ...baseTrack, +// name: rep.id || `${adaptSet.lang || 'unknown'}_${rep.bandwidth}`, +// language: adaptSet.lang || 'unknown', +// channels: rep.AudioChannelConfiguration ? rep.AudioChannelConfiguration.value + 'CH' : '2CH', +// audioSamplingRate: parseInt(rep.audioSamplingRate) +// }); +// } +// }); +// } +// }); +// } +// }); +// } + +// obj.videoTracks = obj.videoTracks.sort((a, b) => b.resolution.width - a.resolution.width); +// obj.audioTracks = obj.audioTracks.sort((a, b) => b.bandwidth - a.bandwidth); + +// obj.videoTracks = obj.videoTracks.filter((track, index, self) => +// index === self.findIndex(t => t.id === track.id && t.bandwidth === track.bandwidth) +// ); +// obj.audioTracks = obj.audioTracks.filter((track, index, self) => +// index === self.findIndex(t => t.id === track.id && t.bandwidth === track.bandwidth) +// ); + +// return obj; +// }; + +// 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: [] +// }; + +// // Ajoutez ça temporairement dans votre fonction parseMPDStream après le parsing +// console.log('=== DEBUG STRUCTURE ==='); + +// // Debug premier élément vidéo +// if (parsedManifest.playlists && parsedManifest.playlists.length > 0) { +// console.log('\n🎥 FIRST VIDEO TRACK:'); +// console.log(JSON.stringify(parsedManifest.playlists[0], null, 2)); +// } + +// // // Debug premier élément audio +// // try { +// // const audioGroups = parsedManifest?.mediaGroups?.AUDIO?.audio; +// // if (audioGroups) { +// // const firstAudioKey = Object.keys(audioGroups)[0]; +// // console.log('\n🔊 FIRST AUDIO TRACK:'); +// // console.log(`Key: "${firstAudioKey}"`); +// // console.log('Group:', JSON.stringify(audioGroups[firstAudioKey], null, 2)); +// // if (audioGroups[firstAudioKey].playlists[0]) { +// // console.log('First playlist:', JSON.stringify(audioGroups[firstAudioKey].playlists[0], null, 2)); +// // } +// // } +// // } catch(e) { +// // console.log('No audio debug possible:', e.message); +// // } + +// // // Debug premier élément subtitle +// // try { +// // const subsGroups = parsedManifest?.mediaGroups?.SUBTITLES?.subs; +// // if (subsGroups) { +// // const firstSubKey = Object.keys(subsGroups)[0]; +// // console.log('\n📝 FIRST SUBTITLE TRACK:'); +// // console.log(`Key: "${firstSubKey}"`); +// // console.log('Group:', JSON.stringify(subsGroups[firstSubKey], null, 2)); +// // if (subsGroups[firstSubKey].playlists[0]) { +// // console.log('First playlist:', JSON.stringify(subsGroups[firstSubKey].playlists[0], null, 2)); +// // } +// // } +// // } catch(e) { +// // console.log('No subtitle debug possible:', e.message); +// // } + +// console.log('=== END DEBUG ===\n'); +// // Fonction pour filtrer les durées anormales (< 10min ou > 4h) +// const isValidDuration = (duration) => { +// if (!duration) return true; // Garder si pas de durée spécifiée +// const durationInSeconds = duration; // Conversion en secondes si nécessaire +// return durationInSeconds >= 600 && durationInSeconds <= 14400; // Entre 10min et 4h +// }; + +// // Traitement des groupes de média (AUDIO et SUBTITLES) +// const toParse = [{ +// rootProp: 'AUDIO', +// subProp: 'audio', +// targetProp: 'audioTracks' +// }, { +// rootProp: 'SUBTITLES', +// subProp: 'subs', +// targetProp: 'subtitles' +// }]; + +// toParse.forEach(({ rootProp, subProp, targetProp }) => { +// try { +// for (const [key, value] of Object.entries(parsedManifest?.mediaGroups?.[rootProp]?.[subProp])) { +// for (let i = 0; i < value.playlists.length; i++) { +// const playlist = value.playlists[i]; + +// // Filtrer les tracks avec des durées anormales +// if (!isValidDuration(playlist.targetDuration)) { +// console.log(`🚫 Filtered out ${targetProp} "${key}" with suspicious duration: ${playlist.targetDuration}min`); +// continue; +// } + +// if (targetProp === 'audioTracks') { +// obj.audioTracks.push({ +// id: playlist?.attributes?.NAME || key, +// name: key, +// language: value.language || 'unknown', +// codec: playlist?.attributes?.CODECS, +// bandwidth: playlist?.attributes?.BANDWIDTH, +// channels: playlist?.attributes?.CHANNELS || '2CH', +// defaultKID: playlist?.contentProtection?.mp4protection?.attributes['cenc:default_KID']?.replaceAll('-', '') || null, +// duration: playlist?.targetDuration, +// formatedDuration: formatDuration(playlist?.targetDuration || 0), +// attributes: playlist.attributes +// }); +// } else if (targetProp === 'subtitles') { +// obj.subtitles.push({ +// id: playlist?.attributes?.NAME || key, +// name: key, +// language: value.language || 'unknown', +// codec: playlist?.attributes?.CODECS, +// format: playlist?.attributes?.FORMAT || 'UNKNOWN', +// defaultKID: playlist?.contentProtection?.mp4protection?.attributes['cenc:default_KID']?.replaceAll('-', '') || null, +// duration: playlist?.targetDuration, +// formatedDuration: formatDuration(playlist?.targetDuration || 0), +// attributes: playlist.attributes +// }); +// } +// } +// } +// } catch(e) { +// console.log(`No ${targetProp} found in manifest`); +// } +// }); + +// // Traitement des tracks vidéo +// for (let i = 0; i < parsedManifest.playlists.length; i++) { +// const playlist = parsedManifest.playlists[i]; + +// // Filtrer les tracks vidéo avec des durées anormales +// if (!isValidDuration(playlist.targetDuration)) { +// console.log(`🚫 Filtered out video track with suspicious duration: ${playlist.targetDuration}min`); +// continue; +// } + +// obj.videoTracks.push({ +// id: playlist?.attributes?.NAME, +// name: `${playlist?.attributes?.RESOLUTION?.width || 'N/C'}x${playlist?.attributes?.RESOLUTION?.height || 'N/C'}`, +// codec: playlist?.attributes?.CODECS, +// bandwidth: playlist?.attributes?.BANDWIDTH, +// defaultKID: playlist?.contentProtection?.mp4protection?.attributes['cenc:default_KID']?.replaceAll('-', '') || null, +// fps: playlist?.attributes?.['FRAME-RATE'], +// resolution: { +// width: playlist?.attributes?.RESOLUTION?.width, +// height: playlist?.attributes?.RESOLUTION?.height +// }, +// duration: playlist?.targetDuration, +// formatedDuration: formatDuration(playlist?.targetDuration || 0), +// attributes: playlist.attributes +// }); +// } + +// // Tri des tracks par qualité/priorité +// obj.videoTracks = obj.videoTracks.sort((a, b) => b.resolution.width - a.resolution.width); +// obj.audioTracks = obj.audioTracks.sort((a, b) => (b.bandwidth || 0) - (a.bandwidth || 0)); + +// return obj; +// }; + app.post('/processMPD', async (req, res, next) => { try { res.json(await parseMPDStream(req.body.mpdUrl)); @@ -889,8 +1285,8 @@ const remuxToMKV = async (options) => { function extractAudioTrackInfo(audioTrack) { const language = audioTrack.language || 'und'; - const codec = audioTrack.attributes?.CODECS?.split('.')[0] || 'unknown'; - const bitrate = Math.round((audioTrack.attributes?.BANDWIDTH || 0) / 1000); + const codec = audioTrack.codec?.split('.')[0] || 'unknown'; + const bitrate = Math.round((audioTrack.bandwidth || 0) / 1000); return { language, codec, bitrate }; } @@ -1158,6 +1554,29 @@ async function prepareDirectories(config) { } } +const downloadMPDManifest = async (url, filepath) => { + try { + const response = await axios({ + method: 'GET', + url, + headers: { + 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:142.0) Gecko/20100101 Firefox/142.0', + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', + 'Accept-Language': 'en-US,en;q=0.5', + 'Accept-Encoding': 'gzip, deflate, br, zstd' + }, + decompress: true + }); + + fs.writeFileSync(filepath, response.data); + + return response.data; + } catch (error) { + console.error('Erreur lors du téléchargement:', error.message); + throw error; + } +}; + async function downloadEncryptedFiles(job, config, { mpdUrl, wantedResolution, wantedAudioTracks, wantedSubtitles, mp4Filename }) { const { workdir, downloaderPath } = config; const filesExist = await checkFilesExistance('encrypted', workdir); @@ -1166,57 +1585,45 @@ async function downloadEncryptedFiles(job, config, { mpdUrl, wantedResolution, w console.log('📁 Encrypted files already exist, bypassing download...'); return; } - console.log('⬇️ Encrypted files not found, downloading...'); - // Construction des paramètres de téléchargement const downloadParams = buildDownloadParameters(wantedAudioTracks, wantedSubtitles); const objectsToDownload = 1 + wantedAudioTracks.length + wantedSubtitles.length; job.progress(10); - - const command = `${downloaderPath} "${mpdUrl}" --save-dir ${workdir} --save-name ${mp4Filename}_encrypted --select-video id="${wantedResolution.id}" ${downloadParams.audioPart} ${downloadParams.subPart}`; + await downloadMPDManifest(mpdUrl, `${workdir}/manifest.mpd`); + + const baseUrl = mpdUrl.substring(0, mpdUrl.lastIndexOf('/') + 1); + + const command = `${downloaderPath} "${workdir}/manifest.mpd" --base-url "${baseUrl}" --save-dir "${workdir}" --save-name "${mp4Filename}_encrypted" --select-video id="${wantedResolution.id}" ${downloadParams.audioPart} ${downloadParams.subPart} -H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:142.0) Gecko/20100101 Firefox/142.0"`; const { executeCommand, emitter } = runProgressCommand(command, true); - // Gestion du progress avec meilleure lisibilité let objectsDownloaded = -1; let previousPercentage = -1; - emitter.on('percentage', (percentage) => { if (percentage < previousPercentage) { objectsDownloaded++; } previousPercentage = percentage; - const subPercMax = 50 / objectsToDownload; const newProgress = Math.round((10 + (objectsDownloaded * subPercMax)) + (percentage * subPercMax / 100)); job.progress(newProgress); }); - await executeCommand; } function buildDownloadParameters(wantedAudioTracks, wantedSubtitles) { - // Construire les paramètres bandwidth pour audio - const bwAudio = wantedAudioTracks.length === 1 - ? `:bwMin="${wantedAudioTracks.map(elem => Math.floor(elem.attributes.BANDWIDTH / 1000 - 1)).join('|')}":bwMax="${wantedAudioTracks.map(elem => Math.round(elem.attributes.BANDWIDTH / 1000 + 1)).join('|')}"` - : ''; - - // Construire les paramètres bandwidth pour sous-titres - const bwSubs = wantedSubtitles.length === 1 - ? `:bwMin="${wantedSubtitles.map(elem => Math.floor(elem.attributes.BANDWIDTH / 1000 - 1)).join('|')}":bwMax="${wantedSubtitles.map(elem => Math.round(elem.attributes.BANDWIDTH / 1000 + 1)).join('|')}"` - : ''; - - // Construire les parties de la commande - const subPart = wantedSubtitles.length > 0 - ? `--select-subtitle lang="${wantedSubtitles.map(elem => elem.language).join('|')}"${bwSubs}` - : '--drop-subtitle lang=".*"'; - + // Construire la sélection par ID pour audio const audioPart = wantedAudioTracks.length > 0 - ? `--select-audio lang="${wantedAudioTracks.map(elem => elem.language).join('|')}":codecs="${[...new Set(wantedAudioTracks.map(elem => elem.attributes.CODECS))].join('|')}":for=all${bwAudio}` - : '--drop-audio lang=".*"'; - + ? `--select-audio id="${wantedAudioTracks.map(elem => elem.id).join('|')}":for=all` + : '--drop-audio id=".*"'; + + // Construire la sélection par ID pour sous-titres + const subPart = wantedSubtitles.length > 0 + ? `--select-subtitle id="${wantedSubtitles.map(elem => elem.id).join('|')}":for=all` + : '--drop-subtitle id=".*"'; + return { audioPart, subPart }; } diff --git a/package.json b/package.json index 4e5e403..080d979 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "mpd-parser": "^1.3.1", "path": "^0.12.7", "tar": "^7.4.3", - "unzipper": "^0.12.3" + "unzipper": "^0.12.3", + "xml2js": "^0.6.2" } }