diff options
-rw-r--r-- | man/http-music-play.1 | 15 | ||||
-rw-r--r-- | man/http-music.1 | 2 | ||||
-rw-r--r-- | package.json | 1 | ||||
-rw-r--r-- | src/downloaders.js | 14 | ||||
-rw-r--r-- | src/loop-play.js | 56 | ||||
-rwxr-xr-x | src/play.js | 27 | ||||
-rw-r--r-- | todo.txt | 7 | ||||
-rw-r--r-- | yarn.lock | 4 |
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" |