Add new binary update mechanism
This commit is contained in:
@@ -47,3 +47,4 @@ jspm_packages
|
|||||||
|
|
||||||
# Docker compose spec
|
# Docker compose spec
|
||||||
docker-compose.*
|
docker-compose.*
|
||||||
|
data/
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -52,3 +52,4 @@ jspm_packages
|
|||||||
|
|
||||||
[Uu]ploads/
|
[Uu]ploads/
|
||||||
dist/
|
dist/
|
||||||
|
data/
|
||||||
52
index.js
52
index.js
@@ -1,12 +1,13 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const Queue = require('bull');
|
const Queue = require('bull');
|
||||||
const { exec, spawn } = require('child_process');
|
const { spawn } = require('child_process');
|
||||||
const fs = require('fs').promises;
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const cors = require('cors');
|
const cors = require('cors');
|
||||||
const EventEmitter = require('events');
|
const EventEmitter = require('events');
|
||||||
|
const softwareService = require('./services/softwares');
|
||||||
|
|
||||||
const BASE_PATH = `/data`;
|
const BASE_PATH = process.env.DATA_PATH || `./data`;
|
||||||
const OUTPUT_PATH = `${BASE_PATH}/output`;
|
const OUTPUT_PATH = `${BASE_PATH}/output`;
|
||||||
const TMP_PATH = `${BASE_PATH}/tmp`;
|
const TMP_PATH = `${BASE_PATH}/tmp`;
|
||||||
|
|
||||||
@@ -134,6 +135,28 @@ app.get('/download/:filename', async (req, res) => {
|
|||||||
res.download(file);
|
res.download(file);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.get('/hello', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
res.json({
|
||||||
|
downloader: await softwareService.checkDownloaderUpdate(),
|
||||||
|
mp4decrypt: await softwareService.checkMp4decryptUpdate()
|
||||||
|
})
|
||||||
|
} catch(e) {
|
||||||
|
console.log(e);
|
||||||
|
next(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/processUpdate', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
console.log(req.body);
|
||||||
|
await softwareService.processUpdate(req.body);
|
||||||
|
res.end();
|
||||||
|
} catch(e) {
|
||||||
|
console.log(e);
|
||||||
|
next(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const checkFilesExistance = (pattern) => {
|
const checkFilesExistance = (pattern) => {
|
||||||
return new Promise(async (resolve, reject) => {
|
return new Promise(async (resolve, reject) => {
|
||||||
@@ -235,7 +258,7 @@ videoQueue.process((job) => {
|
|||||||
let counter = 1;
|
let counter = 1;
|
||||||
for (const file of audioFiles) {
|
for (const file of audioFiles) {
|
||||||
if (file.startsWith(`${mp4TmpFilepath}_encrypted`) && file.endsWith('.srt')) {
|
if (file.startsWith(`${mp4TmpFilepath}_encrypted`) && file.endsWith('.srt')) {
|
||||||
await fs.rename(file, `${mp4FinalFilepath}_${counter}.srt`);
|
fs.renameSync(file, `${mp4FinalFilepath}_${counter}.srt`);
|
||||||
counter++;
|
counter++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -254,4 +277,25 @@ videoQueue.process((job) => {
|
|||||||
|
|
||||||
app.listen(port, () => {
|
app.listen(port, () => {
|
||||||
console.log(`Server running at http://localhost:${port}`);
|
console.log(`Server running at http://localhost:${port}`);
|
||||||
|
const dirsToCheck = [{
|
||||||
|
path: BASE_PATH,
|
||||||
|
name: 'data'
|
||||||
|
}, {
|
||||||
|
path: OUTPUT_PATH,
|
||||||
|
name: 'output'
|
||||||
|
}, {
|
||||||
|
path: TMP_PATH,
|
||||||
|
name: 'tmp'
|
||||||
|
}];
|
||||||
|
|
||||||
|
for (let i = 0; i < dirsToCheck.length; i++) {
|
||||||
|
const dir = dirsToCheck[i];
|
||||||
|
if (!fs.existsSync(dir.path)) {
|
||||||
|
console.log(`Creating ${dir.name} directory...`);
|
||||||
|
fs.mkdirSync(dir.path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
softwareService.init();
|
||||||
|
|
||||||
});
|
});
|
||||||
@@ -9,10 +9,14 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"axios": "^1.7.7",
|
||||||
"bull": "^4.16.3",
|
"bull": "^4.16.3",
|
||||||
|
"cheerio": "^1.0.0",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"express": "^4.21.0",
|
"express": "^4.21.0",
|
||||||
"fs": "^0.0.1-security",
|
"fs": "^0.0.1-security",
|
||||||
"path": "^0.12.7"
|
"path": "^0.12.7",
|
||||||
|
"tar": "^7.4.3",
|
||||||
|
"unzipper": "^0.12.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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