123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332 |
- import {spawn} from 'node:child_process'
- import {readFile} from 'node:fs/promises'
- import {fileURLToPath, URL} from 'node:url'
- import npmCommandExists from 'command-exists'
- import fetch from 'node-fetch'
- export function promisifyProcess(proc, showLogging = true) {
- // Takes a process (from the child_process module) and returns a promise
- // that resolves when the process exits (or rejects, if the exit code is
- // non-zero).
- return new Promise((resolve, reject) => {
- if (showLogging) {
- proc.stdout.pipe(process.stdout)
- proc.stderr.pipe(process.stderr)
- }
- proc.on('exit', code => {
- if (code === 0) {
- resolve()
- } else {
- reject(code)
- }
- })
- })
- }
- export async function commandExists(command) {
- // When the command-exists module sees that a given command doesn't exist, it
- // throws an error instead of returning false, which is not what we want.
- try {
- return await npmCommandExists(command)
- } catch(err) {
- return false
- }
- }
- export async function killProcess(proc) {
- // Windows is stupid and doesn't like it when we try to kill processes.
- // So instead we use taskkill! https://stackoverflow.com/a/28163919/4633828
- if (await commandExists('taskkill')) {
- await promisifyProcess(
- spawn('taskkill', ['/pid', proc.pid, '/f', '/t']),
- false
- )
- } else {
- proc.kill()
- }
- }
- export function downloadPlaylistFromURL(url) {
- return fetch(url).then(res => res.text())
- }
- export function downloadPlaylistFromLocalPath(path) {
- return readFile(path).then(buf => buf.toString())
- }
- export function downloadPlaylistFromOptionValue(arg) {
- let argURL
- try {
- argURL = new URL(arg)
- } catch (err) {
- // Definitely not a URL.
- }
- if (argURL) {
- if (argURL.protocol === 'http:' || argURL.protocol === 'https:') {
- return downloadPlaylistFromURL(arg)
- } else if (argURL.protocol === 'file:') {
- return downloadPlaylistFromLocalPath(fileURLToPath(argURL))
- }
- } else {
- return downloadPlaylistFromLocalPath(arg)
- }
- }
- export function shuffleArray(array) {
- // Shuffles the items in an array. Returns a new array (does not modify the
- // passed array). Super-interesting post on how this algorithm works:
- // https://bost.ocks.org/mike/shuffle/
- const workingArray = array.slice(0)
- let m = array.length
- while (m) {
- let i = Math.floor(Math.random() * m)
- m--
- // Stupid lol; avoids the need of a temporary variable!
- Object.assign(workingArray, {
- [m]: workingArray[i],
- [i]: workingArray[m]
- })
- }
- return workingArray
- }
- export function throttlePromise(maximumAtOneTime = 10) {
- // Returns a function that takes a callback to create a promise and either
- // runs it now, if there is an available slot, or enqueues it to be run
- // later, if there is not.
- let activeCount = 0
- const queue = []
- const execute = function(callback) {
- activeCount++
- return callback().finally(() => {
- activeCount--
- if (queue.length) {
- return execute(queue.shift())
- }
- })
- }
- const enqueue = function(callback) {
- if (activeCount >= maximumAtOneTime) {
- return new Promise((resolve, reject) => {
- queue.push(function() {
- return callback().then(resolve, reject)
- })
- })
- } else {
- return execute(callback)
- }
- }
- enqueue.queue = queue
- return enqueue
- }
- export function getSecFromTimestamp(timestamp) {
- const parts = timestamp.split(':').map(n => parseInt(n))
- switch (parts.length) {
- case 3: return parts[0] * 3600 + parts[1] * 60 + parts[2]
- case 2: return parts[0] * 60 + parts[1]
- case 1: return parts[0]
- default: return 0
- }
- }
- export function getTimeStringsFromSec(curSecTotal, lenSecTotal, fraction = false) {
- const percentVal = (100 / lenSecTotal) * curSecTotal
- const percentDone = (
- (Math.trunc(percentVal * 100) / 100).toFixed(2) + '%'
- )
- const leftSecTotal = lenSecTotal - curSecTotal
- let leftHour = Math.floor(leftSecTotal / 3600)
- let leftMin = Math.floor((leftSecTotal - leftHour * 3600) / 60)
- let leftSec = Math.floor(leftSecTotal - leftHour * 3600 - leftMin * 60)
- let leftFrac = lenSecTotal % 1
- // Yeah, yeah, duplicate math.
- let curHour = Math.floor(curSecTotal / 3600)
- let curMin = Math.floor((curSecTotal - curHour * 3600) / 60)
- let curSec = Math.floor(curSecTotal - curHour * 3600 - curMin * 60)
- let curFrac = curSecTotal % 1
- // Wee!
- let lenHour = Math.floor(lenSecTotal / 3600)
- let lenMin = Math.floor((lenSecTotal - lenHour * 3600) / 60)
- let lenSec = Math.floor(lenSecTotal - lenHour * 3600 - lenMin * 60)
- let lenFrac = lenSecTotal % 1
- const pad = val => val.toString().padStart(2, '0')
- const padFrac = val => Math.floor(val * 1000).toString().padEnd(3, '0')
- curMin = pad(curMin)
- curSec = pad(curSec)
- lenMin = pad(lenMin)
- lenSec = pad(lenSec)
- leftMin = pad(leftMin)
- leftSec = pad(leftSec)
- curFrac = padFrac(curFrac)
- lenFrac = padFrac(lenFrac)
- leftFrac = padFrac(leftFrac)
- // We don't want to display hour counters if the total length is less
- // than an hour.
- let timeDone, timeLeft, duration
- if (parseInt(lenHour) > 0 || parseInt(curHour) > 0) {
- timeDone = `${curHour}:${curMin}:${curSec}`
- timeLeft = `${leftHour}:${leftMin}:${leftSec}`
- duration = `${lenHour}:${lenMin}:${lenSec}`
- } else {
- timeDone = `${curMin}:${curSec}`
- timeLeft = `${leftMin}:${leftSec}`
- duration = `${lenMin}:${lenSec}`
- }
- if (fraction) {
- timeDone += '.' + curFrac
- timeLeft += '.' + leftFrac
- duration += '.' + lenFrac
- }
- return {percentDone, timeDone, timeLeft, duration, curSecTotal, lenSecTotal}
- }
- export function getTimeStrings({curHour, curMin, curSec, lenHour, lenMin, lenSec}) {
- // Multiplication casts to numbers; addition prioritizes strings.
- // Thanks, JavaScript!
- const curSecTotal = (3600 * curHour) + (60 * curMin) + (1 * curSec)
- const lenSecTotal = (3600 * lenHour) + (60 * lenMin) + (1 * lenSec)
- return getTimeStringsFromSec(curSecTotal, lenSecTotal)
- }
- export async function parseOptions(options, optionDescriptorMap) {
- // This function is sorely lacking in comments, but the basic usage is
- // as such:
- //
- // options is the array of options you want to process;
- // optionDescriptorMap is a mapping of option names to objects that describe
- // the expected value for their corresponding options.
- // Returned is a mapping of any specified option names to their values, or
- // a process.exit(1) and error message if there were any issues.
- //
- // Here are examples of optionDescriptorMap to cover all the things you can
- // do with it:
- //
- // optionDescriptorMap: {
- // 'telnet-server': {type: 'flag'},
- // 't': {alias: 'telnet-server'}
- // }
- //
- // options: ['t'] -> result: {'telnet-server': true}
- //
- // optionDescriptorMap: {
- // 'directory': {
- // type: 'value',
- // validate(name) {
- // // const whitelistedDirectories = ['apple', 'banana']
- // if (whitelistedDirectories.includes(name)) {
- // return true
- // } else {
- // return 'a whitelisted directory'
- // }
- // }
- // },
- // 'files': {type: 'series'}
- // }
- //
- // ['--directory', 'apple'] -> {'directory': 'apple'}
- // ['--directory', 'artichoke'] -> (error)
- // ['--files', 'a', 'b', 'c', ';'] -> {'files': ['a', 'b', 'c']}
- //
- // TODO: Be able to validate the values in a series option.
- const handleDashless = optionDescriptorMap[parseOptions.handleDashless]
- const result = {}
- for (let i = 0; i < options.length; i++) {
- const option = options[i]
- if (option.startsWith('--')) {
- // --x can be a flag or expect a value or series of values
- let name = option.slice(2).split('=')[0] // '--x'.split('=') = ['--x']
- let descriptor = optionDescriptorMap[name]
- if (!descriptor) {
- console.error(`Unknown option name: ${name}`)
- process.exit(1)
- }
- if (descriptor.alias) {
- name = descriptor.alias
- descriptor = optionDescriptorMap[name]
- }
- if (descriptor.type === 'flag') {
- result[name] = true
- } else if (descriptor.type === 'value') {
- let value = option.slice(2).split('=')[1]
- if (!value) {
- value = options[++i]
- if (!value || value.startsWith('-')) {
- value = null
- }
- }
- if (!value) {
- console.error(`Expected a value for --${name}`)
- process.exit(1)
- }
- result[name] = value
- } else if (descriptor.type === 'series') {
- if (!options.slice(i).includes(';')) {
- console.error(`Expected a series of values concluding with ; (\\;) for --${name}`)
- process.exit(1)
- }
- const endIndex = i + options.slice(i).indexOf(';')
- result[name] = options.slice(i + 1, endIndex)
- i = endIndex
- }
- if (descriptor.validate) {
- const validation = await descriptor.validate(result[name])
- if (validation !== true) {
- console.error(`Expected ${validation} for --${name}`)
- process.exit(1)
- }
- }
- } else if (option.startsWith('-')) {
- // mtui doesn't use any -x=y or -x y format optionuments
- // -x will always just be a flag
- let name = option.slice(1)
- let descriptor = optionDescriptorMap[name]
- if (!descriptor) {
- console.error(`Unknown option name: ${name}`)
- process.exit(1)
- }
- if (descriptor.alias) {
- name = descriptor.alias
- descriptor = optionDescriptorMap[name]
- }
- if (descriptor.type === 'flag') {
- result[name] = true
- } else {
- console.error(`Use --${name} (value) to specify ${name}`)
- process.exit(1)
- }
- } else if (handleDashless) {
- handleDashless(option)
- }
- }
- return result
- }
- parseOptions.handleDashless = Symbol()
|