« get me outta code hell

Remove downloaders - http-music - Command-line music player + utils (not a server!)
about summary refs log tree commit diff
diff options
context:
space:
mode:
authorliam4 <towerofnix@gmail.com>2017-06-26 23:16:11 -0300
committerliam4 <towerofnix@gmail.com>2017-06-26 23:16:11 -0300
commit0fcec41cd757adcd3c7f2f4c80b33141bb0a9bee (patch)
tree5ac2425d927f0624266a11433602f63f7eb9145e
parente56cdd69c330f2747c77b5db1595fddb6f6d1f10 (diff)
Remove downloaders
-rw-r--r--man/http-music.118
-rw-r--r--src/download-playlist.js4
-rw-r--r--src/downloaders.js86
-rwxr-xr-xsrc/http-music.js28
-rw-r--r--src/loop-play.js236
-rw-r--r--todo.txt7
6 files changed, 22 insertions, 357 deletions
diff --git a/man/http-music.1 b/man/http-music.1
index e767bed..751a397 100644
--- a/man/http-music.1
+++ b/man/http-music.1
@@ -59,10 +59,6 @@ Quits the http-music process and stops music currently being played.
 .BR s
 Skips the currently playing track.
 
-.TP
-.BR <del>
-Assigns a new track to be played next, skipping the current up-next track.
-
 
 .SH OPTIONS
 .TP
@@ -70,10 +66,6 @@ Assigns a new track to be played next, skipping the current up-next track.
 Clears the active playlist.
 This does not effect the source playlist, so specific groups can be selected using \fB\-\-keep\fR.
 
-.TP
-.BR \-\-downloader " \fIdownloaderType\fR"
-Sets the downloader type used for actually retrieving track files.
-The default is \fBhttp\fR.
 
 .TP
 .BR \-h ", " \-? ", " \-\-help
@@ -161,16 +153,6 @@ $ http-music-crawl-local /example/path > playlist.json
 .fi
 
 .PP
-Play music from a playlist generated from local file system:
-
-.PP
-.nf
-.RS
-$ http-music --downloader local
-.RE
-.fi
-
-.PP
 Open a specific playlist file:
 
 .PP
diff --git a/src/download-playlist.js b/src/download-playlist.js
index bb6b86c..c8476e4 100644
--- a/src/download-playlist.js
+++ b/src/download-playlist.js
@@ -1,3 +1,7 @@
+// TODO: This almost definitely doesn't work, ever since downloaders were
+// removed! Maybe it's possible to make mpv only download (and not play) a
+// file?
+
 'use strict'
 
 const fs = require('fs')
diff --git a/src/downloaders.js b/src/downloaders.js
deleted file mode 100644
index 8fa830c..0000000
--- a/src/downloaders.js
+++ /dev/null
@@ -1,86 +0,0 @@
-'use strict'
-
-const fs = require('fs')
-const fetch = require('node-fetch')
-const promisifyProcess = require('./promisify-process')
-const tempy = require('tempy')
-const path = require('path')
-const sanitize = require('sanitize-filename')
-
-const { spawn } = require('child_process')
-const { promisify } = require('util')
-
-const writeFile = promisify(fs.writeFile)
-
-function makeHTTPDownloader() {
-  return function(arg) {
-    const dir = tempy.directory()
-    const out = dir + '/' + sanitize(decodeURIComponent(path.basename(arg)))
-
-    return fetch(arg)
-      .then(response => response.buffer())
-      .then(buffer => writeFile(out, buffer))
-      .then(() => out)
-  }
-}
-
-function makeYouTubeDownloader() {
-  return function(arg) {
-    const tempDir = tempy.directory()
-
-    const opts = [
-      '--quiet',
-      '--extract-audio',
-      '--audio-format', 'wav',
-      '--output', tempDir + '/dl.%(ext)s',
-      arg
-    ]
-
-    return promisifyProcess(spawn('youtube-dl', opts))
-      .then(() => tempDir + '/dl.wav')
-  }
-}
-
-function makeLocalDownloader() {
-  return function(arg) {
-    // Since we're grabbing the file from the local file system, there's no
-    // need to download or copy it!
-    return arg
-  }
-}
-
-function makePowerfulDownloader(downloader, maxAttempts = 5) {
-  // This should totally be named better..
-
-  return async function recursive(arg, attempts = 0) {
-    try {
-      return await downloader(arg)
-    } catch(err) {
-      if (attempts < maxAttempts) {
-        console.warn('Failed - attempting again:', arg)
-        return await recursive(arg, attempts + 1)
-      } else {
-        throw err
-      }
-    }
-  }
-}
-
-module.exports = {
-  makeHTTPDownloader,
-  makeYouTubeDownloader,
-  makeLocalDownloader,
-  makePowerfulDownloader,
-
-  getDownloader: downloaderType => {
-    if (downloaderType === 'http') {
-      return makeHTTPDownloader()
-    } else if (downloaderType === 'youtube') {
-      return makeYouTubeDownloader()
-    } else if (downloaderType === 'local') {
-      return makeLocalDownloader()
-    } else {
-      return null
-    }
-  }
-}
diff --git a/src/http-music.js b/src/http-music.js
index 31f0dca..863d170 100755
--- a/src/http-music.js
+++ b/src/http-music.js
@@ -8,7 +8,6 @@ const { promisify } = require('util')
 const loopPlay = require('./loop-play')
 const processArgv = require('./process-argv')
 
