mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2024-12-20 19:06:06 +01:00
315 lines
8.3 KiB
JavaScript
315 lines
8.3 KiB
JavaScript
const os = require('os')
|
|
const path = require('path')
|
|
const axios = require('axios')
|
|
const fse = require('../fsExtra')
|
|
const async = require('../async')
|
|
const StreamZip = require('../nodeStreamZip')
|
|
const { finished } = require('stream/promises')
|
|
|
|
var API_URL = 'https://ffbinaries.com/api/v1'
|
|
|
|
var RUNTIME_CACHE = {}
|
|
var errorMsgs = {
|
|
connectionIssues: 'Couldn\'t connect to ffbinaries.com API. Check your Internet connection.',
|
|
parsingVersionData: 'Couldn\'t parse retrieved version data.',
|
|
parsingVersionList: 'Couldn\'t parse the list of available versions.',
|
|
notFound: 'Requested data not found.',
|
|
incorrectVersionParam: '"version" parameter must be a string.'
|
|
}
|
|
|
|
function ensureDirSync(dir) {
|
|
try {
|
|
fse.accessSync(dir)
|
|
} catch (e) {
|
|
fse.mkdirSync(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
|
|
*/
|
|
async function downloadUrls(components, urls, opts) {
|
|
const destinationDir = opts.destination
|
|
const 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) {
|
|
const oldpath = path.join(destinationDir, zipFilename)
|
|
const zip = new StreamZip.async({ file: oldpath })
|
|
const count = await zip.extract(null, destinationDir)
|
|
await zip.close()
|
|
}
|
|
|
|
|
|
await async.each(remappedUrls, async function (urlObject) {
|
|
try {
|
|
const url = urlObject.url
|
|
|
|
const zipFilename = url.split('/').pop()
|
|
const binFilenameBase = urlObject.component
|
|
const binFilename = getBinaryFilename(binFilenameBase, opts.platform || detectPlatform())
|
|
|
|
let runningTotal = 0
|
|
let totalFilesize
|
|
let interval
|
|
|
|
|
|
if (typeof opts.tickerFn === 'function') {
|
|
opts.tickerInterval = parseInt(opts.tickerInterval, 10)
|
|
const tickerInterval = (!Number.isNaN(opts.tickerInterval)) ? opts.tickerInterval : 1000
|
|
const 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)
|
|
}
|
|
|
|
|
|
// Check if file already exists in target directory
|
|
const binPath = path.join(destinationDir, binFilename)
|
|
if (!opts.force && await fse.pathExists(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
|
|
}
|
|
|
|
if (opts.quiet) clearInterval(interval)
|
|
|
|
const zipPath = path.join(destinationDir, zipFilename)
|
|
const zipFileTempName = zipPath + '.part'
|
|
const zipFileFinalName = zipPath
|
|
|
|
const response = await axios({
|
|
url,
|
|
method: 'GET',
|
|
responseType: 'stream'
|
|
})
|
|
totalFilesize = response.headers?.['content-length'] || []
|
|
|
|
const writer = fse.createWriteStream(zipFileTempName)
|
|
response.data.on('data', (chunk) => {
|
|
runningTotal += chunk.length
|
|
})
|
|
response.data.pipe(writer)
|
|
await finished(writer)
|
|
await fse.rename(zipFileTempName, zipFileFinalName)
|
|
await extractZipToDestination(zipFilename)
|
|
await fse.remove(zipFileFinalName)
|
|
|
|
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'
|
|
})
|
|
} catch (err) {
|
|
console.error(`Failed to download or extract file for component: ${urlObject.component}`, err)
|
|
}
|
|
})
|
|
|
|
return 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 await downloadUrls(components, urls, opts)
|
|
}
|
|
|
|
module.exports = {
|
|
downloadBinaries: downloadBinaries,
|
|
getVersionData: getVersionData,
|
|
listVersions: listVersions,
|
|
listPlatforms: listPlatforms,
|
|
detectPlatform: detectPlatform,
|
|
resolvePlatform: resolvePlatform,
|
|
getBinaryFilename: getBinaryFilename
|
|
} |