« 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')
-rw-r--r--src/downloaders.js14
-rw-r--r--src/loop-play.js56
-rwxr-xr-xsrc/play.js27
3 files changed, 88 insertions, 9 deletions
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