« get me outta code hell

Update JS to have one main http-music command - http-music - Command-line music player + utils (not a server!)
about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorliam4 <towerofnix@gmail.com>2017-07-20 18:55:18 -0300
committerliam4 <towerofnix@gmail.com>2017-07-20 18:55:18 -0300
commit1a02fe1688d66aea6277f61fcc305a6aed4e7d76 (patch)
tree6e64e928acf81af89fd7f0e802d94eb0c844a63e /src
parent2680ea9126499fb775b699f1dbd4ac9aacbc4e4a (diff)
Update JS to have one main http-music command
Diffstat (limited to 'src')
-rw-r--r--src/cli.js37
-rwxr-xr-xsrc/crawl-http.js20
-rwxr-xr-xsrc/crawl-itunes.js12
-rwxr-xr-xsrc/crawl-local.js23
-rwxr-xr-xsrc/download-playlist.js14
-rwxr-xr-xsrc/http-music.js404
-rw-r--r--src/loop-play.js2
-rwxr-xr-xsrc/play.js412
8 files changed, 494 insertions, 430 deletions
diff --git a/src/cli.js b/src/cli.js
new file mode 100644
index 0000000..d88ddc0
--- /dev/null
+++ b/src/cli.js
@@ -0,0 +1,37 @@
+#!/usr/bin/env node
+
+// Let this forever be of use to people who run into
+// maxlistenersexceededwarning.
+process.on('warning', e => console.warn(e.stack))
+
+async function main(args) {
+  let script
+
+  if (args.length === 0) {
+    console.error("No command provided.")
+    console.error("Try 'man http-music'?")
+    return
+  }
+
+  switch (args[0]) {
+    case 'play': script = require('./play'); break
+    case 'crawl-http': script = require('./crawl-http'); break
+    case 'crawl-local': script = require('./crawl-local'); break
+    case 'crawl-itunes': script = require('./crawl-itunes'); break
+    case 'download-playlist': script = require('./download-playlist'); break
+
+    default:
+      console.error(`Invalid command "${args[0]}" provided.`)
+      console.error("Try 'man http-music'?")
+      return
+  }
+
+  await script(args.slice(1))
+}
+
+module.exports = main
+
+if (require.main === module) {
+  main(process.argv.slice(2))
+    .catch(err => console.error(err))
+}
diff --git a/src/crawl-http.js b/src/crawl-http.js
index a2bf884..29c59d2 100755
--- a/src/crawl-http.js
+++ b/src/crawl-http.js
@@ -139,14 +139,20 @@ function getHTMLLinks(text) {
   })
 }
 
