Add ffbinaries lib

This commit is contained in:
advplyr 2023-12-05 17:35:15 -06:00
parent c074c835d4
commit 1ce1904c89
2 changed files with 362 additions and 10 deletions

View File

@ -0,0 +1,354 @@
const os = require('os')
const path = require('path')
const axios = require('axios')
const fse = require('../fsExtra')
const async = require('../async')
const StreamZip = require('../nodeStreamZip')
var API_URL = 'https://ffbinaries.com/api/v1'
var LOCAL_CACHE_DIR = path.join(os.homedir() + '/.ffbinaries-cache')
var RUNTIME_CACHE = {}
var errorMsgs = {
connectionIssues: 'Couldn\'t connect to ffbinaries.com API. Check your Internet connection.',
parsingVersionData: 'Couldn\'t parse retrieved version data. Try "ffbinaries clearcache".',
parsingVersionList: 'Couldn\'t parse the list of available versions. Try "ffbinaries clearcache".',
notFound: 'Requested data not found.',
incorrectVersionParam: '"version" parameter must be a string.'
}
function ensureDirSync(dir) {
try {
fse.accessSync(dir)
} catch (e) {
fse.mkdirSync(dir)
}
}
ensureDirSync(LOCAL_CACHE_DIR)
/**
* Resolves the platform key based on input string
*/
function resolvePlatform(input) {
var rtn = null
switch (input) {
case 'mac':
case 'osx':
case 'mac-64':
case 'osx-64':
rtn = 'osx-64'
break
case 'linux':
case 'linux-32':
rtn = 'linux-32'
break
case 'linux-64':
rtn = 'linux-64'
break
case 'linux-arm':
case 'linux-armel':
rtn = 'linux-armel'
break
case 'linux-armhf':
rtn = 'linux-armhf'
break
case 'win':
case 'win-32':
case 'windows':
case 'windows-32':
rtn = 'windows-32'
break
case 'win-64':
case 'windows-64':
rtn = 'windows-64'
break
default:
rtn = null
}
return rtn
}
/**
* Detects the platform of the machine the script is executed on.
* Object can be provided to detect platform from info derived elsewhere.
*
* @param {object} osinfo Contains "type" and "arch" properties
*/
function detectPlatform(osinfo) {
var inputIsValid = typeof osinfo === 'object' && typeof osinfo.type === 'string' && typeof osinfo.arch === 'string'
var type = (inputIsValid ? osinfo.type : os.type()).toLowerCase()
var arch = (inputIsValid ? osinfo.arch : os.arch()).toLowerCase()
if (type === 'darwin') {
return 'osx-64'
}
if (type === 'windows_nt') {
return arch === 'x64' ? 'windows-64' : 'windows-32'
}
if (type === 'linux') {
if (arch === 'arm' || arch === 'arm64') {
return 'linux-armel'
}
return arch === 'x64' ? 'linux-64' : 'linux-32'
}
return null
}
/**
* Gets the binary filename (appends exe in Windows)
*
* @param {string} component "ffmpeg", "ffplay", "ffprobe" or "ffserver"
* @param {platform} platform "ffmpeg", "ffplay", "ffprobe" or "ffserver"
*/
function getBinaryFilename(component, platform) {
var platformCode = resolvePlatform(platform)
if (platformCode === 'windows-32' || platformCode === 'windows-64') {
return component + '.exe'
}
return component
}
function listPlatforms() {
return ['osx-64', 'linux-32', 'linux-64', 'linux-armel', 'linux-armhf', 'windows-32', 'windows-64']
}
/**
*
* @returns {Promise<string[]>} array of version strings
*/
function listVersions() {
if (RUNTIME_CACHE.versionsAll) {
return RUNTIME_CACHE.versionsAll
}
return axios.get(API_URL).then((res) => {
if (!res.data?.versions || !Object.keys(res.data.versions)?.length) {
throw new Error(errorMsgs.parsingVersionList)
}
const versionKeys = Object.keys(res.data.versions)
RUNTIME_CACHE.versionsAll = versionKeys
return versionKeys
})
}
/**
* Gets full data set from ffbinaries.com
*/
function getVersionData(version) {
if (RUNTIME_CACHE[version]) {
return RUNTIME_CACHE[version]
}
if (version && typeof version !== 'string') {
throw new Error(errorMsgs.incorrectVersionParam)
}
var url = version ? '/version/' + version : '/latest'
return axios.get(`${API_URL}${url}`).then((res) => {
RUNTIME_CACHE[version] = res.data
return res.data
}).catch((error) => {
if (error.response?.status == 404) {
throw new Error(errorMsgs.notFound)
} else {
throw new Error(errorMsgs.connectionIssues)
}
})
}
/**
* Download file(s) and save them in the specified directory
*/
function downloadUrls(components, urls, opts, callback) {
var destinationDir = opts.destination
var results = []
const remappedUrls = []
if (components && !Array.isArray(components)) {
components = [components]
} else if (!components || !Array.isArray(components)) {
components = []
}
// returns an array of objects like this: {component: 'ffmpeg', url: 'https://...'}
if (typeof urls === 'object') {
for (const key in urls) {
if (components.includes(key) && urls[key]) {
remappedUrls.push({
component: key,
url: urls[key]
})
}
}
}
async function extractZipToDestination(zipFilename, cb) {
var oldpath = path.join(LOCAL_CACHE_DIR, zipFilename)
const zip = new StreamZip.async({ file: oldpath })
const count = await zip.extract(null, destinationDir)
console.log(`Extracted ${count} entries`)
await zip.close()
cb()
}
async.each(remappedUrls, function (urlObject, cb) {
if (!urlObject?.url || !urlObject?.component) {
return cb()
}
var url = urlObject.url
var zipFilename = url.split('/').pop()
var binFilenameBase = urlObject.component
var binFilename = getBinaryFilename(binFilenameBase, opts.platform || detectPlatform())
var runningTotal = 0
var totalFilesize
var interval
if (typeof opts.tickerFn === 'function') {
opts.tickerInterval = parseInt(opts.tickerInterval, 10)
var tickerInterval = (!Number.isNaN(opts.tickerInterval)) ? opts.tickerInterval : 1000
var tickData = { filename: zipFilename, progress: 0 }
// Schedule next ticks
interval = setInterval(function () {
if (totalFilesize && runningTotal == totalFilesize) {
return clearInterval(interval)
}
tickData.progress = totalFilesize > -1 ? runningTotal / totalFilesize : 0
opts.tickerFn(tickData)
}, tickerInterval)
}
try {
if (opts.force) {
throw new Error('Force mode specified - will overwrite existing binaries in target location')
}
// Check if file already exists in target directory
var binPath = path.join(destinationDir, binFilename)
fse.accessSync(binPath)
// if the accessSync method doesn't throw we know the binary already exists
results.push({
filename: binFilename,
path: destinationDir,
status: 'File exists',
code: 'FILE_EXISTS'
})
clearInterval(interval)
return cb()
} catch (errBinExists) {
var zipPath = path.join(LOCAL_CACHE_DIR, zipFilename)
// If there's no binary then check if the zip file is already in cache
try {
fse.accessSync(zipPath)
results.push({
filename: binFilename,
path: destinationDir,
status: 'File extracted to destination (archive found in cache)',
code: 'DONE_FROM_CACHE'
})
clearInterval(interval)
return extractZipToDestination(zipFilename, cb)
} catch (errZipExists) {
// If zip is not cached then download it and store in cache
if (opts.quiet) clearInterval(interval)
var cacheFileTempName = zipPath + '.part'
var cacheFileFinalName = zipPath
axios({
url,
method: 'GET',
responseType: 'stream'
}).then((response) => {
totalFilesize = response.headers?.['content-length'] || []
// Write to filepath
const writer = fse.createWriteStream(cacheFileTempName)
response.data.pipe(writer)
writer.on('finish', () => {
results.push({
filename: binFilename,
path: destinationDir,
size: Math.floor(totalFilesize / 1024 / 1024 * 1000) / 1000 + 'MB',
status: 'File extracted to destination (downloaded from "' + url + '")',
code: 'DONE_CLEAN'
})
fse.renameSync(cacheFileTempName, cacheFileFinalName)
extractZipToDestination(zipFilename, cb)
})
writer.on('error', (err) => {
// TODO: Handle writer err
throw new Error(err)
})
}).catch((err) => {
// TODO: Handle error
console.error(`Failed to download file "${zipFilename}"`, err)
cb()
})
}
}
}, function () {
return callback(null, results)
})
}
/**
* Gets binaries for the platform
* It will get the data from ffbinaries, pick the correct files
* and save it to the specified directory
*
* @param {Array} components
* @param {Object} [opts]
*/
async function downloadBinaries(components, opts = {}) {
var platform = resolvePlatform(opts.platform) || detectPlatform()
opts.destination = path.resolve(opts.destination || '.')
ensureDirSync(opts.destination)
const versionData = await getVersionData(opts.version)
const urls = versionData?.bin?.[platform]
if (!urls) {
throw new Error('No URLs!')
}
return new Promise((resolve, reject) => {
downloadUrls(components, urls, opts, (err, data) => {
if (err) reject(err)
else resolve(data)
})
})
}
function clearCache() {
fse.emptyDirSync(LOCAL_CACHE_DIR)
}
module.exports = {
downloadBinaries: downloadBinaries,
getVersionData: getVersionData,
listVersions: listVersions,
listPlatforms: listPlatforms,
detectPlatform: detectPlatform,
resolvePlatform: resolvePlatform,
getBinaryFilename: getBinaryFilename,
clearCache: clearCache
}

