319 lines
8.9 KiB
JavaScript
319 lines
8.9 KiB
JavaScript
const axios = require('axios');
|
|
const os = require('os');
|
|
const fs = require("fs");
|
|
const cheerio = require('cheerio');
|
|
const unzipper = require('unzipper');
|
|
const tar = require('tar');
|
|
|
|
|
|
const BASE_PATH = process.env.DATA_PATH || `./data`;
|
|
const BIN_PATH = `${BASE_PATH}/bin`;
|
|
|
|
const fetchAndParseHTML = async (url) => {
|
|
try {
|
|
const { data: html } = await axios.get(url);
|
|
|
|
const $ = cheerio.load(html);
|
|
|
|
const files = [];
|
|
|
|
$('a').each((index, element) => {
|
|
const fileName = $(element).text().trim();
|
|
const href = $(element).attr('href');
|
|
|
|
const nextElement = $(element).nextAll().filter((i, el) => $(el).text().trim() !== '-').first();
|
|
const details = nextElement.text().trim().split(/\s+/);
|
|
if (href && fileName !== "Parent Directory") {
|
|
files.push({
|
|
fileName: href,
|
|
href: `https://www.bok.net/Bento4/binaries/${href}`
|
|
});
|
|
}
|
|
});
|
|
|
|
return files;
|
|
} catch (error) {
|
|
console.error('Erreur lors de la récupération/parsing:', error);
|
|
}
|
|
}
|
|
|
|
const getLatestBentoBinary = async (platform) => {
|
|
try {
|
|
const last = (await fetchAndParseHTML('https://www.bok.net/Bento4/binaries/')).slice(-3);
|
|
|
|
const releases = last.map((elem) => {
|
|
let version = elem.fileName.split('.');
|
|
version = version?.[0]?.split('Bento4-SDK-')?.[1] || null;
|
|
|
|
return {
|
|
downloadUrl: elem.href,
|
|
filename: elem.fileName,
|
|
platform: elem.fileName.includes('macosx') ? 'darwin' : elem.fileName.includes('linux') ? 'linux' : 'win32',
|
|
version
|
|
}
|
|
});
|
|
return releases.find((elem) => elem.platform === platform);
|
|
} catch(e) {
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
const translateDownloaderArch = () => {
|
|
const platform = os.platform();
|
|
const arch = os.arch();
|
|
|
|
switch (platform) {
|
|
case 'linux':
|
|
if (arch === 'x64') {
|
|
return 'linux-x64';
|
|
} else if (arch === 'arm64') {
|
|
return 'linux-arm64';
|
|
}
|
|
break;
|
|
|
|
case 'darwin': // macOS
|
|
if (arch === 'x64') {
|
|
return 'osx-x64';
|
|
} else if (arch === 'arm64') {
|
|
return 'osx-arm64';
|
|
}
|
|
break;
|
|
|
|
case 'win32': // Windows
|
|
if (arch === 'x64') {
|
|
return 'win-x64';
|
|
} else if (arch === 'arm64') {
|
|
return 'win-arm64';
|
|
}
|
|
break;
|
|
|
|
default:
|
|
throw new Error(`OS or architecture unsuported : ${platform}, ${arch}`);
|
|
}
|
|
|
|
throw new Error(`Unsuported architecture for this platform ${platform} : ${arch}`);
|
|
};
|
|
|
|
const writeBinVersion = (versionFilePath, version) => {
|
|
const data = { version };
|
|
|
|
try {
|
|
fs.writeFileSync(versionFilePath, JSON.stringify(data, null, 2), 'utf8');
|
|
} catch (error) {
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
getBinVersion = (versionFilePath) => {
|
|
try {
|
|
const data = fs.readFileSync(versionFilePath, 'utf8');
|
|
const json = JSON.parse(data);
|
|
return json.version || null;
|
|
} catch (error) {
|
|
if (error.code === 'ENOENT') {
|
|
return null;
|
|
} else {
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
|
|
const getLocalBinFileInfo = (binType) => {
|
|
if (['downloader', 'mp4decrypt'].includes(binType)) {
|
|
const path = `${BIN_PATH}/${binType}`;
|
|
return {
|
|
path,
|
|
stat: fs.existsSync(path) ? fs.statSync(path) : null,
|
|
version: getBinVersion(`${BIN_PATH}/.${binType}.version`)
|
|
}
|
|
} else {
|
|
throw new Error(`Bad binType "${binType}" provided`);
|
|
}
|
|
}
|
|
|
|
const downloadFile = async (url, dest) => {
|
|
const writer = fs.createWriteStream(dest);
|
|
const response = await axios({
|
|
url,
|
|
method: 'GET',
|
|
responseType: 'stream',
|
|
});
|
|
response.data.pipe(writer);
|
|
return new Promise((resolve, reject) => {
|
|
writer.on('finish', resolve);
|
|
writer.on('error', reject);
|
|
});
|
|
}
|
|
|
|
const checkDownloaderUpdate = async () => {
|
|
try {
|
|
const localInfos = getLocalBinFileInfo('downloader');
|
|
const remoteInfos = await getLatestGithubReleaseAssetUrl('nilaoda', 'N_m3u8DL-RE', translateDownloaderArch());
|
|
return {
|
|
details: {
|
|
localInfos,
|
|
remoteInfos
|
|
},
|
|
newReleaseAvailable: remoteInfos ? localInfos?.version ? parseInt(localInfos.version) !== remoteInfos?.id : remoteInfos?.name || false : false,
|
|
filsIsPresent: localInfos?.stat ? true : false
|
|
}
|
|
} catch(e) {
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
const extractFile = (source, destination) => {
|
|
return new Promise((resolve, reject) => {
|
|
if (source.endsWith('.zip')) {
|
|
fs.createReadStream(source)
|
|
.pipe(unzipper.Extract({ path: destination }))
|
|
.on('close', resolve)
|
|
.on('error', reject);
|
|
} else if (source.endsWith('.tar.gz')) {
|
|
fs.createReadStream(source)
|
|
.pipe(tar.x({ C: destination }))
|
|
.on('close', resolve)
|
|
.on('error', reject);
|
|
} else {
|
|
reject(new Error('Unsupported file type'));
|
|
}
|
|
});
|
|
}
|
|
|
|
const processUpdate = async (data) => {
|
|
const tmpUpdatePath = `${BIN_PATH}/update/`;
|
|
|
|
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, { 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, { recursive: true });
|
|
await extractFile(zipDest, unzippedFolderDest);
|
|
console.log('Unzipped !');
|
|
|
|
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 });
|
|
}
|
|
}
|
|
|
|
const checkMp4decryptUpdate = async () => {
|
|
try {
|
|
const localInfos = getLocalBinFileInfo('mp4decrypt');
|
|
const remoteInfos = await getLatestBentoBinary(os.platform());
|
|
console.log(remoteInfos);
|
|
return {
|
|
details: {
|
|
localInfos,
|
|
remoteInfos
|
|
},
|
|
newReleaseAvailable: localInfos?.version ? localInfos.version !== remoteInfos?.version : remoteInfos?.filename || false,
|
|
filsIsPresent: localInfos?.stat ? true : false
|
|
}
|
|
} catch(e) {
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
const getLatestGithubReleaseAssetUrl = async (owner, repo, assetName) => {
|
|
try {
|
|
const response = await axios.get(`https://api.github.com/repos/${owner}/${repo}/releases/latest`, {
|
|
headers: { 'User-Agent': 'node.js' }
|
|
});
|
|
|
|
const release = response.data;
|
|
const asset = release.assets.find(a => a.name.includes(assetName));
|
|
|
|
if (asset) {
|
|
return asset;
|
|
} else {
|
|
throw new Error('Asset not found');
|
|
}
|
|
} catch (error) {
|
|
console.error('Erreur:', error.message);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
const init = async () => {
|
|
try {
|
|
if (!fs.existsSync(BIN_PATH)) {
|
|
fs.mkdirSync(BIN_PATH);
|
|
console.log('Creating bin path...')
|
|
}
|
|
} catch(e) {
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
init,
|
|
getLatestGithubReleaseAssetUrl,
|
|
translateDownloaderArch,
|
|
getLocalBinFileInfo,
|
|
checkDownloaderUpdate,
|
|
checkMp4decryptUpdate,
|
|
processUpdate
|
|
};
|