« 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-play.115
-rw-r--r--man/http-music.12
-rw-r--r--package.json1
-rw-r--r--src/downloaders.js14
-rw-r--r--src/loop-play.js56
-rwxr-xr-xsrc/play.js27
-rw-r--r--todo.txt7
-rw-r--r--yarn.lock4
8 files changed, 114 insertions, 12 deletions
diff --git a/man/http-music-play.1 b/man/http-music-play.1
index 36e33fd..8f69ab5 100644
--- a/man/http-music-play.1
+++ b/man/http-music-play.1
@@ -10,8 +10,7 @@ http-music-play - plays audio from a playlist file
 .SH DESCRIPTION
 Plays audio referenced from a playlist file.
 Tracks selected using a "picker" (see \fB--picker\fR) and retrieved using a "downloader" (see \fB--downloader\fR).
-Downloaded tracks are played with the \fBmpv\fR process.
-(As such, \fBmpv\fR is a required dependency for http-music to play anything.)
+Downloaded tracks are played with either the \fBmpv\fR (default) or \fBplay\fR (from SoX) command.
 
 
 .SH KEYBOARD CONTROLS
@@ -19,11 +18,13 @@ Downloaded tracks are played with the \fBmpv\fR process.
 .BR <left-arrow>
 Skips backwards 5 seconds in the currently playing track; hold shift to skip by
 30 seconds.
+(Requires MPV player.)
 
 .TP
 .BR <right-arrow>
 Skips forwards 5 seconds in the currently playing track; hold shift to skip by
 30 seconds.
+(Requires MPV player.)
 
 .TP
 .BR <up-arrow>
@@ -31,14 +32,17 @@ Turns the volume up a 10%-notch.
 Unfortunately, at present, the volume setting is NOT kept across tracks.
 You'll need to adjust your audio volume whenever a new song starts.
 (If possible, it might be better just to opt for changing the system volume.)
+(Requires MPV player.)
 
 .TP
 .BR <down-arrow>
 Turns the volume down 10%.
+(Requires MPV player.)
 
 .TP
 .BR <space>
 Pauses (or resumes) playback.
+(Requires MPV player.)
 
 .TP
 .BR i
@@ -103,6 +107,13 @@ The default is \fBshuffle\fR.
 Forces the playlist to actually play, regardless of options such as \fB\-\-list\fR. See also \fB\-\-no\-play\fR.
 
 .TP
+.BR \-\-player " \fIplayer"
+Selects the mode by which audio is played.
+Valid options include "mpv" and "sox" (or "play").
+Most playback controls only work with the "mpv" player, but the "sox"/"play" player is typically much more easy to (and commonly) install than "mpv".
+The default is \fBmpv\fR, but \fBsox\fR will be used if mpv is not installed.
+
+.TP
 .BR \-\-play\-opts
 Sets command line options passed to the \fBplay\fR command.
 For example, playback volume may be set to 30% by using \fB\-\-play\-opts '\-\-volume 30'\fR.
diff --git a/man/http-music.1 b/man/http-music.1
index 634f014..09b733d 100644
--- a/man/http-music.1
+++ b/man/http-music.1
@@ -14,7 +14,7 @@ It features several convenient options which make it powerful while still sticki
 
 .PP
 \fBhttp-music\fR is portable.
-It can be used anywhere with a Node environment, requiring only two commonly installed (and otherwise easy to get) utilities (\fBmpv\fR and \fBavconv\fR, optionally \fByoutube-dl\fR).
+It can be used anywhere with a Node environment, requiring only two commonly installed (and otherwise easy to get) utilities (\fBmpv\fR or \fBplay\fR (SoX), and \fBavconv\fR, and optionally \fByoutube-dl\fR).
 
 .PP
 Playlists are stored as JSON files.
