alot of fixes
This commit is contained in:
551
index.js
551
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}`);
|
||||
// Télécharger le manifest d'abord
|
||||
await downloadMPDManifest(mpdUrl, `./data/tmp/manifest.mpd`);
|
||||
|
||||
const parsedManifest = mpdParser.parse(mpdResponse.data , { mpdUrl, eventHandler });
|
||||
// 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,56 +1585,44 @@ 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);
|
||||
await downloadMPDManifest(mpdUrl, `${workdir}/manifest.mpd`);
|
||||
|
||||
const command = `${downloaderPath} "${mpdUrl}" --save-dir ${workdir} --save-name ${mp4Filename}_encrypted --select-video id="${wantedResolution.id}" ${downloadParams.audioPart} ${downloadParams.subPart}`;
|
||||
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 };
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user