-const downloaders = require('./downloaders')
 const pickers = require('./pickers')
 
 const {
@@ -23,7 +22,6 @@ Promise.resolve()
     let activePlaylist = null
 
     let pickerType = 'shuffle'
-    let downloaderType = 'http'
     let playOpts = []
 
     // WILL play says whether the user has forced playback via an argument.
@@ -221,14 +219,6 @@ Promise.resolve()
         pickerType = util.nextArg()
       },
 
-      '-downloader': function(util) {
-        // --downloader <downloader type>
-        // Selects the mode that songs will be downloaded with.
-        // See downloaders.js.
-
-        downloaderType = util.nextArg()
-      },
-
       '-play-opts': function(util) {
         // --play-opts <opts>
         // Sets command line options passed to the `play` command.
@@ -269,13 +259,7 @@ Promise.resolve()
         return
       }
 
-      let downloader = downloaders.getDownloader(downloaderType)
-      if (!downloader) {
-        console.error("Invalid downloader type: " + downloaderType)
-        return
-      }
-
-      const play = loopPlay(picker, downloader, playOpts)
+      const play = loopPlay(picker, playOpts)
 
       // We're looking to gather standard input one keystroke at a time.
       process.stdin.setRawMode(true)
@@ -328,16 +312,6 @@ Promise.resolve()
           play.skipCurrent()
         }
 