diff --git a/package.json b/package.json
index 57ef8aa..688267a 100644
--- a/package.json
+++ b/package.json
@@ -22,6 +22,7 @@
   "dependencies": {
     "cheerio": "^1.0.0-rc.1",
     "clone": "^2.1.1",
+    "command-exists": "^1.2.2",
     "fifo-js": "^2.1.0",
     "fs-extra": "^3.0.1",
     "ncp": "^2.0.0",
diff --git a/src/downloaders.js b/src/downloaders.js
index 04838c2..c3dc43d 100644
--- a/src/downloaders.js
+++ b/src/downloaders.js
@@ -102,11 +102,25 @@ function makePowerfulDownloader(downloader, maxAttempts = 5) {
   }
 }
 
+function makeConverterDownloader(downloader, type) {
+  return async function(arg) {
+    const inFile = await downloader(arg)
+    const base = path.basename(inFile, path.extname(inFile))
+    const tempDir = tempy.directory()
+    const outFile = tempDir + base + '.' + type
+
+    await promisifyProcess(spawn('avconv', ['-i', inFile, outFile]), false)
+
+    return outFile
+  }
+}
+
 module.exports = {
   makeHTTPDownloader,
   makeYouTubeDownloader,
   makeLocalDownloader,
   makePowerfulDownloader,
+  makeConverterDownloader,
 
   getDownloaderFor(arg) {
     if (arg.startsWith('http://') || arg.startsWith('https://')) {
diff --git a/src/loop-play.js b/src/loop-play.js
index 8fdbdf3..b0bb4dd 100644
--- a/src/loop-play.js
+++ b/src/loop-play.js
@@ -5,8 +5,9 @@
 const { spawn } = require('child_process')
 const FIFO = require('fifo-js')
 const EventEmitter = require('events')
-const { getDownloaderFor } = require('./downloaders')
+const { getDownloaderFor, makeConverterDownloader } = require('./downloaders')
 const { getItemPathString } = require('./playlist-utils')
+const promisifyProcess = require('./promisify-process')
 
 class DownloadController extends EventEmitter {
   waitForDownload() {
@@ -65,11 +66,12 @@ class DownloadController extends EventEmitter {
 
 class PlayController {
   constructor(picker, downloadController) {
-    this.currentTrack = null
-    this.playOpts = []
-    this.process = null
     this.picker = picker
     this.downloadController = downloadController
+    this.playOpts = []
+    this.playerCommand = null
+    this.currentTrack = null
+    this.process = null
   }
 
   async loopPlay() {
@@ -112,13 +114,47 @@ class PlayController {
     if (picked === null) {
       return null
     } else {
-      const downloader = getDownloaderFor(picked.downloaderArg)
+      let downloader = getDownloaderFor(picked.downloaderArg)
+      downloader = makeConverterDownloader(downloader, 'wav')
       this.downloadController.download(downloader, picked.downloaderArg)
       return picked
     }
   }
 
   playFile(file) {
+    if (this.playerCommand === 'sox' || this.playerCommand === 'play') {
+      return this.playFileSoX(file)
+    } else if (this.playerCommand === 'mpv') {
+      return this.playFileMPV(file)
+    } else {
+      if (this.playerCommand) {
+        console.warn('Invalid player command given?', this.playerCommand)
+      } else {
+        console.warn('No player command given?')
+      }
+
+      return Promise.resolve()
+    }
+  }
+
+  playFileSoX(file) {
+    // SoX's play command is useful for systems that don't have MPV. SoX is
+    // much easier to install (and probably more commonly installed, as well).
+    // You don't get keyboard controls such as seeking or volume adjusting
+    // with SoX, though.
+
+    this.process = spawn('play', [
+      ...this.playOpts,
+      file
+    ])
+
+    return promisifyProcess(this.process)
+  }
+
+  playFileMPV(file) {
+    // The more powerful MPV player. MPV is virtually impossible for a human
+    // being to install; if you're having trouble with it, try the SoX player.
+
     this.fifo = new FIFO()
     this.process = spawn('mpv', [
       '--input-file=' + this.fifo.path,
@@ -194,7 +230,7 @@ class PlayController {
   }
 
   sendCommand(command) {
-    if (this.fifo) {
+    if (this.playerCommand === 'mpv' && this.fifo) {
       this.fifo.write(command)
     }
   }
@@ -235,15 +271,19 @@ class PlayController {
   }
 }
 
-module.exports = function loopPlay(picker, playOpts = []) {
+module.exports = function loopPlay(
+  picker, playerCommand = 'mpv', playOpts = []
+) {
   // Looping play function. Takes one argument, the "picker" function,
   // which returns a track to play. Stops when the result of the picker
   // 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()
+
   const playController = new PlayController(picker, downloadController)
-  playController.playOpts = playOpts
+
+  Object.assign(playController, {playerCommand, playOpts})
 
   const promise = playController.loopPlay()
 
diff --git a/src/play.js b/src/play.js
index 9515d97..6db8e68 100755
--- a/src/play.js
+++ b/src/play.js
@@ -6,6 +6,7 @@ const { promisify } = require('util')
 const clone = require('clone')
 const fs = require('fs')
 const fetch = require('node-fetch')
+const commandExists = require('command-exists')
 const pickers = require('./pickers')
 const loopPlay = require('./loop-play')
 const processArgv = require('./process-argv')
@@ -39,11 +40,22 @@ function clearConsoleLine() {
   process.stdout.write('\x1b[1K\r')
 }
 
+async function determineDefaultPlayer() {
+  if (await commandExists('mpv')) {
+    return 'mpv'
+  } else if (await commandExists('play')) {
+    return 'play'
+  } else {
+    return null
+  }
+}
+
 async function main(args) {
   let sourcePlaylist = null
   let activePlaylist = null
 
   let pickerType = 'shuffle'
+  let playerCommand = await determineDefaultPlayer()
   let playOpts = []
 
   // WILL play says whether the user has forced playback via an argument.
@@ -270,6 +282,17 @@ async function main(args) {
 
     '-selector': util => util.alias('-picker'),
 
+    '-player': function(util) {
+      // --player <player>
+      // Sets the shell command by which audio is played.
+      // Valid options include 'sox' (or 'play') and 'mpv'. Use whichever is
+      // installed on your system; mpv is the default.
+
+      playerCommand = util.nextArg()
+    },
+
+    '-player': util => util.alias('-player-command'),
+
     '-play-opts': function(util) {
       // --play-opts <opts>
       // Sets command line options passed to the `play` command.
@@ -301,11 +324,13 @@ async function main(args) {
       return
     }
 
+    console.log(`Using ${playerCommand} player.`)
+
     const {
       promise: playPromise,
       playController: play,
       downloadController
-    } = loopPlay(picker, playOpts)
+    } = loopPlay(picker, playerCommand, playOpts)
 
     // We're looking to gather standard input one keystroke at a time.
     // But that isn't *always* possible, e.g. when piping into the http-music
diff --git a/todo.txt b/todo.txt
index 52c2a94..c16f680 100644
--- a/todo.txt
+++ b/todo.txt
@@ -228,3 +228,10 @@ TODO: Make iTunes crawler take into account track numbers.
 
 TODO: Make a YouTube playlist crawler.
       (Done!)
+
+TODO: The filter utility function shouldn't work at all if it fails to find
+      what it's looking for.
+
+TODO: Make the filter/remove/keep options do a search of some sort.
+
+TODO: Make those options also work with tracks!
diff --git a/yarn.lock b/yarn.lock
index 12bc494..d3d249f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -27,6 +27,10 @@ clone:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.1.tgz#d217d1e961118e3ac9a4b8bba3285553bf647cdb"
 
+command-exists:
+  version "1.2.2"
+  resolved "https://registry.yarnpkg.com/command-exists/-/command-exists-1.2.2.tgz#12819c64faf95446ec0ae07fe6cafb6eb3708b22"
+
 core-util-is@~1.0.0:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"