« get me outta code hell

cli args (bass boost ur music) - mtui - Music Text User Interface - user-friendly command line music player
about summary refs log tree commit diff
diff options
context:
space:
mode:
authorFlorrie <towerofnix@gmail.com>2020-02-15 22:13:53 -0400
committerFlorrie <towerofnix@gmail.com>2020-02-15 22:15:15 -0400
commit48ed5168d477fe11fe4f21ae104e3750935b0943 (patch)
treec0d9260246933d180a2fa5405da8e66e9c484705
parentfd09d0196f8db2102f9364a56f3075bf2cd93c88 (diff)
cli args (bass boost ur music)
$ mtui --player sox --player-options bass +25 \;
-rw-r--r--backend.js17
-rw-r--r--crawlers.js2
-rw-r--r--general-util.js118
-rwxr-xr-xindex.js39
-rw-r--r--players.js33
-rw-r--r--todo.txt10
6 files changed, 195 insertions, 24 deletions
diff --git a/backend.js b/backend.js
index 379a16b..0cceeea 100644
--- a/backend.js
+++ b/backend.js
@@ -57,6 +57,7 @@ async function download(item, record) {
 
 class QueuePlayer extends EventEmitter {
   constructor({
+    getPlayer,
     getRecordFor
   }) {
     super()
@@ -68,11 +69,12 @@ class QueuePlayer extends EventEmitter {
     this.playedTrackToEnd = false
     this.timeData = null
 
+    this.getPlayer = getPlayer
     this.getRecordFor = getRecordFor
   }
 
   async setup() {
-    this.player = await getPlayer()
+    this.player = await this.getPlayer()
 
     if (!this.player) {
       return {
@@ -560,9 +562,19 @@ class QueuePlayer extends EventEmitter {
 }
 
 class Backend extends EventEmitter {
-  constructor() {
+  constructor({
+    playerName = null,
+    playerOptions = []
+  } = {}) {
     super()
 
+    this.playerName = playerName;
+    this.playerOptions = playerOptions;
+
+    if (playerOptions && !playerName) {
+      throw new Error(`Must specify playerName to specify playerOptions`);
+    }
+
     this.queuePlayers = []
 
     this.recordStore = new RecordStore()
@@ -586,6 +598,7 @@ class Backend extends EventEmitter {
 
   async addQueuePlayer() {
     const queuePlayer = new QueuePlayer({
+      getPlayer: () => getPlayer(this.playerName, this.playerOptions),
       getRecordFor: item => this.getRecordFor(item)
     })
 
diff --git a/crawlers.js b/crawlers.js
index 578a9f2..92243c9 100644
--- a/crawlers.js
+++ b/crawlers.js
@@ -237,7 +237,7 @@ function getHTMLLinks(text) {
 
 function crawlLocal(dirPath, extensions = [
   'ogg', 'oga',
-  'wav', 'mp3', 'mp4', 'm4a', 'aac', 'flac',
+  'wav', 'mp3', 'mp4', 'm4a', 'aac', 'flac', 'opus',
   'mod'
 ], isTop = true) {
   // If the passed path is a file:// URL, try to decode it:
diff --git a/general-util.js b/general-util.js
index 3aa4180..0a81cdc 100644
--- a/general-util.js
+++ b/general-util.js
@@ -192,3 +192,121 @@ module.exports.getTimeStrings = function({curHour, curMin, curSec, lenHour, lenM
 
   return module.exports.getTimeStringsFromSec(curSecTotal, lenSecTotal)
 }
+
+const parseOptions = async function(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()
+
+module.exports.parseOptions = parseOptions
diff --git a/index.js b/index.js
index bb0daee..c40bbb9 100755
--- a/index.js
+++ b/index.js
@@ -3,6 +3,8 @@
 // omg I am tired of code
 
 const { getAllCrawlersForArg } = require('./crawlers')
+const { getPlayer } = require('./players')
+const { parseOptions } = require('./general-util')
 const AppElement = require('./ui')
 const Backend = require('./backend')
 const TelnetServer = require('./telnet-server')
@@ -50,7 +52,36 @@ process.on('unhandledRejection', error => {
 })
 
 async function main() {
-  const backend = new Backend()
+  const playlistSources = []
+
+  const options = await parseOptions(process.argv.slice(2), {
+    'player': {
+      type: 'value',
+      async validate(playerName) {
+        if (await getPlayer(playerName)) {
+          return true
+        } else {
+          return 'a known player identifier'
+        }
+      }
+    },
+    'player-options': {
+      type: 'series'
+    },
+    [parseOptions.handleDashless](option) {
+      playlistSources.push(option)
+    }
+  })
+
+  if (options['player-options'] && !options['player']) {
+    console.error('--player must be specified in order to use --player-options')
+    process.exit(1)
+  }
+
+  const backend = new Backend({
+    playerName: options['player'],
+    playerOptions: options['player-options']
+  })
 
   const result = await backend.setup()
   if (result.error) {
@@ -93,10 +124,8 @@ async function main() {
   })
 
   const loadPlaylists = async () => {
-    for (let i = 2; i < process.argv.length; i++) {
-      if (!process.argv[i].startsWith('--')) {
-        await appElement.handlePlaylistSource(process.argv[i], true)
-      }
+    for (const source of playlistSources) {
+      await appElement.handlePlaylistSource(source, true)
     }
   }
 
diff --git a/players.js b/players.js
index 5d332b3..868129d 100644
--- a/players.js
+++ b/players.js
@@ -10,9 +10,11 @@ const util = require('util')
 const unlink = util.promisify(fs.unlink)
 
 class Player extends EventEmitter {
-  constructor() {
+  constructor(processOptions = []) {
     super()
 
+    this.processOptions = processOptions
+
     this.disablePlaybackStatus = false
     this.isLooping = false
     this.isPaused = false
@@ -78,7 +80,7 @@ module.exports.MPVPlayer = class extends Player {
     // The more powerful MPV player. MPV is virtually impossible for a human
     // being to install; if you're having trouble with it, try the SoX player.
 
-    this.process = spawn('mpv', this.getMPVOptions(file))
+    this.process = spawn('mpv', this.getMPVOptions(file).concat(this.processOptions))
 
     let lastPercent = 0
 
@@ -223,7 +225,7 @@ module.exports.SoXPlayer = class extends Player {
     // You don't get keyboard controls such as seeking or volume adjusting
     // with SoX, though.
 
-    this.process = spawn('play', [file])
+    this.process = spawn('play', [file].concat(this.processOptions))
 
     this.process.stdout.on('data', data => {
       process.stdout.write(data.toString())
@@ -276,19 +278,18 @@ module.exports.SoXPlayer = class extends Player {
   }
 }
 
-module.exports.getPlayer = async function() {
-  if (await commandExists('mpv')) {
-    /*
-    if (await commandExists('socat')) {
-      return new module.exports.ControllableMPVPlayer()
-    } else {
-      return new module.exports.MPVPlayer()
-    }
-    */
-    return new module.exports.ControllableMPVPlayer()
-  } else if (await commandExists('play')) {
-    return new module.exports.SoXPlayer()
-  } else {
+module.exports.getPlayer = async function(name = null, options = []) {
+  if (await commandExists('mpv') && (name === null || name === 'mpv')) {
+    return new module.exports.ControllableMPVPlayer(options)
+  } else if (name === 'mpv') {
+    return null
+  }
+
+  if (await commandExists('play') && (name === null || name === 'sox')) {
+    return new module.exports.SoXPlayer(options)
+  } else if (name === 'sox') {
     return null
   }
+
+  return null
 }
diff --git a/todo.txt b/todo.txt
index 5e1fe65..418ee49 100644
--- a/todo.txt
+++ b/todo.txt
@@ -472,3 +472,13 @@ TODO: Update to work with IPC server mpv (and socat).
 
 TODO: Look into testing ^that on Windows. Remove mkfifo, since it's probably
       no longer necessary!
+
+TODO: Expand selection context menu by pressing the heading button! It should
+      show a list of the tracks contained within the selection. Selecting any
+      item should reveal that item in the main listing pane.
+
+TODO: Opening the selection contxt menu should show an option to either add or
+      remove the cursor-focused item from the selection - this would make
+      selection accessible when a keyboard or the shift key is inaccessible.
+
+TODO: Integrate the rest of the stuff that handles argv into parseOptions.