« get me outta code hell

http-music - Command-line music player + utils (not a server!)
about summary refs log tree commit diff
path: root/src/play.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/play.js')
-rwxr-xr-xsrc/play.js349
1 files changed, 64 insertions, 285 deletions
diff --git a/src/play.js b/src/play.js
index 30a151e..dd49afe 100755
--- a/src/play.js
+++ b/src/play.js
@@ -4,7 +4,6 @@
 
 const { promisify } = require('util')
 const { spawn } = require('child_process')
-const clone = require('clone')
 const fs = require('fs')
 const fetch = require('node-fetch')
 const commandExists = require('./command-exists')
@@ -12,20 +11,9 @@ const startLoopPlay = require('./loop-play')
 const processArgv = require('./process-argv')
 const promisifyProcess = require('./promisify-process')
 const { processSmartPlaylist } = require('./smart-playlist')
-
-const {
-  filterPlaylistByPathString, removeGroupByPathString, getPlaylistTreeString,
-  updatePlaylistFormat, collapseGrouplike, filterGrouplikeByProperty, isTrack,
-  flattenGrouplike
-} = require('./playlist-utils')
-
-const {
-  downloadPlaylistFromOptionValue
-} = require('./general-util')
-
-const {
-  compileKeybindings, getComboForCommand, stringifyCombo
-} = require('./keybinder')
+const { filterPlaylistByPathString, isTrack, flattenGrouplike } = require('./playlist-utils')
+const { compileKeybindings, getComboForCommand, stringifyCombo } = require('./keybinder')
+const { makePlaylistOptions } = require('./general-util')
 
 const readFile = promisify(fs.readFile)
 const writeFile = promisify(fs.writeFile)
