« get me outta code hell

http-music - Command-line music player + utils (not a server!)
about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--man/http-music-crawl-http.12
-rw-r--r--man/http-music-setup.115
-rw-r--r--man/http-music.17
-rwxr-xr-xsrc/cli.js1
-rwxr-xr-xsrc/crawl-http.js9
-rwxr-xr-xsrc/crawl-itunes.js9
-rwxr-xr-xsrc/crawl-local.js12
-rw-r--r--src/crawl-youtube.js11
-rwxr-xr-xsrc/play.js7
-rw-r--r--src/setup.js242
-rw-r--r--yarn.lock13
11 files changed, 311 insertions, 17 deletions
diff --git a/man/http-music-crawl-http.1 b/man/http-music-crawl-http.1
index 9202c65..1f96fc6 100644
--- a/man/http-music-crawl-http.1
+++ b/man/http-music-crawl-http.1
@@ -4,7 +4,7 @@
 http-music-crawl-http - create a playlist file using an HTTP-based directory listing
 
 .SH SYNOPSIS
-.B http-music-crawl-http
+.B http-music crawl-http
 <download URL>
 [opts...]
 
diff --git a/man/http-music-setup.1 b/man/http-music-setup.1
new file mode 100644
index 0000000..c65d3c7
--- /dev/null
+++ b/man/http-music-setup.1
@@ -0,0 +1,15 @@
+.TH HTTP-MUSIC-SETUP 1
+
+.SH NAME
+http-music-setup - interactively create an http-music playlist
+
+.SH SYNOPSIS
+.B http-music setup
+
+.SH DESCRIPTION
+\fBhttp-music-setup\fR is a command line utility used to interactively generate a playlist.json file, which can be used with \fBhttp-music-play\fR.
+Once customization finishes, the playlist is generated and saved.
+Alternatively, the user can choose to see the crawler-based command used to generate the playlist file.
+
+.SH EXAMPLES
+Simply use \fBhttp-music setup\fR to get started.
diff --git a/man/http-music.1 b/man/http-music.1
index c2787c6..0735901 100644
--- a/man/http-music.1
+++ b/man/http-music.1
@@ -30,6 +30,10 @@ They are as listed below; each sub-command also has its own \fBman\fR page (for
 Plays a playlist (from the playlist.json file, by default).
 
 .TP
+.BR setup
+Presents a simple wizard to walk through customizing a playlist.json file, which is automatically generated and saved.
+
+.TP
 .BR crawl-http
 Creates a playlist from each music file linked to from an HTML page online.
 
@@ -49,7 +53,6 @@ Creates a playlist from a YouTube playlist URL.
 .BR download-playlist
 Downloads each item in a playlist into a directory.
 
-
-
 .SH EXAMPLES
 See \fBhttp-music-play\fR(1).
+It's recommended to use \fBhttp-music setup\fR to get started.
diff --git a/src/cli.js b/src/cli.js
index 9b21395..9021cc6 100755
--- a/src/cli.js
+++ b/src/cli.js
@@ -24,6 +24,7 @@ async function main(args) {
       case 'play': script = require('./play'); break
       case 'download-playlist': script = require('./download-playlist'); break
       case 'smart-playlist': script = require('./smart-playlist'); break
+      case 'setup': script = require('./setup'); break
 
       default:
         console.error(`Invalid command "${args[0]}" provided.`)
diff --git a/src/crawl-http.js b/src/crawl-http.js
index 4925f12..d3e1533 100755
--- a/src/crawl-http.js
+++ b/src/crawl-http.js
@@ -147,7 +147,7 @@ function getHTMLLinks(text) {
   })
 }
 
