Add new binary update mechanism
This commit is contained in:
275
services/softwares.js
Normal file
275
services/softwares.js
Normal file
@@ -0,0 +1,275 @@
|
||||
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: localInfos?.version ? parseInt(localInfos.version) !== remoteInfos?.id : remoteInfos?.name || 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);
|
||||
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);
|
||||
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);
|
||||
writeBinVersion(`${BIN_PATH}/.${data.binType}.version`, data.details.remoteInfos[data.binType === 'downloader' ? 'id' : 'version'])
|
||||
} catch(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);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
Reference in New Issue
Block a user