« 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
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rwxr-xr-xsrc/cli.js29
-rwxr-xr-xsrc/crawl-http.js2
-rwxr-xr-xsrc/crawl-itunes.js32
-rwxr-xr-xsrc/crawl-local.js2
-rw-r--r--src/crawl-youtube.js2
-rw-r--r--src/crawlers.js13
-rw-r--r--src/downloaders.js10
-rw-r--r--src/loop-play.js22
-rw-r--r--src/playlist-utils.js10
-rw-r--r--src/smart-playlist.js49
10 files changed, 142 insertions, 29 deletions
diff --git a/src/cli.js b/src/cli.js
index 4bc64ab..9b21395 100755
--- a/src/cli.js
+++ b/src/cli.js
@@ -4,6 +4,8 @@
 // maxlistenersexceededwarning.
 process.on('warning', e => console.warn(e.stack))
 
+const { getCrawlerByName } = require('./crawlers')
+
 async function main(args) {
   let script
 
@@ -13,18 +15,21 @@ async function main(args) {
     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 'crawl-youtube': script = require('./crawl-youtube'); break
-    case 'download-playlist': script = require('./download-playlist'); break
-
-    default:
-      console.error(`Invalid command "${args[0]}" provided.`)
-      console.error("Try 'man http-music'?")
-      return
+  const module = getCrawlerByName(args[0])
+
+  if (module) {
+    script = module.main
+  } else {
+    switch (args[0]) {
+      case 'play': script = require('./play'); break
+      case 'download-playlist': script = require('./download-playlist'); break
+      case 'smart-playlist': script = require('./smart-playlist'); break
+
+      default:
+        console.error(`Invalid command "${args[0]}" provided.`)
+        console.error("Try 'man http-music'?")
+        return
+    }
   }
 
   await script(args.slice(1))
diff --git a/src/crawl-http.js b/src/crawl-http.js
index 7308f3f..76f3941 100755
--- a/src/crawl-http.js
+++ b/src/crawl-http.js
@@ -195,7 +195,7 @@ async function main(args) {
   console.log(JSON.stringify(downloadedPlaylist, null, 2))
 }
 
-module.exports = main
+module.exports = {main, crawl}
 
 if (require.main === module) {
   main(process.argv.slice(2))
diff --git a/src/crawl-itunes.js b/src/crawl-itunes.js
index 6060ffa..e6b63b3 100755
--- a/src/crawl-itunes.js
+++ b/src/crawl-itunes.js
@@ -30,7 +30,23 @@ function findChild(grouplike, name) {
   return grouplike.items.find(x => x.name === name)
 }
 
-async function crawl(libraryXML) {
+let NO_LIBRARY_SYMBOL = Symbol('No library')
+
+async function crawl(
+  libraryPath = `${process.env.HOME}/Music/iTunes/iTunes Music Library.xml`
+) {
+  let libraryXML
+
+  try {
+    libraryXML = await readFile(libraryPath)
+  } catch (err) {
+    if (err.code === 'ENOENT') {
+      throw NO_LIBRARY_SYMBOL
+    } else {
+      throw err
+    }
+  }
+
   const document = new xmldoc.XmlDocument(libraryXML)
 
   const libraryDict = document.children.find(child => child.name === 'dict')
@@ -94,16 +110,12 @@ async function crawl(libraryXML) {
 }
 
 async function main(args) {
-  const libraryPath = args[0] || (
-    `${process.env.HOME}/Music/iTunes/iTunes Music Library.xml`
-  )
-
-  let library
+  let playlist
 
   try {
-    library = await readFile(libraryPath)
+    playlist = await crawl(args[0])
   } catch(err) {
-    if (err.code === 'ENOENT') {
+    if (err === NO_LIBRARY_SYMBOL) {
       console.error(
         "It looks like you aren't sharing the iTunes Library XML file."
       )
@@ -125,12 +137,10 @@ async function main(args) {
     }
   }
 
-  const playlist = await crawl(library)
-
   console.log(JSON.stringify(playlist, null, 2))
 }
 
-module.exports = main
+module.exports = {main, crawl}
 
 if (require.main === module) {
   main(process.argv.slice(2))
diff --git a/src/crawl-local.js b/src/crawl-local.js
index 629e015..d4176ed 100755
--- a/src/crawl-local.js
+++ b/src/crawl-local.js
@@ -41,7 +41,7 @@ async function main(args) {
   }
 }
 
-module.exports = main
+module.exports = {main, crawl}
 
 if (require.main === module) {
   main(process.argv.slice(2))
diff --git a/src/crawl-youtube.js b/src/crawl-youtube.js
index 823fef7..4b4c66c 100644
--- a/src/crawl-youtube.js
+++ b/src/crawl-youtube.js
@@ -41,7 +41,7 @@ async function main(args) {
   }
 }
 
-module.exports = main
+module.exports = {main, crawl}
 
 if (require.main === module) {
   main(process.argv.slice(2))
diff --git a/src/crawlers.js b/src/crawlers.js
new file mode 100644
index 0000000..5ad7fb4
--- /dev/null
+++ b/src/crawlers.js
@@ -0,0 +1,13 @@
+'use strict'
+
+module.exports = {
+  getCrawlerByName: function(name) {
+    switch (name) {
+      case 'crawl-http': return require('./crawl-http')
+      case 'crawl-local': return require('./crawl-local')
+      case 'crawl-itunes': return require('./crawl-itunes')
+      case 'crawl-youtube': return require('./crawl-youtube')
+      default: return null
+    }
+  }
+}
diff --git a/src/downloaders.js b/src/downloaders.js
index c3dc43d..b9cc33d 100644
--- a/src/downloaders.js
+++ b/src/downloaders.js
@@ -107,7 +107,7 @@ function makeConverterDownloader(downloader, type) {
     const inFile = await downloader(arg)
     const base = path.basename(inFile, path.extname(inFile))
     const tempDir = tempy.directory()
-    const outFile = tempDir + base + '.' + type
+    const outFile = `${tempDir}/${base}.${type}`
 
     await promisifyProcess(spawn('avconv', ['-i', inFile, outFile]), false)
 
@@ -122,6 +122,14 @@ module.exports = {
   makePowerfulDownloader,
   makeConverterDownloader,
 
+  byName: {
+    'http': makeHTTPDownloader,
+    'local': makeLocalDownloader,
+    'file': makeLocalDownloader,
+    'youtube': makeYouTubeDownloader,
+    'youtube-dl': makeYouTubeDownloader
+  },
+
   getDownloaderFor(arg) {
     if (arg.startsWith('http://') || arg.startsWith('https://')) {
       if (arg.includes('youtube.com')) {
diff --git a/src/loop-play.js b/src/loop-play.js
index b0bb4dd..9328073 100644
--- a/src/loop-play.js
+++ b/src/loop-play.js
@@ -5,7 +5,10 @@
 const { spawn } = require('child_process')
 const FIFO = require('fifo-js')
 const EventEmitter = require('events')
-const { getDownloaderFor, makeConverterDownloader } = require('./downloaders')
+const {
+  getDownloaderFor, makeConverterDownloader,
+  byName: downloadersByName
+} = require('./downloaders')
 const { getItemPathString } = require('./playlist-utils')
 const promisifyProcess = require('./promisify-process')
 
@@ -114,7 +117,22 @@ class PlayController {
     if (picked === null) {
       return null
     } else {
-      let downloader = getDownloaderFor(picked.downloaderArg)
+      let downloader
+
+      if (picked.downloader) {
+        downloader = downloadersByName[picked.downloader]()
+
+        if (!downloader) {
+          console.error(
+            `Invalid downloader for track ${picked.name}:`, downloader
+          )
+
+          return false
+        }
+      } else {
+        downloader = getDownloaderFor(picked.downloaderArg)
+      }
+
       downloader = makeConverterDownloader(downloader, 'wav')
       this.downloadController.download(downloader, picked.downloaderArg)
       return picked
diff --git a/src/playlist-utils.js b/src/playlist-utils.js
index 55638ed..5efd478 100644
--- a/src/playlist-utils.js
+++ b/src/playlist-utils.js
@@ -56,6 +56,16 @@ function updateGroupFormat(group) {
     // isn't a string..
     if (typeof item[1] === 'string' || item.downloaderArg) {
       item = updateTrackFormat(item)
+
+      // TODO: Should this also apply to groups? Is recursion good? Probably
+      // not!
+      //
+      // TODO: How should saving/serializing handle this? For now it just saves
+      // the result, after applying. (I.e., "apply": {"foo": "baz"} will save
+      // child tracks with {"foo": "baz"}.)
+      if (groupObj.apply) {
+        Object.assign(item, groupObj.apply)
+      }
     } else {
       item = updateGroupFormat(item)
     }
diff --git a/src/smart-playlist.js b/src/smart-playlist.js
new file mode 100644
index 0000000..e65ff1f
--- /dev/null
+++ b/src/smart-playlist.js
@@ -0,0 +1,49 @@
+'use strict'
+
+const fs = require('fs')
+const { getCrawlerByName } = require('./crawlers')
+
+const { promisify } = require('util')
+const readFile = promisify(fs.readFile)
+
+async function processItem(item) {
+  // Object.assign is used so that we keep original properties, e.g. "name"
+  // or "apply".
+
+  if ('items' in item) {
+    return Object.assign(item, {
+      items: await Promise.all(item.items.map(processItem))
+    })
+  } else if ('source' in item) {
+    const [ name, ...args ] = item.source
+
+    const crawlModule = getCrawlerByName(name)
+
+    if (crawlModule === null) {
+      console.error(`No crawler by name ${name} - skipped item:`, item)
+      return Object.assign(item, {failed: true})
+    }
+
+    const { crawl } = crawlModule
+
+    return Object.assign(item, await crawl(...args))
+  } else {
+    return item
+  }
+}
+
+async function main(opts) {
+  // TODO: Error when no file is given
+
+  if (opts.length === 0) {
+    console.log("Usage: smart-playlist /path/to/playlist")
+  } else {
+    const playlist = JSON.parse(await readFile(opts[0]))
+    console.log(JSON.stringify(await processItem(playlist), null, 2))
+  }
+}
+
+if (require.main === module) {
+  main(process.argv.slice(2))
+    .catch(err => console.error(err))
+}