-async function main(args) {
+async function main(args, shouldReturn = false) {
   if (args.length === 0) {
     console.log("Usage: crawl-http http://.../example/path/ [opts]")
     return
@@ -202,7 +202,12 @@ async function main(args) {
     filterRegex: filterRegex
   })
 
-  console.log(JSON.stringify(downloadedPlaylist, null, 2))
+  const str = JSON.stringify(downloadedPlaylist, null, 2)
+  if (shouldReturn) {
+    return str
+  } else {
+    console.log(str)
+  }
 }
 
 module.exports = {main, crawl}
diff --git a/src/crawl-itunes.js b/src/crawl-itunes.js
index e6b63b3..ce9ebf9 100755
--- a/src/crawl-itunes.js
+++ b/src/crawl-itunes.js
@@ -109,7 +109,7 @@ async function crawl(
   return resultGroup
 }
 
-async function main(args) {
+async function main(args, shouldReturn = false) {
   let playlist
 
   try {
@@ -137,7 +137,12 @@ async function main(args) {
     }
   }
 
-  console.log(JSON.stringify(playlist, null, 2))
+  const str = JSON.stringify(playlist, null, 2)
+  if (shouldReturn) {
+    return str
+  } else {
+    console.log(str)
+  }
 }
 
 module.exports = {main, crawl}
diff --git a/src/crawl-local.js b/src/crawl-local.js
index 91554af..3134193 100755
--- a/src/crawl-local.js
+++ b/src/crawl-local.js
@@ -56,7 +56,7 @@ function crawl(dirPath, extensions = [
     .then(filteredItems => ({items: filteredItems}))
 }
 
-async function main(args) {
+async function main(args, shouldReturn = false) {
   if (args.length === 0) {
     console.log("Usage: crawl-local /example/path [opts]")
     return
@@ -86,8 +86,14 @@ async function main(args) {
     'e': util => util.alias('-extensions')
   })
 
-  const res = await crawl(path, extensions)
-  console.log(JSON.stringify(res, null, 2))
+  const playlist = await crawl(path, extensions)
+
+  const str = JSON.stringify(playlist, null, 2)
+  if (shouldReturn) {
+    return str
+  } else {
+    console.log(str)
+  }
 }
 
 module.exports = {main, crawl}
diff --git a/src/crawl-youtube.js b/src/crawl-youtube.js
index 4b4c66c..38a531a 100644
--- a/src/crawl-youtube.js
+++ b/src/crawl-youtube.js
@@ -31,13 +31,20 @@ async function crawl(url) {
   }
 }
 
-async function main(args) {
+async function main(args, shouldReturn = false) {
   // TODO: Error message if none is passed.
 
   if (args.length === 0) {
     console.error("Usage: crawl-youtube <playlist URL>")
+    return
+  }
+
+  const playlist = await crawl(args[0])
+  const str = JSON.stringify(playlist, null, 2)
+  if (shouldReturn) {
+    return str
   } else {
-    console.log(JSON.stringify(await crawl(args[0]), null, 2))
+    console.log(str)
   }
 }
 
diff --git a/src/play.js b/src/play.js
index c754836..ac6a232 100755
--- a/src/play.js
+++ b/src/play.js
@@ -544,9 +544,14 @@ async function main(args) {
   await processArgv(args, optionFunctions)
 
   if (activePlaylist === null) {
-    throw new Error(
+    console.error(
       "Cannot play - no open playlist. Try --open <playlist file>?"
     )
+    console.error(
+      "You could also try \x1b[1mhttp-music setup\x1b[0m to easily " +
+      "create a playlist file!"
+    )
+    return false
   }
 
   if (willPlay || (willPlay === null && shouldPlay)) {
diff --git a/src/setup.js b/src/setup.js
new file mode 100644
index 0000000..15aa2f2
--- /dev/null
+++ b/src/setup.js
@@ -0,0 +1,242 @@
+'use strict'
+
+const readline = require('readline')
+const path = require('path')
+const util = require('util')
+const fs = require('fs')
+const { getCrawlerByName } = require('./crawlers')
+
+const writeFile = util.promisify(fs.writeFile)
+const access = util.promisify(fs.access)
+
+async function exists(file) {
+  try {
+    await access(file)
+    return true
+  } catch(err) {
+    return false
+  }
+}
+
+function prompt(rl, promptMessage = '', defaultChoice = null, options = {}) {
+  return new Promise((resolve, reject) => {
+    const hasOptions = (Object.keys(options).length > 0)
+
+    console.log('')
+
+    if (hasOptions) {
+      for (const [ option, message ] of Object.entries(options)) {
+        if (option === defaultChoice) {
+          console.log(`  [${option.toUpperCase()} (default)]: ${message}`)
+        } else {
+          console.log(`  [${option.toLowerCase()}]: ${message}`)
+        }
+      }
+      console.log('')
+    }
+
+    let promptStr = ''
+
+    if (promptMessage) {
+      promptStr += promptMessage + ' '
+    }
+
+    if (hasOptions) {
+      promptStr += '['
+      promptStr += Object.keys(options).map(option => {
+        if (option === defaultChoice) {
+          return option.toUpperCase()
+        } else {
+          return option.toLowerCase()
+        }
+      }).join('/')
+      promptStr += ']'
+    } else if (defaultChoice) {
+      promptStr += `[default: ${defaultChoice}]`
+    }
+
+    promptStr += '> '
+
+    rl.question(promptStr, choice => {
+      toRepeat: {
+        if (choice.length === 0 && defaultChoice) {
+          resolve(defaultChoice)
+        } else if (
+          hasOptions && Object.keys(options).includes(choice.toLowerCase())
+        ) {
+          resolve(choice.toLowerCase())
+        } else if (choice.length > 0 && !hasOptions) {
+          resolve(choice)
+        } else {
+          break toRepeat
+        }
+
+        console.log('')
+        return
+      }
+
+      resolve(prompt(rl, promptMessage, defaultChoice, options))
+    })
+  })
+}
+
+async function setupTool() {
+  const rl = readline.createInterface({
+    input: process.stdin,
+    output: process.stdout
+  })
+
+  console.log('Which source would you like to play music from?')
+
+  const wd = process.cwd()
+
+  const crawlerCommand = {
+    l: 'crawl-local',
+    h: 'crawl-http'
+  }[await prompt(rl, 'Which source?', 'l', {
+    l: 'Files on this local computer.',
+    h: 'Downloadable files linked from a page on the web.'
+  })]
+
+  const crawlerOptions = []
+
+  if (crawlerCommand === 'crawl-local') {
+    console.log('What directory would you like to download music from?')
+    console.log(`(Your current working directory is: ${wd})`)
+
+    crawlerOptions.push(await prompt(rl, 'What directory path?', '.'))
+  }
+
+  if (crawlerCommand === 'crawl-http') {
+    console.log('What URL would you like to download music from?')
+    console.log('(This only works if the actual song files are linked; you')
+    console.log("can't, for example, give a Bandcamp album link here.")
+
+    crawlerOptions.push(await prompt(rl, 'What URL?'))
+  }
+
+  console.log('Would you like http-music to automatically process your')
+  console.log('playlist to find out which music to play every time?')
+  console.log('This is handy if you expect new music to be added to your')
+  console.log('source often (e.g. a folder you frequently add new music')
+  console.log('to, or a webpage that often has new links added to it).')
+  console.log('')
+  console.log('If you choose this, http-music may take longer to run.')
+  console.log('(If you are loading music from a webpage, then the amount of')
+  console.log("time you'll have to wait depends on your internet connection;")
+  console.log('if you are loading files from your own computer, the delay')
+  console.log('will depend on your hard drive speed - not a big deal, on')
+  console.log('most computers.)')
+
+  const useSmartPlaylist = {
+    y: true,
+    n: false
+  }[await prompt(rl, 'Process playlist every time?', 'y', {
+    y: 'Yes, process the playlist for new items every time.',
+    n: "No, don't automatically process the playlist."
+  })]
+
+  console.log("Do you want to save your playlist to a file? If not, you'll")
+  console.log('just be given the command you can use to generate the file.')
+
+  const smartPlaylistString = JSON.stringify({
+    source: [crawlerCommand, ...crawlerOptions]
+  }, null, 2)
+
+  const savePlaylist = {
+    y: true,
+    n: false
+  }[await prompt(rl, 'Save playlist?', 'y', {
+    y: 'Yes, save the playlist to a file.',
+    n: 'No, just show the command.'
+  })]
+
+  if (savePlaylist) {
+    console.log('What would you like to name your playlist file?')
+    console.log('"playlist.json" will be automatically detected by http-music,')
+    console.log('but you can specify a different file or path if you want.')
+
+    let defaultOutput = 'playlist.json'
+
+    const playlistExists = await exists('playlist.json')
+
+    if (playlistExists) {
+      console.log('')
+      console.log(
+        '\x1b[1mBeware!\x1b[0m There is already a file called playlist.json' +
+        ' in this'
+      )
+      console.log(`directory. (Your current working directory is: ${wd})`)
+      console.log('You may want to write to another file.')
+      defaultOutput = null
+    }
+
+    let outputFile = await prompt(rl, 'Playlist file name?', defaultOutput)
+
+    if (path.extname(outputFile) !== '.json') {
+      console.log('(http-music playlist files are JSON files, so your file')
+      console.log('was changed to a .json file.)')
+      console.log('')
+
+      outputFile = path.basename(outputFile, path.extname(outputFile))
+
+      if (playlistExists && path.relative(outputFile, 'playlist') === '') {
+        console.log('(Since that would overwrite the playlist.json that already')
+        console.log(
+          "exists in this directory, it'll instead be saved to playlist2.json.)"
+        )
+        console.log('')
+        outputFile += '2'
+      }
+
+      outputFile += '.json'
+    }
+
+    if (useSmartPlaylist) {
+      await writeFile(outputFile, smartPlaylistString)
+    } else {
+      console.log('Generating your playlist file. This could take a little while..')
+      const { main: crawlerMain } = getCrawlerByName(crawlerCommand)
+      const out = await crawlerMain(crawlerOptions, true)
+      await writeFile(outputFile, out)
+    }
+
+    console.log('Done setting up and saving your playlist file.')
+    console.log(`Try it out with \x1b[1mhttp-music play${
+      (path.relative(outputFile, 'playlist.json') === '')
+      ? ''
+      : ` --open ${path.relative('.', outputFile)}`
+    }\x1b[0m!`)
+  } else {
+    if (useSmartPlaylist) {
+      console.log("You'll want to create a playlist JSON file containing")
+      console.log('the following:')
+      console.log('')
+      console.log(`\x1b[1m${smartPlaylistString}\x1b[0m`)
+    } else {
+      console.log(
+        `You'll want to use the \x1b[1m${crawlerCommand}\x1b[0m crawler command.`
+      )
+
+      if (crawlerOptions.length > 1) {
+        console.log(
+          'You should give it these arguments:',
+          crawlerOptions.map(l => `\x1b[1m${l}\x1b[0m`).join(', ')
+        )
+      } else if (crawlerOptions.length === 1) {
+        const opt = crawlerOptions[0]
+        console.log(`You should give it this argument: \x1b[1m${opt}\x1b[0m`)
+      }
+    }
+    console.log('')
+  }
+
+  rl.close()
+}
+
+module.exports = setupTool
+
+if (require.main === module) {
+  setupTool()
+    .catch(err => console.error(err))
+}
diff --git a/yarn.lock b/yarn.lock
index bbb0f25..c17dd9c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1,5 +1,7 @@
 # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
 # yarn lockfile v1
+
+
 "@types/node@^6.0.46":
   version "6.0.73"
   resolved "https://registry.yarnpkg.com/@types/node/-/node-6.0.73.tgz#85dc4bb6f125377c75ddd2519a1eeb63f0a4ed70"
@@ -52,14 +54,14 @@ css-what@2.1:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.0.tgz#9467d032c38cfaefb9f2d79501253062f87fa1bd"
 
-dom-serializer@~0.1.0, dom-serializer@0:
+dom-serializer@0, dom-serializer@~0.1.0:
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82"
   dependencies:
     domelementtype "~1.1.1"
     entities "~1.1.1"
 
-domelementtype@^1.3.0, domelementtype@1:
+domelementtype@1, domelementtype@^1.3.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.0.tgz#b17aed82e8ab59e52dd9c19b1756e0fc187204c2"
 
@@ -73,7 +75,7 @@ domhandler@^2.3.0:
   dependencies:
     domelementtype "1"
 
-domutils@^1.5.1, domutils@1.5.1:
+domutils@1.5.1, domutils@^1.5.1:
   version "1.5.1"
   resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf"
   dependencies:
@@ -220,6 +222,10 @@ sax@^1.2.1:
   version "1.2.4"
   resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
 
+seed-random@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/seed-random/-/seed-random-2.2.0.tgz#2a9b19e250a817099231a5b99a4daf80b7fbed54"
+
 string_decoder@~1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.1.tgz#62e200f039955a6810d8df0a33ffc0f013662d98"
@@ -267,4 +273,3 @@ xmldoc:
   resolved "https://registry.yarnpkg.com/xmldoc/-/xmldoc-1.1.0.tgz#25c92f08f263f344dac8d0b32370a701ee9d0e93"
   dependencies:
     sax "^1.2.1"
-