diff options
-rw-r--r-- | README.md | 2 | ||||
-rw-r--r-- | backend.js | 4 | ||||
-rw-r--r-- | players.js | 38 | ||||
-rw-r--r-- | ui.js | 33 |
4 files changed, 72 insertions, 5 deletions
diff --git a/README.md b/README.md index 40c4d95..3efa0df 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ You're also welcome to share any ideas, suggestions, and questions through there * [: focus the main track/group listing * ]: focus the queue listing * Enter: play the selected track -* Ctrl+Up, p: play previous track +* Ctrl+Up, p: play previous track or seek to start of current track * Ctrl+Down, n: play next track * o: open the selected item through the system * Shift+Up/Down or drag: select multiple items at once diff --git a/backend.js b/backend.js index 048aec5..51419da 100644 --- a/backend.js +++ b/backend.js @@ -544,6 +544,10 @@ class QueuePlayer extends EventEmitter { this.player.seekTo(seconds) } + seekToStart() { + this.player.seekToStart() + } + togglePause() { this.player.togglePause() } diff --git a/players.js b/players.js index b41ce0c..c707494 100644 --- a/players.js +++ b/players.js @@ -41,6 +41,7 @@ class Player extends EventEmitter { seekAhead(secs) {} seekBack(secs) {} seekTo(timeInSecs) {} + seekToStart() {} volUp(amount) {} volDown(amount) {} setVolume(value) {} @@ -197,6 +198,10 @@ module.exports.ControllableMPVPlayer = class extends module.exports.MPVPlayer { this.sendCommand('seek', timeInSecs, 'absolute') } + seekToStart() { + this.seekTo(0) + } + volUp(amount) { this.setVolume(this.volume + amount) } @@ -261,6 +266,8 @@ module.exports.SoXPlayer = class extends Player { // You don't get keyboard controls such as seeking or volume adjusting // with SoX, though. + this._file = file + this.process = spawn('play', [file].concat( this.processOptions, startTime ? ['trim', startTime] : [] @@ -313,8 +320,39 @@ module.exports.SoXPlayer = class extends Player { return new Promise(resolve => { this.process.on('close', () => resolve()) + }).then(() => { + if (this._restartPromise) { + const p = this._restartPromise + this._restartPromise = null + return p + } }) } + + async seekToStart() { + // SoX doesn't support a command interface to interact while playback is + // ongoing. However, we can simulate seeking to the start by restarting + // playback altogether. We just need to be careful not to resolve the + // original playback promise before the new one is complete! + + if (!this._file) { + return + } + + let resolve = null + let reject = null + + // The original call of playFile() will yield control to this promise, which + // we bind to the resolve/reject of a new call to playFile(). + this._restartPromise = new Promise((res, rej) => { + resolve = res + reject = rej + }) + + await this.kill() + + this.playFile(this._file).then(resolve, reject) + } } module.exports.getPlayer = async function(name = null, options = []) { diff --git a/ui.js b/ui.js index 75dfe41..3601071 100644 --- a/ui.js +++ b/ui.js @@ -199,6 +199,7 @@ class AppElement extends FocusElement { canProcessMetadata: true, canSuspend: true, menubarColor: 4, // blue + seekToStartThreshold: 3, showTabberPane: true, stopPlayingUponQuit: true }, config) @@ -1190,15 +1191,39 @@ class AppElement extends FocusElement { } } + skipBackOrSeekToStart() { + // Perform the same action - skipping to the previous track or seeking to + // the start of the current track - for all target queue players. If any is + // past an arbitrary time position (default 3 seconds), seek to start; if + // all are before this position, skip to previous. + + let maxCurSec = 0 + this.forEachQueuePlayerToActOn(({ timeData }) => { + if (timeData) { + maxCurSec = Math.max(maxCurSec, timeData.curSecTotal) + } + }) + + if (Math.floor(maxCurSec) < this.config.seekToStartThreshold) { + this.actOnQueuePlayers(qp => qp.playPrevious(qp.playingTrack, true)) + } else { + this.actOnQueuePlayers(qp => qp.seekToStart()) + } + } + actOnQueuePlayers(fn) { - const actOn = this.queuePlayersToActOn.length ? this.queuePlayersToActOn : [this.SQP] - for (const queuePlayer of actOn) { + this.forEachQueuePlayerToActOn(queuePlayer => { fn(queuePlayer) const PIE = this.getPlaybackInfoElementForQueuePlayer(queuePlayer) if (PIE) { PIE.updateProgress() } - } + }) + } + + forEachQueuePlayerToActOn(fn) { + const actOn = this.queuePlayersToActOn.length ? this.queuePlayersToActOn : [this.SQP] + actOn.forEach(fn) } showMenuForItemElement(el, listing) { @@ -1564,7 +1589,7 @@ class AppElement extends FocusElement { } else if (input.isStop(keyBuf)) { this.actOnQueuePlayers(qp => qp.stopPlaying()) } else if (input.isSkipBack(keyBuf)) { - this.actOnQueuePlayers(qp => qp.playPrevious(qp.playingTrack, true)) + this.skipBackOrSeekToStart() } else if (input.isSkipAhead(keyBuf)) { this.actOnQueuePlayers(qp => qp.playNext(qp.playingTrack, true)) } |