@@ -60,7 +48,6 @@ async function determineDefaultConverter() {
 
 async function main(args) {
   let sourcePlaylist = null
-  let activePlaylist = null
 
   let pickerSortMode = 'shuffle'
   let pickerLoopMode = 'loop-regenerate'
@@ -93,16 +80,6 @@ async function main(args) {
   // The file to output the playlist path to the current file.
   let trackDisplayFile
 
-  // Whether or not a playlist has been opened yet. This is just used to
-  // decide when exactly to load the default playlist. (We don't want to load
-  // it as soon as the process starts, since there might be an --open-playlist
-  // option that specifies opening a *different* playlist! But if we encounter
-  // an action that requires a playlist, and no playlist has yet been opened,
-  // we assume that the user probably wants to do something with the default
-  // playlist, and that's when we open it. See requiresOpenPlaylist for the
-  // implementation of this.)
-  let hasOpenedPlaylist = false
-
   const keybindings = [
     [['space'], 'togglePause'],
     [['left'], 'seek', -5],
@@ -119,59 +96,6 @@ async function main(args) {
     [['q'], 'quit'], [['Q'], 'quit']
   ]
 
-  async function openPlaylist(arg, silent = false) {
-    // Takes a playlist download argument and loads it as the source and
-    // active playlist.
-
-    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 importedPlaylist = JSON.parse(playlistText)
-
-    hasOpenedPlaylist = true
-
-    await loadPlaylist(importedPlaylist)
-  }
-
-  async function loadPlaylist(importedPlaylist) {
-    // Takes an actual playlist object and sets it up as the source and active
-    // playlist.
-
-    const openedPlaylist = updatePlaylistFormat(importedPlaylist)
-
-    // We also want to de-smart-ify (stupidify? - simplify?) the playlist.
-    const processedPlaylist = await processSmartPlaylist(openedPlaylist)
-
-    // ..And finally, we have to update the playlist format again, since
-    // processSmartPlaylist might have added new (un-updated) items:
-    const finalPlaylist = updatePlaylistFormat(processedPlaylist, true)
-    // We also pass true so that the playlist-format-updater knows that this
-    // is the source playlist.
-
-    sourcePlaylist = finalPlaylist
-
-    // 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.
-    activePlaylist = clone(sourcePlaylist)
-
-    await processArgv(processedPlaylist.options, optionFunctions)
-  }
-
   async function openKeybindings(arg, add = true) {
     console.log("Opening keybindings from: " + arg)
 
@@ -198,95 +122,36 @@ async function main(args) {
     keybindings.unshift(...openedKeybindings)
   }
 
-  async function requiresOpenPlaylist() {
-    if (activePlaylist === null) {
-      if (hasOpenedPlaylist === false) {
-        await openDefaultPlaylist()
-      } else {
-        throw new Error(
-          "This action requires an open playlist - try --open (file)"
-        )
-      }
-    }
-  }
+  const {
+    optionFunctions, getStuff,
+    openDefaultPlaylist
+  } = await makePlaylistOptions()
 
-  function openDefaultPlaylist() {
-    return openPlaylist('./playlist.json', true)
-  }
+  const {
+    '-write-playlist': originalWritePlaylist,
+    '-print-playlist': originalPrintPlaylist,
+    '-list-groups': originalListGroups,
+    '-list-all': originalListAll
+  } = optionFunctions
 
-  const optionFunctions = {
-    '-help': function(util) {
-      // --help  (alias: -h, -?)
-      // Presents a help message.
+  Object.assign(optionFunctions, {
 
-      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'),
-
-    '-open-playlist-string': async function(util) {
-      // --open-playlist-string <string>
-      // Opens a playlist, using the given string as the JSON text of the
-      // playlist. This sets the source playlist.
-
-      await loadPlaylist(JSON.parse(util.nextArg()))
-    },
-
-    '-playlist-string': util => util.alias('-open-playlist-string'),
+    // Extra play-specific behavior -------------------------------------------
 
     '-write-playlist': async 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.
-
-      await requiresOpenPlaylist()
-
-      const playlistString = JSON.stringify(activePlaylist, null, 2)
-      const file = util.nextArg()
-
-      console.log(`Saving playlist to file ${file}...`)
+      await originalWritePlaylist(util)
 
-      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
-        }
-      })
+      // 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': async function(util) {
-      // --print-playlist  (alias: --log-playlist, --json)
-      // Prints out the JSON representation of the active playlist.
-
-      await requiresOpenPlaylist()
-
-      console.log(JSON.stringify(activePlaylist, null, 2))
+      await originalPrintPlaylist(util)
 
       // As with --write-playlist, the user probably doesn't want to actually
       // play anything if this is the last option.
@@ -295,152 +160,57 @@ async function main(args) {
       }
     },
 
-    '-log-playlist': util => util.alias('-print-playlist'),
-    '-json': util => util.alias('-print-playlist'),
-
-    // Add appends the keybindings to the existing keybindings; import replaces
-    // the current ones with the opened ones.
-
-    '-add-keybindings': async function(util) {
-      await openKeybindings(util.nextArg())
-    },
-
-    '-open-keybindings': util => util.alias('-add-keybindings'),
-
-    '-import-keybindings': async function(util) {
-      await openKeybindings(util.nextArg(), false)
-    },
-
-    '-clear': async function(util) {
-      // --clear  (alias: -c)
-      // Clears the active playlist. This does not affect the source
-      // playlist.
-
-      await requiresOpenPlaylist()
-
-      activePlaylist.items = []
-    },
-
-    'c': util => util.alias('-clear'),
-
-    '-keep': async 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`.
-
-      await requiresOpenPlaylist()
-
-      const pathString = util.nextArg()
-      const group = filterPlaylistByPathString(sourcePlaylist, pathString)
+    '-list-groups': async function(util) {
+      await originalListGroups(util)
 
-      if (group) {
-        activePlaylist.items.push(group)
+      // 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
       }
     },
 
-    'k': util => util.alias('-keep'),
-
-    '-remove': async function(util) {
-      // --remove <groupPath>  (alias: -r, -x)
-      // Filters the playlist so that the given path is removed.
-
-      await requiresOpenPlaylist()
-
-      const pathString = util.nextArg()
-      console.log("Ignoring path: " + pathString)
-      removeGroupByPathString(activePlaylist, pathString)
-    },
-
-    'r': util => util.alias('-remove'),
-    'x': util => util.alias('-remove'),
-
-    '-filter': async function(util) {
-      // --filter <filterJSON>
-      // Filters the playlist so that only tracks that match the given filter
-      // are kept. FilterJSON should be a JSON object as described in the
-      // man page section "filters".
 
-      const filterJSON = util.nextArg()
+    '-list-all': async function(util) {
+      await originalListAll(util)
 
-      let filterObj
-      try {
-        filterObj = JSON.parse(filterJSON)
-      } catch (error) {
-        console.error('Invalid JSON for filter:', filterJSON)
-        return
+      // 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
       }
-
-      activePlaylist.filters = [filterObj]
-      activePlaylist = await processSmartPlaylist(activePlaylist)
-      activePlaylist = updatePlaylistFormat(activePlaylist)
-    },
-
-    'f': util => util.alias('-filter'),
-
-    '-collapse-groups': async function() {
-      // --collapse-groups  (alias: --collapse)
-      // Collapses groups in the active playlist so that there is only one
-      // level of sub-groups. Handy for shuffling the order groups play in;
-      // try `--collapse-groups --sort shuffle-groups`.
-
-      await requiresOpenPlaylist()
-
-      activePlaylist = updatePlaylistFormat(collapseGrouplike(activePlaylist))
     },
 
-    '-collapse': util => util.alias('-collapse-groups'),
+    // Other options, specific to play ----------------------------------------
 
-    '-flatten-tracks': async function() {
-      // --flatten-tracks  (alias: --flatten)
-      // Flattens the entire active playlist, so that only tracks remain,
-      // and there are no groups.
-
-      await requiresOpenPlaylist()
-
-      activePlaylist = updatePlaylistFormat(flattenGrouplike(activePlaylist))
-    },
-
-    '-flatten': util => util.alias('-flatten-tracks'),
-
-    '-list-groups': async function(util) {
-      // --list-groups  (alias: -l, --list)
-      // Lists all groups in the playlist.
-
-      await requiresOpenPlaylist()
+    '-help': function(util) {
+      // --help  (alias: -h, -?)
+      // Presents a help message.
 
-      console.log(getPlaylistTreeString(activePlaylist))
+      console.log('http-music\nTry man http-music!')
 
-      // 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'),
+    'h': util => util.alias('-help'),
+    '?': util => util.alias('-help'),
 
-    '-list-all': async function(util) {
-      // --list-all  (alias: --list-tracks, -L)
-      // Lists all groups and tracks in the playlist.
+    // Add appends the keybindings to the existing keybindings; import replaces
+    // the current ones with the opened ones.
 
-      await requiresOpenPlaylist()
+    '-add-keybindings': async function(util) {
+      await openKeybindings(util.nextArg())
+    },
 
-      console.log(getPlaylistTreeString(activePlaylist, true))
+    '-open-keybindings': util => util.alias('-add-keybindings'),
 
-      // 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
-      }
+    '-import-keybindings': async function(util) {
+      await openKeybindings(util.nextArg(), false)
     },
 
-    '-list-tracks': util => util.alias('-list-all'),
-    'L': util => util.alias('-list-all'),
-
     '-list-keybindings': function() {
       console.log('Keybindings:')
 
@@ -513,8 +283,13 @@ async function main(args) {
       // Sets the first track to be played.
       // This is especially useful when using an ordered sort; this option
       // could be used to start a long album part way through.
+
       const pathString = util.nextArg()
-      const track = filterPlaylistByPathString(activePlaylist, pathString)
+
+      const track = filterPlaylistByPathString(
+        getStuff.activePlaylist, pathString
+      )
+
       if (isTrack(track)) {
         startTrack = track
         console.log('Starting on track', pathString)
@@ -647,14 +422,18 @@ async function main(args) {
     },
 
     '-trust': util => util.alias('-trust-shell-commands')
-  }
+  })
 
   await processArgv(args, optionFunctions)
 
-  if (!hasOpenedPlaylist) {
+  if (!getStuff.hasOpenedPlaylist) {
     await openDefaultPlaylist()
   }
 
+  // All done processing: let's actually grab the active playlist, which
+  // we'll quickly validate and then play (if it contains tracks).
+  const activePlaylist = getStuff.activePlaylist
+
   if (activePlaylist === null) {
     console.error(
       "Cannot play - no open playlist. Try --open <playlist file>?"