« get me outta code hell

mtui - Music Text User Interface - user-friendly command line music player
about summary refs log tree commit diff
path: root/general-util.js
diff options
context:
space:
mode:
Diffstat (limited to 'general-util.js')
-rw-r--r--general-util.js157
1 files changed, 101 insertions, 56 deletions
diff --git a/general-util.js b/general-util.js
index 0f5bdd5..d369848 100644
--- a/general-util.js
+++ b/general-util.js
@@ -1,13 +1,11 @@
-const { spawn } = require('child_process')
-const { promisify } = require('util')
-const fetch = require('node-fetch')
-const fs = require('fs')
-const npmCommandExists = require('command-exists')
-const url = require('url')
+import {spawn} from 'node:child_process'
+import {readFile} from 'node:fs/promises'
+import {fileURLToPath, URL} from 'node:url'
 
-const readFile = promisify(fs.readFile)
+import npmCommandExists from 'command-exists'
+import fetch from 'node-fetch'
 
-module.exports.promisifyProcess = function(proc, showLogging = true) {
+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).
@@ -28,7 +26,7 @@ module.exports.promisifyProcess = function(proc, showLogging = true) {
   })
 }
 
-module.exports.commandExists = async function(command) {
+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.
 
@@ -39,12 +37,12 @@ module.exports.commandExists = async function(command) {
   }
 }
 