-        if (Buffer.from([0x7f]).equals(data)) { // Delete
-          clearConsoleLine()
-          console.log(
-            "Skipping the track that's up next. " +
-            "(Press I for track info!)"
-          )
-
-          play.skipUpNext()
-        }
-
         if (
           Buffer.from('i').equals(data) ||
           Buffer.from('t').equals(data)
diff --git a/src/loop-play.js b/src/loop-play.js
index 328bb0b..3c00ef5 100644
--- a/src/loop-play.js
+++ b/src/loop-play.js
@@ -10,231 +10,26 @@ const FIFO = require('fifo-js')
 
 const EventEmitter = require('events')
 
-class DownloadController extends EventEmitter {
-  constructor(picker, downloader) {
-    super()
-
-    this.pickedTrack = null
-    this.process = null
-    this.isDownloading = false
-
-    this.picker = picker
-    this.downloader = downloader
-
-    this._downloadNext = null
-  }
-
-  downloadNext() {
-    this.downloadNextHelper()
-
-    return new Promise(resolve => {
-      this.once('downloadFinished', resolve)
-    })
-  }
-
-  async downloadNextHelper() {
-    this.isDownloading = true
-
-    const destroyedObj = {wasDestroyed: false}
-
-    this._destroyDownload = () => {
-      destroyedObj.wasDestroyed = true
-    }
-
-    // We need to actually pick something to download; we'll use the picker
-    // (given in the DownloadController constructor) for that.
-    // (See pickers.js.)
-    const picked = this.picker()
-
-    // If the picker returns null, nothing was picked; that means that we
-    // should stop now. No point in trying to play nothing!
-    if (picked == null) {
-      this.playFile = null
-      return false
-    }
-
-    // Having the picked song being available is handy, for UI stuff (i.e. for
-    // being displayed to the user through the console).
-    this.pickedTrack = picked
-    this.emit('trackPicked', picked)
-
-    // The picked result is an array containing the title of the track (only
-    // really used to display to the user) and an argument to be passed to the
-    // downloader. The downloader argument doesn't have to be anything in
-    // particular; but typically it's a string containing a URL or file path.
-    // It's up to the downloader to decide what to do with it.
-    const [ title, downloaderArg ] = picked
-
-    // The "from" file is downloaded by the downloader (given in the
-    // DownloadController constructor) using the downloader argument we just
-    // got.
-    const fromFile = await this.downloader(downloaderArg)
-
-    // Ignore the '.' at the start.
-    const format = path.extname(fromFile).slice(1)
-
-    // We'll only want to convert the "from" file if it's not already supported
-    // by SoX; so we check the supported format list.
-
-    const supportedFormats = await this.getSupportedFormats()
-
-    if (supportedFormats.includes(format)) {
-      this.playFile = fromFile
-    } else {
-      this.playFile = await this.convert(picked, fromFile, destroyedObj)
-    }
-
-    // If this download was destroyed, we quit now; we don't want to emit that
-    // the download was finished if the finished download was the destroyed
-    // one!
-    if (destroyedObj.wasDestroyed) {
-      return
-    }
-
-    this.emit('downloadFinished')
-  }
-
-  async getSupportedFormats() {
-    // TODO: This is irrelevant with `mpv` instead of `play`.
-
-    // Gets the formats supported by SoX (i.e., the `play` command) by
-    // searching the help output for the line that starts with
-    // 'AUDIO FILE FORMATS:'. This seems to be the only way to list the formats
-    // that any installation of SoX accepts; in the documentation, this is also
-    // the recommended way (though it's not particularly suggested to be parsed
-    // automatically): "To see if SoX has support for an optional format or
-    // device, enter sox −h and look for its name under the list: 'AUDIO FILE
-    // FORMATS' or 'AUDIO DEVICE DRIVERS'."
-
-    if (this._supportedSoXFormats) {
-      return this._supportedSoXFormats
-    }
-
-    const sox = spawn('sox', ['-h'])
-
-    const buffers = []
-
-    sox.stdout.on('data', buf => {
-      buffers.push(buf)
-    })
-
-    await promisifyProcess(sox, false)
-
-    const str = Buffer.concat(buffers).toString()
-
-    const lines = str.split('\n')
-
-    const prefix = 'AUDIO FILE FORMATS: '
-
-    const formatsLine = lines.find(line => line.startsWith(prefix))
-
-    const formats = formatsLine.slice(prefix.length).split(' ')
-
-    this._supportedSoXFormats = formats
-
-    return formats
-  }
-
-  async convert(picked, fromFile, destroyedObj) {
-    // The "to" file is simply an MP3 file. We give this MP3 file a specific
-    // name - the title of the track we got earlier, sanitized to be file-safe
-    // - so that when `play` outputs the name of the song, it's obvious to the
-    // user what's being played.
-    //
-    // Previously a WAV file was used here. Converting to a WAV file is
-    // considerably faster than converting to an MP3; however, the file sizes
-    // of WAVs tend to be drastically larger than MP3s. When saving disk space
-    // is one of the greatest concerns (it's essentially the point of
-    // http-music!), it's better to opt for an MP3. Additionally, most audio
-    // convertion is done in the background, while another track is already
-    // playing, so an extra few seconds of background time can hardly be
-    // noticed.
-    const title = picked[1]
-    const tempDir = tempy.directory()
-    const toFile = tempDir + `/.${sanitize(title)}.mp3`
-
-    // Now that we've got the `to` and `from` file names, we can actually do
-    // the convertion. We don't want any output from `avconv` at all, since the
-    // output of `play` will usually be displayed while `avconv` is running,
-    // so we pass `-loglevel quiet` into `avconv`.
-    const convertProcess = spawn('avconv', [
-      '-loglevel', 'quiet', '-i', fromFile, toFile
-    ])
-
-    // We store the convert process so that we can kill it before it finishes,
-    // if that's most convenient (e.g. if skipping the current song or quitting
-    // the entire program).
-    this.process = convertProcess
-
-    // Now it's only a matter of time before the process is finished.
-    // Literally; we need to await the promisified version of our convertion
-    // child process.
-    try {
-      await promisifyProcess(convertProcess)
-    } catch(err) {
-      // There's a chance we'll fail, though. That could happen if the passed
-      // "from" file doesn't actually contain audio data. In that case, we
-      // have to attempt this whole process over again, so that we get a
-      // different file. (Technically, the picker might always pick the same
-      // file; if that's the case, and the convert process is failing on it,
-      // we could end up in an infinite loop. That would be bad, since there
-      // isn't any guarding against a situation like that here.)
-
-      // Usually we'll log a warning message saying that the convertion failed,
-      // but if this download was destroyed, it's expected for the avconv
-      // process to fail; so in that case we don't bother warning the user.
-      if (!destroyedObj.wasDestroyed) {
-        console.warn("Failed to convert " + title)
-        console.warn("Selecting a new track")
-
-        return await this.downloadNext()
-      }
-    }
-
-    return toFile
-  }
-
-  skipUpNext() {
-    if (this._destroyDownload) {
-      this._destroyDownload()
-    }
-
-    this.downloadNextHelper()
-  }
-
-  killProcess() {
-    if (this.process) {
-      this.process.kill()
-    }
-  }
-}
-
 class PlayController {
-  constructor(downloadController) {
+  constructor(picker) {
     this.currentTrack = null
-    this.upNextTrack = null
     this.playArgs = []
     this.process = null
-
-    this.downloadController = downloadController
-
-    this.downloadController.on('trackPicked', track => {
-      this.upNextTrack = track
-    })
+    this.picker = picker
   }
 
   async loopPlay() {
     // Playing music in a loop isn't particularly complicated; essentially, we
-    // just want to keep downloading and playing tracks until none is picked.
+    // just want to keep picking and playing tracks until none is picked.
 
-    await this.downloadController.downloadNext()
+    let nextTrack = await this.picker()
 
-    while (this.downloadController.playFile) {
-      this.currentTrack = this.downloadController.pickedTrack
+    while (nextTrack) {
+      this.currentTrack = nextTrack
 
-      await this.playFile(this.downloadController.playFile)
+      await this.playFile(nextTrack[1])
 
-      await this.downloadController.downloadNext()
+      nextTrack = await this.picker()
     }
   }
 
@@ -332,7 +127,7 @@ class PlayController {
   }
 }
 
-module.exports = function loopPlay(picker, downloader, playArgs = []) {
+module.exports = function loopPlay(picker, playArgs = []) {
   // Looping play function. Takes one argument, the "pick" function,
   // which returns a track to play. Preemptively downloads the next
   // track while the current one is playing for seamless continuation
@@ -340,9 +135,7 @@ module.exports = function loopPlay(picker, downloader, playArgs = []) {
   // function is null (or similar). Optionally takes a second argument
   // used as arguments to the `play` process (before the file name).
 
-  const downloadController = new DownloadController(picker, downloader)
-
-  const playController = new PlayController(downloadController)
+  const playController = new PlayController(picker)
   playController.playArgs = playArgs
 
   const promise = playController.loopPlay()
@@ -353,14 +146,12 @@ module.exports = function loopPlay(picker, downloader, playArgs = []) {
     seekBack: secs => playController.seekBack(secs),
     seekAhead: secs => playController.seekAhead(secs),
     skipCurrent: () => playController.skipCurrent(),
-    skipUpNext: () => downloadController.skipUpNext(),
     volUp: amount => playController.volUp(amount),
     volDown: amount => playController.volDown(amount),
     togglePause: () => playController.togglePause(),
 
     kill: function() {
       playController.killProcess()
-      downloadController.killProcess()
     },
 
     logTrackInfo: function() {
@@ -370,13 +161,6 @@ module.exports = function loopPlay(picker, downloader, playArgs = []) {
       } else {
         console.log("No song currently playing.")
       }
-
-      if (playController.upNextTrack) {
-        const [ nextTitle, nextArg ] = playController.upNextTrack
-        console.log(`Up next: \x1b[1m${nextTitle} \x1b[2m${nextArg}\x1b[0m`)
-      } else {
-        console.log("No song up next.")
-      }
     }
   }
 }
diff --git a/todo.txt b/todo.txt
index dd3bb78..a55893c 100644
--- a/todo.txt
+++ b/todo.txt
@@ -148,7 +148,14 @@ TODO: Fix the bug in loop-play.js where wasDestroyed doesn't exist!
       (Done!)
 
 TODO: Use `mpv` instead of `play`!!!!!!!!! This would fix every problem ever.
+      (Done!)
 
 TODO: It looks like up-next doesn't seem to work properly when using the
       YouTube downloader; though it's possible we'll be scrapping downloaders
       altogether..
+      (Done! - Removed downloaders.)
+
+TODO: The results of pressing key commands aren't very clear currently. Useful
+      things that come to mind would be presenting the volume when it's
+      changed; making it clear that a song is being skipped when it is; and
+      having "paused" be part of the status bar.