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 };