-module.exports.killProcess = async function(proc) {
+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 module.exports.commandExists('taskkill')) {
-    await module.exports.promisifyProcess(
+  if (await commandExists('taskkill')) {
+    await promisifyProcess(
       spawn('taskkill', ['/pid', proc.pid, '/f', '/t']),
       false
     )
@@ -53,18 +51,18 @@ module.exports.killProcess = async function(proc) {
   }
 }
 
-function downloadPlaylistFromURL(url) {
+export function downloadPlaylistFromURL(url) {
   return fetch(url).then(res => res.text())
 }
 
-function downloadPlaylistFromLocalPath(path) {
+export function downloadPlaylistFromLocalPath(path) {
   return readFile(path).then(buf => buf.toString())
 }
 
-module.exports.downloadPlaylistFromOptionValue = function(arg) {
+export function downloadPlaylistFromOptionValue(arg) {
   let argURL
   try {
-    argURL = new url.URL(arg)
+    argURL = new URL(arg)
   } catch (err) {
     // Definitely not a URL.
   }
@@ -73,14 +71,14 @@ module.exports.downloadPlaylistFromOptionValue = function(arg) {
     if (argURL.protocol === 'http:' || argURL.protocol === 'https:') {
       return downloadPlaylistFromURL(arg)
     } else if (argURL.protocol === 'file:') {
-      return downloadPlaylistFromLocalPath(url.fileURLToPath(argURL))
+      return downloadPlaylistFromLocalPath(fileURLToPath(argURL))
     }
   } else {
     return downloadPlaylistFromLocalPath(arg)
   }
 }
 
-module.exports.shuffleArray = function(array) {
+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/
@@ -103,7 +101,7 @@ module.exports.shuffleArray = function(array) {
   return workingArray
 }
 
-module.exports.throttlePromise = function(maximumAtOneTime = 10) {
+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.
@@ -139,61 +137,99 @@ module.exports.throttlePromise = function(maximumAtOneTime = 10) {
   return enqueue
 }
 
-module.exports.getTimeStringsFromSec = function(curSecTotal, lenSecTotal) {
-  const percentVal = (100 / lenSecTotal) * curSecTotal
-  const percentDone = (
-    (Math.trunc(percentVal * 100) / 100).toFixed(2) + '%'
-  )
+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 = null, fraction = false) {
+  const pad = val => val.toString().padStart(2, '0')
+  const padFrac = val => Math.floor(val * 1000).toString().padEnd(3, '0')
+
+  // We don't want to display hour counters if the total length is less
+  // than an hour.
+  const displayAsHours = Math.max(curSecTotal, lenSecTotal ?? 0) >= 3600
 
-  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)
+  const strings = {curSecTotal, lenSecTotal}
 
-  // 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)
-
-  const pad = val => val.toString().padStart(2, '0')
   curMin = pad(curMin)
   curSec = pad(curSec)
-  lenMin = pad(lenMin)
-  lenSec = pad(lenSec)
-  leftMin = pad(leftMin)
-  leftSec = pad(leftSec)
+  curFrac = padFrac(curFrac)
 
-  // 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) {
-    timeDone = `${curHour}:${curMin}:${curSec}`
-    timeLeft = `${leftHour}:${leftMin}:${leftSec}`
-    duration = `${lenHour}:${lenMin}:${lenSec}`
+  if (displayAsHours) {
+    strings.timeDone = `${curHour}:${curMin}:${curSec}`
   } else {
-    timeDone = `${curMin}:${curSec}`
-    timeLeft = `${leftMin}:${leftSec}`
-    duration = `${lenMin}:${lenSec}`
+    strings.timeDone = `${curMin}:${curSec}`
+  }
+
+  if (fraction) {
+    strings.timeDone += '.' + curFrac
   }
 
-  return {percentDone, timeDone, timeLeft, duration, curSecTotal, lenSecTotal}
+  if (typeof lenSecTotal === 'number') {
+    const percentVal = (100 / lenSecTotal) * curSecTotal
+    strings.percentDone = (Math.trunc(percentVal * 100) / 100).toFixed(2) + '%'
+
+    // Yeah, yeah, duplicate math.
+    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 = leftSecTotal % 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
+
+    lenMin = pad(lenMin)
+    lenSec = pad(lenSec)
+    lenFrac = padFrac(lenFrac)
+
+    leftMin = pad(leftMin)
+    leftSec = pad(leftSec)
+    leftFrac = padFrac(leftFrac)
+
+    if (typeof lenSecTotal === 'number') {
+      if (displayAsHours) {
+        strings.timeLeft = `${leftHour}:${leftMin}:${leftSec}`
+        strings.duration = `${lenHour}:${lenMin}:${lenSec}`
+      } else {
+        strings.timeLeft = `${leftMin}:${leftSec}`
+        strings.duration = `${lenMin}:${lenSec}`
+      }
+
+      if (fraction) {
+        strings.timeLeft += '.' + leftFrac
+        strings.duration += '.' + lenFrac
+      }
+    }
+  }
+
+  return strings
 }
 
-module.exports.getTimeStrings = function({curHour, curMin, curSec, lenHour, lenMin, lenSec}) {
+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 module.exports.getTimeStringsFromSec(curSecTotal, lenSecTotal)
+  return getTimeStringsFromSec(curSecTotal, lenSecTotal)
 }
 
-const parseOptions = async function(options, optionDescriptorMap) {
+export async function parseOptions(options, optionDescriptorMap) {
   // This function is sorely lacking in comments, but the basic usage is
   // as such:
   //
@@ -309,9 +345,7 @@ const parseOptions = async function(options, optionDescriptorMap) {
 
 parseOptions.handleDashless = Symbol()
 
-module.exports.parseOptions = parseOptions
-
-module.exports.silenceEvents = async function(emitter, eventsToSilence, callback) {
+export async function silenceEvents(emitter, eventsToSilence, callback) {
   const oldEmit = emitter.emit
 
   emitter.emit = function(event, ...data) {
@@ -324,3 +358,14 @@ module.exports.silenceEvents = async function(emitter, eventsToSilence, callback
 
   emitter.emit = oldEmit
 }
+
+// Kindly stolen from ESDiscuss:
+// https://esdiscuss.org/topic/proposal-add-an-option-to-omit-prototype-of-objects-created-by-json-parse#content-1
+export function parseWithoutPrototype(string) {
+  return JSON.parse(string, function(k, v) {
+    if (v && typeof v === 'object' && !Array.isArray(v)) {
+      return Object.assign(Object.create(null), v)
+    }
+    return v
+  })
+}