« 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.116
-rw-r--r--src/downloaders.js3
-rwxr-xr-xsrc/http-music.js17
-rw-r--r--src/loop-play.js75
-rw-r--r--src/promisify-process.js7
-rw-r--r--todo.txt2
6 files changed, 95 insertions, 25 deletions
diff --git a/man/http-music.1 b/man/http-music.1
index 5fc93fb..3bda2f5 100644
--- a/man/http-music.1
+++ b/man/http-music.1
@@ -24,12 +24,22 @@ It can be used anywhere with a Node environment, requiring only two commonly ins
 
 .SH KEYBOARD CONTROLS
 .TP
-.BR s
-Skips the currently playing track.
+.BR <del>
+Assigns a new track to be played next, overwriting the current up-next track.
+
+.TP
+.BR i
+Shows information (title, URL/path) on the currently playing track.
+(\fBt\fR also works.)
+
 .TP
 .BR q
-Quits the http-music process and stops music currently being played. (\fB^C\fR and \fB^D\fR also work.)
+Quits the http-music process and stops music currently being played.
+(\fB^C\fR and \fB^D\fR also work.)
 
+.TP
+.BR s
+Skips the currently playing track.
 
 
 .SH OPTIONS
diff --git a/src/downloaders.js b/src/downloaders.js
index 5f65346..28dab92 100644
--- a/src/downloaders.js
+++ b/src/downloaders.js
@@ -24,13 +24,14 @@ function makeYouTubeDownloader() {
     const tempDir = tempy.directory()
 
     const opts = [
+      '--quiet',
       '--extract-audio',
       '--audio-format', 'wav',
       '--output', tempDir + '/dl.%(ext)s',
       arg
     ]
 
-    return promisifyProcess(spawn('youtube-dl', opts), false)
+    return promisifyProcess(spawn('youtube-dl', opts))
       .then(() => tempDir + '/dl.wav')
   }
 }
diff --git a/src/http-music.js b/src/http-music.js
index f6c1a52..1392c34 100755
--- a/src/http-music.js
+++ b/src/http-music.js
@@ -246,10 +246,20 @@ setupDefaultPlaylist('./playlist.json')
 
       process.stdin.on('data', data => {
         if (Buffer.from('s').equals(data)) {
+          // console.log(
+          //   "Skipping the track that's currently playing. " +
+          //   "(Press I for track info!)"
+          // )
+
           play.skipCurrent()
         }
 
         if (Buffer.from([0x7f]).equals(data)) { // Delete
+          console.log(
+            "Skipping the track that's up next. " +
+            "(Press I for track info!)"
+          )
+
           play.skipUpNext()
         }
 
@@ -262,6 +272,13 @@ setupDefaultPlaylist('./playlist.json')
           process.stdout.write('\n')
           process.exit(0)
         }
+
+        if (
+          Buffer.from('i').equals(data) ||
+          Buffer.from('t').equals(data)
+        ) {
+          play.logTrackInfo()
+        }
       })
 
       return play.promise
diff --git a/src/loop-play.js b/src/loop-play.js
index b49d9e4..cf6d829 100644
--- a/src/loop-play.js
+++ b/src/loop-play.js
@@ -5,8 +5,13 @@ const promisifyProcess = require('./promisify-process')
 const sanitize = require('sanitize-filename')
 const tempy = require('tempy')
 
