From 0fcec41cd757adcd3c7f2f4c80b33141bb0a9bee Mon Sep 17 00:00:00 2001 From: liam4 Date: Mon, 26 Jun 2017 23:16:11 -0300 Subject: Remove downloaders --- src/download-playlist.js | 4 + src/downloaders.js | 86 ----------------- src/http-music.js | 28 +----- src/loop-play.js | 236 ++--------------------------------------------- 4 files changed, 15 insertions(+), 339 deletions(-) delete mode 100644 src/downloaders.js (limited to 'src') 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 - // Selects the mode that songs will be downloaded with. - // See downloaders.js. - - downloaderType = util.nextArg() - }, - '-play-opts': function(util) { // --play-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.") - } } } } -- cgit 1.3.0-6-gf8a5