View File

@ -1,16 +1,14 @@
const path = require('path')
const which = require('../libs/which')
const fs = require('../libs/fsExtra')
const ffbinaries = require('../libs/ffbinaries')
const Logger = require('../Logger')
const ffbinaries = require('ffbinaries')
const { promisify } = require('util')
class BinaryManager {
downloadBinaries = promisify(ffbinaries.downloadBinaries)
class BinaryManager {
defaultRequiredBinaries = [
{ name: 'ffmpeg', envVariable: 'FFMPEG_PATH' },
{ name: 'ffprobe', envVariable: 'FFPROBE_PATH' }
defaultRequiredBinaries = [
{ name: 'ffmpeg', envVariable: 'FFMPEG_PATH' },
{ name: 'ffprobe', envVariable: 'FFPROBE_PATH' }
]
constructor(requiredBinaries = this.defaultRequiredBinaries) {
@ -65,12 +63,12 @@ class BinaryManager {
if (binaries.length == 0) return
Logger.info(`[BinaryManager] Installing binaries: ${binaries.join(', ')}`)
let destination = this.mainInstallPath
try {
try {
await fs.access(destination, fs.constants.W_OK)
} catch (err) {
} catch (err) {
destination = this.altInstallPath
}
await this.downloadBinaries(binaries, { destination })
await ffbinaries.downloadBinaries(binaries, { destination })
Logger.info(`[BinaryManager] Binaries installed to ${destination}`)
}