« get me outta code hell

mtui - Music Text User Interface - user-friendly command line music player
about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--README.md2
-rw-r--r--backend.js4
-rw-r--r--players.js38
-rw-r--r--ui.js33
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))
       }