-async function main() {
-  let url = process.argv[2]
+async function main(args) {
+  if (args.length === 0) {
+    console.log("Usage: crawl-http http://.../example/path/ [opts]")
+    process.exit(1)
+    return
+  }
+
+  let url = args[0]
 
   let maxDownloadAttempts = 5
   let verbose = false
   let filterRegex = null
 
-  await processArgv(process.argv.slice(3), {
+  await processArgv(args.slice(1), {
     '-max-download-attempts': function(util) {
       // --max-download-attempts <max>  (alias: -m)
       // Sets the maximum number of times to attempt downloading the index for
@@ -190,9 +196,9 @@ async function main() {
   console.log(JSON.stringify(downloadedPlaylist, null, 2))
 }
 
-if (process.argv.length === 2) {
-  console.log("Usage: http-music-crawl-http http://.../example/path/ [opts]")
-} else {
-  main()
+module.exports = main
+
+if (require.main === module) {
+  main(process.argv.slice(2))
     .catch(err => console.error(err))
 }
diff --git a/src/crawl-itunes.js b/src/crawl-itunes.js
index 5cc1b55..6060ffa 100755
--- a/src/crawl-itunes.js
+++ b/src/crawl-itunes.js
@@ -93,8 +93,8 @@ async function crawl(libraryXML) {
   return resultGroup
 }
 
-async function main() {
-  const libraryPath = process.argv[2] || (
+async function main(args) {
+  const libraryPath = args[0] || (
     `${process.env.HOME}/Music/iTunes/iTunes Music Library.xml`
   )
 
@@ -130,5 +130,9 @@ async function main() {
   console.log(JSON.stringify(playlist, null, 2))
 }
 
-main()
-  .catch(err => console.error(err))
+module.exports = main
+
+if (require.main === module) {
+  main(process.argv.slice(2))
+    .catch(err => console.error(err))
+}
diff --git a/src/crawl-local.js b/src/crawl-local.js
index 6d245ae..629e015 100755
--- a/src/crawl-local.js
+++ b/src/crawl-local.js
@@ -30,13 +30,20 @@ function crawl(dirPath) {
   }).then(items => ({items}))
 }
 
-if (process.argv.length === 2) {
-  console.log("Usage: http-music-crawl-local /example/path..")
-  console.log("..or, npm run crawl-local /example/path")
-} else {
-  const path = process.argv[2]
-
-  crawl(path)
-    .then(res => console.log(JSON.stringify(res, null, 2)))
+async function main(args) {
+  if (args.length === 0) {
+    console.log("Usage: crawl-local /example/path")
+  } else {
+    const path = args[0]
+
+    const res = await crawl(path)
+    console.log(JSON.stringify(res, null, 2))
+  }
+}
+
+module.exports = main
+
+if (require.main === module) {
+  main(process.argv.slice(2))
     .catch(err => console.error(err))
 }
diff --git a/src/download-playlist.js b/src/download-playlist.js
index 18e1a7f..0b5ea58 100755
--- a/src/download-playlist.js
+++ b/src/download-playlist.js
@@ -102,17 +102,15 @@ async function downloadCrawl(topPlaylist, initialOutPath = './out/') {
   return recursive(topPlaylist.items, initialOutPath)
 }
 
-async function main() {
+async function main(args) {
   // TODO: Implement command line stuff here
 
-  if (process.argv.length === 2) {
+  if (args.length === 0) {
     console.error('Usage: download-playlist <playlistFile> [opts]')
     return
   }
 
-  const playlist = updatePlaylistFormat(
-    JSON.parse(await readFile(process.argv[2]))
-  )
+  const playlist = updatePlaylistFormat(JSON.parse(await readFile(args[0])))
 
   const outPlaylist = await downloadCrawl(playlist)
 
@@ -122,5 +120,7 @@ async function main() {
   process.exit(0)
 }
 
-main()
-  .catch(err => console.error(err))
+if (require.main === module) {
+  main(process.argv.slice(2))
+    .catch(err => console.error(err))
+}
diff --git a/src/http-music.js b/src/http-music.js
deleted file mode 100755
index 04d1f57..0000000
--- a/src/http-music.js
+++ /dev/null
@@ -1,404 +0,0 @@
-#!/usr/bin/env node
-
-'use strict'
-
-const { promisify } = require('util')
-const clone = require('clone')
-const fs = require('fs')
-const fetch = require('node-fetch')
-const pickers = require('./pickers')
-const loopPlay = require('./loop-play')
-const processArgv = require('./process-argv')
-
-const {
-  filterPlaylistByPathString, removeGroupByPathString, getPlaylistTreeString,
-  updatePlaylistFormat
-} = require('./playlist-utils')
-
-const readFile = promisify(fs.readFile)
-const writeFile = promisify(fs.writeFile)
-
-function downloadPlaylistFromURL(url) {
-  return fetch(url).then(res => res.text())
-}
-
-function downloadPlaylistFromLocalPath(path) {
-  return readFile(path)
-}
-
-function downloadPlaylistFromOptionValue(arg) {
-  // TODO: Verify things!
-  if (arg.startsWith('http://') || arg.startsWith('https://')) {
-    return downloadPlaylistFromURL(arg)
-  } else {
-    return downloadPlaylistFromLocalPath(arg)
-  }
-}
-
-// Let this forever be of use to people who run into
-// maxlistenersexceededwarning.
-process.on('warning', e => console.warn(e.stack))
-
-Promise.resolve()
-  .then(async () => {
-    let sourcePlaylist = null
-    let activePlaylist = null
-
-    let pickerType = 'shuffle'
-    let playOpts = []
-
-    // WILL play says whether the user has forced playback via an argument.
-    // SHOULD play says whether the program has automatically decided to play
-    // or not, if the user hasn't set WILL play.
-    let shouldPlay = true
-    let willPlay = null
-
-    async function openPlaylist(arg, silent = false) {
-      let playlistText
-
-      if (!silent) {
-        console.log("Opening playlist from: " + arg)
-      }
-
-      try {
-        playlistText = await downloadPlaylistFromOptionValue(arg)
-      } catch(err) {
-        if (!silent) {
-          console.error("Failed to open playlist file: " + arg)
-          console.error(err)
-        }
-
-        return false
-      }
-
-      const openedPlaylist = updatePlaylistFormat(JSON.parse(playlistText))
-
-      // The active playlist is a clone of the source playlist; after all it's
-      // quite possible we'll be messing with the value of the active playlist,
-      // and we don't want to reflect those changes in the source playlist.
-      sourcePlaylist = openedPlaylist
-      activePlaylist = clone(openedPlaylist)
-
-      processArgv(openedPlaylist.options, optionFunctions)
-    }
-
-    function requiresOpenPlaylist() {
-      if (activePlaylist === null) {
-        throw new Error(
-          "This action requires an open playlist - try --open (file)"
-        )
-      }
-    }
-
-    const optionFunctions = {
-      '-help': function(util) {
-        // --help  (alias: -h, -?)
-        // Presents a help message.
-
-        console.log('http-music\nTry man http-music!')
-
-        if (util.index === util.argv.length - 1) {
-          shouldPlay = false
-        }
-      },
-
-      'h': util => util.alias('-help'),
-      '?': util => util.alias('-help'),
-
-      '-open-playlist': async function(util) {
-        // --open-playlist <file>  (alias: --open, -o)
-        // Opens a separate playlist file.
-        // This sets the source playlist.
-
-        await openPlaylist(util.nextArg())
-      },
-
-      '-open': util => util.alias('-open-playlist'),
-      'o': util => util.alias('-open-playlist'),
-
-      '-write-playlist': function(util) {
-        // --write-playlist <file>  (alias: --write, -w, --save)
-        // Writes the active playlist to a file. This file can later be used
-        // with --open <file>; you won't need to stick in all the filtering
-        // options again.
-
-        requiresOpenPlaylist()
-
-        const playlistString = JSON.stringify(activePlaylist, null, 2)
-        const file = util.nextArg()
-
-        console.log(`Saving playlist to file ${file}...`)
-
-        return writeFile(file, playlistString).then(() => {
-          console.log("Saved.")
-
-          // If this is the last option, the user probably doesn't actually
-          // want to play the playlist. (We need to check if this is len - 2
-          // rather than len - 1, because of the <file> option that comes
-          // after --write-playlist.)
-          if (util.index === util.argv.length - 2) {
-            shouldPlay = false
-          }
-        })
-      },
-
-      '-write': util => util.alias('-write-playlist'),
-      'w': util => util.alias('-write-playlist'),
-      '-save': util => util.alias('-write-playlist'),
-
-      '-print-playlist': function(util) {
-        // --print-playlist  (alias: --log-playlist, --json)
-        // Prints out the JSON representation of the active playlist.
-
-        requiresOpenPlaylist()
-
-        console.log(JSON.stringify(activePlaylist, null, 2))
-
-        // As with --write-playlist, the user probably doesn't want to actually
-        // play anything if this is the last option.
-        if (util.index === util.argv.length - 1) {
-          shouldPlay = false
-        }
-      },
-
-      '-log-playlist': util => util.alias('-print-playlist'),
-      '-json': util => util.alias('-print-playlist'),
-
-      '-clear': function(util) {
-        // --clear  (alias: -c)
-        // Clears the active playlist. This does not affect the source
-        // playlist.
-
-        requiresOpenPlaylist()
-
-        activePlaylist.items = []
-      },
-
-      'c': util => util.alias('-clear'),
-
-      '-keep': function(util) {
-        // --keep <groupPath>  (alias: -k)
-        // Keeps a group by loading it from the source playlist into the
-        // active playlist. This is usually useful after clearing the
-        // active playlist; it can also be used to keep a subgroup when
-        // you've removed an entire parent group, e.g. `-r foo -k foo/baz`.
-
-        requiresOpenPlaylist()
-
-        const pathString = util.nextArg()
-        const group = filterPlaylistByPathString(sourcePlaylist, pathString)
-        activePlaylist.items.push(group)
-      },
-
-      'k': util => util.alias('-keep'),
-
-      '-remove': function(util) {
-        // --remove <groupPath>  (alias: -r, -x)
-        // Filters the playlist so that the given path is removed.
-
-        requiresOpenPlaylist()
-
-        const pathString = util.nextArg()
-        console.log("Ignoring path: " + pathString)
-        removeGroupByPathString(activePlaylist, pathString)
-      },
-
-      'r': util => util.alias('-remove'),
-      'x': util => util.alias('-remove'),
-
-      '-list-groups': function(util) {
-        // --list-groups  (alias: -l, --list)
-        // Lists all groups in the playlist.
-
-        requiresOpenPlaylist()
-
-        console.log(getPlaylistTreeString(activePlaylist))
-
-        // If this is the last item in the argument list, the user probably
-        // only wants to get the list, so we'll mark the 'should run' flag
-        // as false.
-        if (util.index === util.argv.length - 1) {
-          shouldPlay = false
-        }
-      },
-
-      '-list': util => util.alias('-list-groups'),
-      'l': util => util.alias('-list-groups'),
-
-      '-list-all': function(util) {
-        // --list-all  (alias: --list-tracks, -L)
-        // Lists all groups and tracks in the playlist.
-
-        requiresOpenPlaylist()
-
-        console.log(getPlaylistTreeString(activePlaylist, true))
-
-        // As with -l, if this is the last item in the argument list, we
-        // won't actually be playing the playlist.
-        if (util.index === util.argv.length - 1) {
-          shouldPlay = false
-        }
-      },
-
-      '-list-tracks': util => util.alias('-list-all'),
-      'L': util => util.alias('-list-all'),
-
-      '-play': function(util) {
-        // --play  (alias: -p)
-        // Forces the playlist to actually play.
-
-        willPlay = true
-      },
-
-      'p': util => util.alias('-play'),
-
-      '-no-play': function(util) {
-        // --no-play  (alias: -np)
-        // Forces the playlist not to play.
-
-        willPlay = false
-      },
-
-      'np': util => util.alias('-no-play'),
-
-      '-picker': function(util) {
-        // --picker <picker type>  (alias: --selector)
-        // Selects the mode that the song to play is picked.
-        // See pickers.js.
-
-        pickerType = util.nextArg()
-      },
-
-      '-selector': util => util.alias('-picker'),
-
-      '-play-opts': function(util) {
-        // --play-opts <opts>
-        // Sets command line options passed to the `play` command.
-
-        playOpts = util.nextArg().split(' ')
-      }
-    }
-
-    await openPlaylist('./playlist.json', true)
-
-    await processArgv(process.argv, optionFunctions)
-
-    if (activePlaylist === null) {
-      throw new Error(
-        "Cannot play - no open playlist. Try --open <playlist file>?"
-      )
-    }
-
-    if (willPlay || (willPlay === null && shouldPlay)) {
-      let picker
-      if (pickerType === 'shuffle') {
-        console.log("Using shuffle picker.")
-        picker = pickers.makeShufflePlaylistPicker(activePlaylist)
-      } else if (pickerType === 'ordered') {
-        console.log("Using ordered picker.")
-        picker = pickers.makeOrderedPlaylistPicker(activePlaylist)
-      } else {
-        console.error("Invalid picker type: " + pickerType)
-        return
-      }
-
-      const {
-        promise: playPromise,
-        playController: play,
-        downloadController
-      } = loopPlay(picker, playOpts)
-
-      // We're looking to gather standard input one keystroke at a time.
-      process.stdin.setRawMode(true)
-
-      process.stdin.on('data', data => {
-        const escModifier = Buffer.from('\x1b[')
-        const shiftModifier = Buffer.from('1;2')
-
-        const esc = num => Buffer.concat([escModifier, Buffer.from([num])])
-
-        const shiftEsc = num => (
-          Buffer.concat([escModifier, shiftModifier, Buffer.from([num])])
-        )
-
-        if (Buffer.from([0x20]).equals(data)) {
-          play.togglePause()
-        }
-
-        if (esc(0x43).equals(data)) {
-          play.seekAhead(5)
-        }
-
-        if (esc(0x44).equals(data)) {
-          play.seekBack(5)
-        }
-
-        if (shiftEsc(0x43).equals(data)) {
-          play.seekAhead(30)
-        }
-
-        if (shiftEsc(0x44).equals(data)) {
-          play.seekBack(30)
-        }
-
-        if (esc(0x41).equals(data)) {
-          play.volUp(10)
-        }
-
-        if (esc(0x42).equals(data)) {
-          play.volDown(10)
-        }
-
-        if (Buffer.from('s').equals(data)) {
-          clearConsoleLine()
-          console.log(
-            "Skipping the track that's currently playing. " +
-            "(Press I for track info!)"
-          )
-
-          play.skip()
-        }
-
-        if (Buffer.from([0x7f]).equals(data)) {
-          clearConsoleLine()
-          console.log(
-            "Skipping the track that's up next. " +
-            "(Press I for track info!)"
-          )
-
-          // TODO: It would be nice to have this as a method of
-          // PlayController.
-          // Double TODO: This doesn't actually work!!
-          downloadController.cancel()
-          play.startNextDownload()
-        }
-
-        if (
-          Buffer.from('i').equals(data) ||
-          Buffer.from('t').equals(data)
-        ) {
-          clearConsoleLine()
-          play.logTrackInfo()
-        }
-
-        if (
-          Buffer.from('q').equals(data) ||
-          Buffer.from([0x03]).equals(data) || // ^C
-          Buffer.from([0x04]).equals(data) // ^D
-        ) {
-          play.kill()
-          process.stdout.write('\n')
-          process.exit(0)
-        }
-      })
-
-      return playPromise
-    } else {
-      return activePlaylist
-    }
-  })
-  .catch(err => console.error(err))
-
-function clearConsoleLine() {
-  process.stdout.write('\x1b[1K\r')
-}
diff --git a/src/loop-play.js b/src/loop-play.js
index 7457b41..884d3cb 100644
--- a/src/loop-play.js
+++ b/src/loop-play.js
@@ -1,3 +1,5 @@
+// This isn't actually the code for the `play` command! That's in `play.js`.
+
 'use strict'
 
 const { spawn } = require('child_process')
diff --git a/src/play.js b/src/play.js
new file mode 100755
index 0000000..9515d97
--- /dev/null
+++ b/src/play.js
@@ -0,0 +1,412 @@
+#!/usr/bin/env node
+
+'use strict'
+
+const { promisify } = require('util')
+const clone = require('clone')
+const fs = require('fs')
+const fetch = require('node-fetch')
+const pickers = require('./pickers')
+const loopPlay = require('./loop-play')
+const processArgv = require('./process-argv')
+
+const {
+  filterPlaylistByPathString, removeGroupByPathString, getPlaylistTreeString,
+  updatePlaylistFormat
+} = require('./playlist-utils')
+
+const readFile = promisify(fs.readFile)
+const writeFile = promisify(fs.writeFile)
+
+function downloadPlaylistFromURL(url) {
+  return fetch(url).then(res => res.text())
+}
+
+function downloadPlaylistFromLocalPath(path) {
+  return readFile(path)
+}
+
+function downloadPlaylistFromOptionValue(arg) {
+  // TODO: Verify things!
+  if (arg.startsWith('http://') || arg.startsWith('https://')) {
+    return downloadPlaylistFromURL(arg)
+  } else {
+    return downloadPlaylistFromLocalPath(arg)
+  }
+}
+
+function clearConsoleLine() {
+  process.stdout.write('\x1b[1K\r')
+}
+
+async function main(args) {
+  let sourcePlaylist = null
+  let activePlaylist = null
+
+  let pickerType = 'shuffle'
+  let playOpts = []
+
+  // WILL play says whether the user has forced playback via an argument.
+  // SHOULD play says whether the program has automatically decided to play
+  // or not, if the user hasn't set WILL play.
+  let shouldPlay = true
+  let willPlay = null
+
+  async function openPlaylist(arg, silent = false) {
+    let playlistText
+
+    if (!silent) {
+      console.log("Opening playlist from: " + arg)
+    }
+
+    try {
+      playlistText = await downloadPlaylistFromOptionValue(arg)
+    } catch(err) {
+      if (!silent) {
+        console.error("Failed to open playlist file: " + arg)
+        console.error(err)
+      }
+
+      return false
+    }
+
+    const openedPlaylist = updatePlaylistFormat(JSON.parse(playlistText))
+
+    // The active playlist is a clone of the source playlist; after all it's
+    // quite possible we'll be messing with the value of the active playlist,
+    // and we don't want to reflect those changes in the source playlist.
+    sourcePlaylist = openedPlaylist
+    activePlaylist = clone(openedPlaylist)
+
+    processArgv(openedPlaylist.options, optionFunctions)
+  }
+
+  function requiresOpenPlaylist() {
+    if (activePlaylist === null) {
+      throw new Error(
+        "This action requires an open playlist - try --open (file)"
+      )
+    }
+  }
+
+  const optionFunctions = {
+    '-help': function(util) {
+      // --help  (alias: -h, -?)
+      // Presents a help message.
+
+      console.log('http-music\nTry man http-music!')
+
+      if (util.index === util.argv.length - 1) {
+        shouldPlay = false
+      }
+    },
+
+    'h': util => util.alias('-help'),
+    '?': util => util.alias('-help'),
+
+    '-open-playlist': async function(util) {
+      // --open-playlist <file>  (alias: --open, -o)
+      // Opens a separate playlist file.
+      // This sets the source playlist.
+
+      await openPlaylist(util.nextArg())
+    },
+
+    '-open': util => util.alias('-open-playlist'),
+    'o': util => util.alias('-open-playlist'),
+
+    '-write-playlist': function(util) {
+      // --write-playlist <file>  (alias: --write, -w, --save)
+      // Writes the active playlist to a file. This file can later be used
+      // with --open <file>; you won't need to stick in all the filtering
+      // options again.
+
+      requiresOpenPlaylist()
+
+      const playlistString = JSON.stringify(activePlaylist, null, 2)
+      const file = util.nextArg()
+
+      console.log(`Saving playlist to file ${file}...`)
+
+      return writeFile(file, playlistString).then(() => {
+        console.log("Saved.")
+
+        // If this is the last option, the user probably doesn't actually
+        // want to play the playlist. (We need to check if this is len - 2
+        // rather than len - 1, because of the <file> option that comes
+        // after --write-playlist.)
+        if (util.index === util.argv.length - 2) {
+          shouldPlay = false
+        }
+      })
+    },
+
+    '-write': util => util.alias('-write-playlist'),
+    'w': util => util.alias('-write-playlist'),
+    '-save': util => util.alias('-write-playlist'),
+
+    '-print-playlist': function(util) {
+      // --print-playlist  (alias: --log-playlist, --json)
+      // Prints out the JSON representation of the active playlist.
+
+      requiresOpenPlaylist()
+
+      console.log(JSON.stringify(activePlaylist, null, 2))
+
+      // As with --write-playlist, the user probably doesn't want to actually
+      // play anything if this is the last option.
+      if (util.index === util.argv.length - 1) {
+        shouldPlay = false
+      }
+    },
+
+    '-log-playlist': util => util.alias('-print-playlist'),
+    '-json': util => util.alias('-print-playlist'),
+
+    '-clear': function(util) {
+      // --clear  (alias: -c)
+      // Clears the active playlist. This does not affect the source
+      // playlist.
+
+      requiresOpenPlaylist()
+
+      activePlaylist.items = []
+    },
+
+    'c': util => util.alias('-clear'),
+
+    '-keep': function(util) {
+      // --keep <groupPath>  (alias: -k)
+      // Keeps a group by loading it from the source playlist into the
+      // active playlist. This is usually useful after clearing the
+      // active playlist; it can also be used to keep a subgroup when
+      // you've removed an entire parent group, e.g. `-r foo -k foo/baz`.
+
+      requiresOpenPlaylist()
+
+      const pathString = util.nextArg()
+      const group = filterPlaylistByPathString(sourcePlaylist, pathString)
+      activePlaylist.items.push(group)
+    },
+
+    'k': util => util.alias('-keep'),
+
+    '-remove': function(util) {
+      // --remove <groupPath>  (alias: -r, -x)
+      // Filters the playlist so that the given path is removed.
+
+      requiresOpenPlaylist()
+
+      const pathString = util.nextArg()
+      console.log("Ignoring path: " + pathString)
+      removeGroupByPathString(activePlaylist, pathString)
+    },
+
+    'r': util => util.alias('-remove'),
+    'x': util => util.alias('-remove'),
+
+    '-list-groups': function(util) {
+      // --list-groups  (alias: -l, --list)
+      // Lists all groups in the playlist.
+
+      requiresOpenPlaylist()
+
+      console.log(getPlaylistTreeString(activePlaylist))
+
+      // If this is the last item in the argument list, the user probably
+      // only wants to get the list, so we'll mark the 'should run' flag
+      // as false.
+      if (util.index === util.argv.length - 1) {
+        shouldPlay = false
+      }
+    },
+
+    '-list': util => util.alias('-list-groups'),
+    'l': util => util.alias('-list-groups'),
+
+    '-list-all': function(util) {
+      // --list-all  (alias: --list-tracks, -L)
+      // Lists all groups and tracks in the playlist.
+
+      requiresOpenPlaylist()
+
+      console.log(getPlaylistTreeString(activePlaylist, true))
+
+      // As with -l, if this is the last item in the argument list, we
+      // won't actually be playing the playlist.
+      if (util.index === util.argv.length - 1) {
+        shouldPlay = false
+      }
+    },
+
+    '-list-tracks': util => util.alias('-list-all'),
+    'L': util => util.alias('-list-all'),
+
+    '-play': function(util) {
+      // --play  (alias: -p)
+      // Forces the playlist to actually play.
+
+      willPlay = true
+    },
+
+    'p': util => util.alias('-play'),
+
+    '-no-play': function(util) {
+      // --no-play  (alias: -np)
+      // Forces the playlist not to play.
+
+      willPlay = false
+    },
+
+    'np': util => util.alias('-no-play'),
+
+    '-picker': function(util) {
+      // --picker <picker type>  (alias: --selector)
+      // Selects the mode that the song to play is picked.
+      // See pickers.js.
+
+      pickerType = util.nextArg()
+    },
+
+    '-selector': util => util.alias('-picker'),
+
+    '-play-opts': function(util) {
+      // --play-opts <opts>
+      // Sets command line options passed to the `play` command.
+
+      playOpts = util.nextArg().split(' ')
+    }
+  }
+
+  await openPlaylist('./playlist.json', true)
+
+  await processArgv(args, optionFunctions)
+
+  if (activePlaylist === null) {
+    throw new Error(
+      "Cannot play - no open playlist. Try --open <playlist file>?"
+    )
+  }
+
+  if (willPlay || (willPlay === null && shouldPlay)) {
+    let picker
+    if (pickerType === 'shuffle') {
+      console.log("Using shuffle picker.")
+      picker = pickers.makeShufflePlaylistPicker(activePlaylist)
+    } else if (pickerType === 'ordered') {
+      console.log("Using ordered picker.")
+      picker = pickers.makeOrderedPlaylistPicker(activePlaylist)
+    } else {
+      console.error("Invalid picker type: " + pickerType)
+      return
+    }
+
+    const {
+      promise: playPromise,
+      playController: play,
+      downloadController
+    } = loopPlay(picker, playOpts)
+
+    // We're looking to gather standard input one keystroke at a time.
+    // But that isn't *always* possible, e.g. when piping into the http-music
+    // command through the shell.
+    if ('setRawMode' in process.stdin) {
+      process.stdin.setRawMode(true)
+    } else {
+      console.warn("User input cannot be gotten!")
+      console.warn("If you're piping into http-music, this is normal.")
+    }
+
+    process.stdin.on('data', data => {
+      const escModifier = Buffer.from('\x1b[')
+      const shiftModifier = Buffer.from('1;2')
+
+      const esc = num => Buffer.concat([escModifier, Buffer.from([num])])
+
+      const shiftEsc = num => (
+        Buffer.concat([escModifier, shiftModifier, Buffer.from([num])])
+      )
+
+      if (Buffer.from([0x20]).equals(data)) {
+        play.togglePause()
+      }
+
+      if (esc(0x43).equals(data)) {
+        play.seekAhead(5)
+      }
+
+      if (esc(0x44).equals(data)) {
+        play.seekBack(5)
+      }
+
+      if (shiftEsc(0x43).equals(data)) {
+        play.seekAhead(30)
+      }
+
+      if (shiftEsc(0x44).equals(data)) {
+        play.seekBack(30)
+      }
+
+      if (esc(0x41).equals(data)) {
+        play.volUp(10)
+      }
+
+      if (esc(0x42).equals(data)) {
+        play.volDown(10)
+      }
+
+      if (Buffer.from('s').equals(data)) {
+        clearConsoleLine()
+        console.log(
+          "Skipping the track that's currently playing. " +
+          "(Press I for track info!)"
+        )
+
+        play.skip()
+      }
+
+      if (Buffer.from([0x7f]).equals(data)) {
+        clearConsoleLine()
+        console.log(
+          "Skipping the track that's up next. " +
+          "(Press I for track info!)"
+        )
+
+        // TODO: It would be nice to have this as a method of
+        // PlayController.
+        // Double TODO: This doesn't actually work!!
+        downloadController.cancel()
+        play.startNextDownload()
+      }
+
+      if (
+        Buffer.from('i').equals(data) ||
+        Buffer.from('t').equals(data)
+      ) {
+        clearConsoleLine()
+        play.logTrackInfo()
+      }
+
+      if (
+        Buffer.from('q').equals(data) ||
+        Buffer.from([0x03]).equals(data) || // ^C
+        Buffer.from([0x04]).equals(data) // ^D
+      ) {
+        play.kill()
+        process.stdout.write('\n')
+        process.exit(0)
+      }
+    })
+
+    return playPromise
+  } else {
+    return activePlaylist
+  }
+}
+
+module.exports = main
+
+if (require.main === module) {
+  main(process.argv.slice(2))
+    .catch(err => console.error(err))
+}