-class DownloadController {
+const EventEmitter = require('events')
+
+class DownloadController extends EventEmitter {
   constructor(picker, downloader) {
+    super()
+
+    this.pickedTrack = null
     this.process = null
     this.requestingSkipUpNext = false
     this.isDownloading = false
@@ -31,6 +36,11 @@ class DownloadController {
       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
@@ -38,13 +48,6 @@ class DownloadController {
     // It's up to the downloader to decide what to do with it.
     const [ title, downloaderArg ] = picked
 
-    console.log(`Downloading ${title}..\nDownloader arg: ${downloaderArg}`)
-
-    // The "from" file is downloaded by the downloader (given in the
-    // DownloadController constructor) using the downloader argument we just
-    // got.
-    const from = await this.downloader(downloaderArg)
-
     // The "to" file is simply a WAV file. We give this WAV 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
@@ -52,6 +55,14 @@ class DownloadController {
     const tempDir = tempy.directory()
     const to = tempDir + `/.${sanitize(title)}.wav`
 
+    // We'll use this wav file later, to actually play the track.
+    this.wavFile = to
+
+    // The "from" file is downloaded by the downloader (given in the
+    // DownloadController constructor) using the downloader argument we just
+    // got.
+    const from = await this.downloader(downloaderArg)
+
     // 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,
@@ -60,10 +71,9 @@ class DownloadController {
       '-loglevel', 'quiet', '-i', from, to
     ])
 
-    // It's handy to store the output WAV file (the "to" file) and the `avconv`
-    // process; the WAV file is used later to be played, and the convert
-    // process is stored so it can be killed before it finishes.
-    this.wavFile = to
+    // 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.
@@ -72,9 +82,6 @@ class DownloadController {
     try {
       await promisifyProcess(convertProcess)
     } catch(err) {
-      console.warn("Failed to convert " + title)
-      console.warn("Selecting a new track\n")
-
       // 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
@@ -82,6 +89,15 @@ class DownloadController {
       // 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 we're requesting a skip-up-next, it's expected for the avconv
+      // process to fail; so in that case we don't bother warning the user.
+      if (!this.requestingSkipUpNext) {
+        console.warn("Failed to convert " + title)
+        console.warn("Selecting a new track")
+      }
+
       return await this.downloadNext()
     }
 
@@ -121,10 +137,17 @@ class DownloadController {
 
 class PlayController {
   constructor(downloadController) {
+    this.currentTrack = null
+    this.upNextTrack = null
     this.playArgs = []
     this.process = null
 
     this.downloadController = downloadController
+
+    this.downloadController.on('trackPicked', track => {
+      console.log('Changed:', track[0])
+      this.upNextTrack = track
+    })
   }
 
   async loopPlay() {
@@ -134,17 +157,19 @@ class PlayController {
     await this.downloadController.downloadNext()
 
     while (this.downloadController.wavFile) {
-      const nextPromise = this.downloadController.downloadNext()
+      this.currentTrack = this.downloadController.pickedTrack
 
       const file = this.downloadController.wavFile
       const playProcess = spawn('play', [...this.playArgs, file])
       const playPromise = promisifyProcess(playProcess)
       this.process = playProcess
 
+      const nextPromise = this.downloadController.downloadNext()
+
       try {
         await playPromise
       } catch(err) {
-        console.warn(err)
+        console.warn(err + '\n')
       }
 
       await nextPromise
@@ -187,6 +212,22 @@ module.exports = function loopPlay(picker, downloader, playArgs = []) {
     kill: function() {
       playController.killProcess()
       downloadController.killProcess()
+    },
+
+    logTrackInfo: function() {
+      if (playController.currentTrack) {
+        const [ curTitle, curArg ] = playController.currentTrack
+        console.log(`Playing: \x1b[1m${curTitle} \x1b[2m${curArg}\x1b[0m`)
+      } 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/src/promisify-process.js b/src/promisify-process.js
index d1c09d7..d330055 100644
--- a/src/promisify-process.js
+++ b/src/promisify-process.js
@@ -3,9 +3,9 @@
 const { Writable } = require('stream')
 
 module.exports = function promisifyProcess(proc) {
-  // Takes a process (from the child_process module) and returns a promise that
-  // resolves when the process exits (or rejects with a warning, if the exit
-  // code is non-zero).
+  // Takes a process (from the child_process module) and returns a promise
+  // that resolves when the process exits (or rejects, if the exit code is
+  // non-zero).
 
   return new Promise((resolve, reject) => {
     proc.stdout.pipe(process.stdout)
@@ -15,7 +15,6 @@ module.exports = function promisifyProcess(proc) {
       if (code === 0) {
         resolve()
       } else {
-        console.error("Process failed!", proc.spawnargs)
         reject(code)
       }
     })
diff --git a/todo.txt b/todo.txt
index 5cfac31..8ef5e44 100644
--- a/todo.txt
+++ b/todo.txt
@@ -112,3 +112,5 @@ TODO: A way to kill the up-next song.
 
 TODO: A way to see information about the currently playing song, as well as
       the up-next song.
+
+TODO: A way to see the previously played songs, and to skip back (or forwards).