From 2c150dfedba07b95334bbf2211739b5614e4949d Mon Sep 17 00:00:00 2001 From: Liam Date: Fri, 14 Jul 2017 17:11:22 +0000 Subject: Return downloaders :sparkles: --- src/downloaders.js | 42 +++++++++++++++++++++- src/loop-play.js | 100 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 138 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/downloaders.js b/src/downloaders.js index 8fa830c..1de6730 100644 --- a/src/downloaders.js +++ b/src/downloaders.js @@ -1,6 +1,7 @@ 'use strict' const fs = require('fs') +const fse = require('fs-extra') const fetch = require('node-fetch') const promisifyProcess = require('./promisify-process') const tempy = require('tempy') @@ -11,6 +12,7 @@ const { spawn } = require('child_process') const { promisify } = require('util') const writeFile = promisify(fs.writeFile) +const copyFile = fse.copy function makeHTTPDownloader() { return function(arg) { @@ -42,6 +44,32 @@ function makeYouTubeDownloader() { } function makeLocalDownloader() { + // Usually we'd just return the given argument in a local + // downloader, which is efficient, since there's no need to + // copy a file from one place on the hard drive to another. + // But reading from a separate drive (e.g. a USB stick or a + // CD) can take a lot longer than reading directly from the + // computer's own drive, so this downloader copies the file + // to a temporary file on the computer's drive. + // Ideally, we'd be able to check whether a file is on the + // computer's main drive mount or not before going through + // the steps to copy, but I'm not sure if there's a way to + // do that (and it's even less likely there'd be a cross- + // platform way). + + return function(arg) { + const dir = tempy.directory() + // TODO: Is it necessary to sanitize here? + // haha, the answer to "should I sanitize" is probably always + // YES.. + const base = path.basename(arg, path.extname(arg)) + const file = dir + '/' + sanitize(base) + '.mp3' + return copyFile(arg, file) + .then(() => file) + } +} + +function makeLocalEchoDownloader() { return function(arg) { // Since we're grabbing the file from the local file system, there's no // need to download or copy it! @@ -72,7 +100,7 @@ module.exports = { makeLocalDownloader, makePowerfulDownloader, - getDownloader: downloaderType => { + getDownloaderFor: downloaderType => { if (downloaderType === 'http') { return makeHTTPDownloader() } else if (downloaderType === 'youtube') { @@ -82,5 +110,17 @@ module.exports = { } else { return null } + }, + + getDownloaderFor(arg) { + if (arg.startsWith('http://') || arg.startsWith('https://')) { + if (arg.includes('youtube.com')) { + return makeYouTubeDownloader() + } else { + return makeHTTPDownloader() + } + } else { + return makeLocalDownloader() + } } } diff --git a/src/loop-play.js b/src/loop-play.js index 10b1d5f..aca518a 100644 --- a/src/loop-play.js +++ b/src/loop-play.js @@ -3,24 +3,111 @@ const { spawn } = require('child_process') const FIFO = require('fifo-js') const EventEmitter = require('events') +const { getDownloaderFor } = require('./downloaders') + +class DownloadController extends EventEmitter { + waitForDownload() { + // Returns a promise that resolves when a download is + // completed. Note that this isn't necessarily the download + // that was initiated immediately before a call to + // waitForDownload (if any), since that download may have + // been canceled (see cancel). You can also listen for the + // 'downloaded' event instead. + + return new Promise(resolve => { + this.once('downloaded', file => resolve(file)) + }) + } + + async download(downloader, arg) { + // Downloads a file. This doesn't return anything; use + // waitForDownload to get the result of this. + // (The reasoning is that it's possible for a download to + // be canceled and replaced with a new download (see cancel) + // which would void the result of the old download.) + + let canceled = false + this.once('skipped', () => { + canceled = true + }) + + const file = await downloader(arg) + + if (!canceled) { + this.emit('downloaded', file) + } + } + + cancel() { + // Cancels the current download. This doesn't cancel any + // waitForDownload promises, though -- you'll need to start + // a new download to resolve those. + + this.emit('skipped') + } +} class PlayController { - constructor(picker) { + constructor(picker, downloadController) { this.currentTrack = null this.playArgs = [] this.process = null this.picker = picker + this.downloadController = downloadController } async loopPlay() { + let next + + let downloadNext = () => { + if (this.startNextDownload() !== null) { + return this.downloadController.waitForDownload().then(_next => { + next = _next + }) + } else { + next = null + return Promise.resolve() + } + } + + await downloadNext() + + while (next) { + await Promise.all([ + this.playFile(next), + downloadNext() + ]) + } + } + + startNextDownload() { + // TODO: Is there a method for this? + // TODO: Handle/test null return from picker. + const arg = this.picker()[1] + + if (arg === null) { + return null + } else { + const downloader = getDownloaderFor(arg) + this.downloadController.download(downloader, arg) + } + } + + async old_loopPlay() { // Playing music in a loop isn't particularly complicated; essentially, we // just want to keep picking and playing tracks until none is picked. let nextTrack = await this.picker() + await this.downloadManager.download(getDownloaderFor(nextTrack), nextTrack) + + let downloadNext + while (nextTrack) { this.currentTrack = nextTrack + this.downloadManager.download(getDownloaderFor(nextTrack), nextTrack) + await this.playFile(nextTrack[1]) nextTrack = await this.picker() @@ -35,7 +122,7 @@ class PlayController { file ]) - this.process.stderr.on('data', data => { + const handleData = data => { const match = data.toString().match( /(..):(..):(..) \/ (..):(..):(..) \(([0-9]+)%\)/ ) @@ -70,6 +157,12 @@ class PlayController { `\x1b[K~ (${percentStr}%) ${curStr} / ${lenStr}\r` ) } + } + + this.process.stderr.on('data', handleData) + + this.process.once('exit', () => { + this.process.stderr.removeListener('data', handleData) }) return new Promise(resolve => { @@ -136,7 +229,8 @@ module.exports = function loopPlay(picker, playArgs = []) { // function is null (or similar). Optionally takes a second argument // used as arguments to the `play` process (before the file name). - const playController = new PlayController(picker) + const downloadController = new DownloadController() + const playController = new PlayController(picker, downloadController) playController.playArgs = playArgs const promise = playController.loopPlay() -- cgit 1.3.0-6-gf8a5