« get me outta code hell

Add track number to status line and make status line more consistent - http-music - Command-line music player + utils (not a server!)
about summary refs log tree commit diff
diff options
context:
space:
mode:
authorFlorrie <towerofnix@gmail.com>2017-10-29 15:55:47 -0300
committerFlorrie <towerofnix@gmail.com>2017-10-29 15:55:47 -0300
commit5ccf456d9ba332d587c97e8724a5b767e96a5802 (patch)
treef23324d40607d5239554cab70ff6f65dc9db8e4e
parent3b2c76544e67255698ca8b106b86064702f25f47 (diff)
Add track number to status line and make status line more consistent
This does two big things - there's a few changes in pickers.js to support
track numbers (and a new supporting function in playlist-utils.js), and
some changes in loop-play.js to make the status line formatting consistent
regardless of the active player.
-rw-r--r--src/loop-play.js100
-rw-r--r--src/pickers.js146
-rw-r--r--src/playlist-utils.js27
3 files changed, 204 insertions, 69 deletions
diff --git a/src/loop-play.js b/src/loop-play.js
index 9724f7c..01e1574 100644
--- a/src/loop-play.js
+++ b/src/loop-play.js
@@ -23,8 +23,14 @@ const {
   getItemPathString, safeUnlink, parentSymbol
 } = require('./playlist-utils')
 
-class Player {
+function createStatusLine({percentStr, curStr, lenStr}) {
+  return `(${percentStr}) ${curStr} / ${lenStr}`
+}
+
+class Player extends EventEmitter {
   constructor() {
+    super()
+
     this.disablePlaybackStatus = false
   }
 
@@ -35,6 +41,15 @@ class Player {
   volDown(amount) {}
   togglePause() {}
   kill() {}
+
+  printStatusLine(str) {
+    // Quick sanity check - we don't want to print the status line if it's
+    // disabled! Hopefully printStatusLine won't be called in that case, but
+    // if it is, we should be careful.
+    if (!this.disablePlaybackStatus) {
+      this.emit('printStatusLine', str)
+    }
+  }
 }
 
 class MPVPlayer extends Player {
@@ -83,9 +98,7 @@ class MPVPlayer extends Player {
         const percentVal = (100 / lenSecTotal) * curSecTotal
         const percentStr = (Math.trunc(percentVal * 100) / 100).toFixed(2)
 
-        process.stdout.write(
-          `\x1b[K~ (${percentStr}%) ${curStr} / ${lenStr}\r`
-        )
+        this.printStatusLine(createStatusLine({percentStr, curStr, lenStr}))
       }
     })
 
@@ -168,9 +181,44 @@ class SoXPlayer extends Player {
         if (this.disablePlaybackStatus) {
           return
         }
-      }
 
-      process.stdout.write(data.toString())
+        const timeRegex = '([0-9]*):([0-9]*):([0-9]*)\.([0-9]*)'
+        const match = data.toString().trim().match(new RegExp(
+          `^In:([0-9.]+%)\\s*${timeRegex}\\s*\\[${timeRegex}\\]`
+        ))
+
+        if (match) {
+          const percentStr = match[1]
+
+          const [
+            curHour, curMin, curSec, curSecFrac, // ##:##:##.##
+            remHour, remMin, remSec, remSecFrac // ##:##:##.##
+          ] = match.slice(2).map(n => parseInt(n))
+
+          const duration = Math.round(
+            (curHour + remHour) * 3600 +
+            (curMin + remMin) * 60 +
+            (curSec + remSec) * 1
+          )
+
+          const lenHour = Math.floor(duration / 3600)
+          const lenMin = Math.floor((duration - lenHour * 3600) / 60)
+          const lenSec = Math.floor(duration - lenHour * 3600 - lenMin * 60)
+
+          let curStr, lenStr
+          const pad = val => val.toString().padStart(2, '0')
+
+          if (lenHour > 0) {
+            curStr = `${curHour}:${pad(curMin)}:${pad(curSec)}`
+            lenStr = `${lenHour}:${pad(lenMin)}:${pad(lenSec)}`
+          } else {
+            curStr = `${curMin}:${pad(curSec)}`
+            lenStr = `${lenMin}:${pad(lenSec)}`
+          }
+
+          this.printStatusLine(createStatusLine({percentStr, curStr, lenStr}))
+        }
+      }
     })
 
     return new Promise(resolve => {
@@ -311,6 +359,46 @@ class PlayController extends EventEmitter {
     this.stopped = false
     this.shouldMoveNext = true
     this.failedCount = 0
+
+    this.player.on('printStatusLine', playerString => {
+      let fullStatusLine = ''
+
+      const track = this.currentTrack
+
+      if (track) {
+        if (track.overallTrackIndex || track.groupTrackIndex) {
+          fullStatusLine += '('
+
+          if (track.overallTrackIndex) {
+            const [ cur, len ] = track.overallTrackIndex
+            fullStatusLine += `${cur + 1} / ${len}`
+
+            if (track.groupTrackIndex) {
+              fullStatusLine += ' [All]; '
+            }
+          }
+
+          if (track.groupTrackIndex) {
+            const [ cur, len ] = track.groupTrackIndex
+            fullStatusLine += `${cur + 1} / ${len}`
+
+            if (track.overallTrackIndex) {
+              fullStatusLine += ' [Group]'
+            }
+          }
+
+          fullStatusLine += ') '
+        }
+      }
+
+      fullStatusLine += playerString
+
+      // Carriage return - moves the cursor back to the start of the line,
+      // so that the next status line is printed on top of this one.
+      fullStatusLine += '\r'
+
+      process.stdout.write(fullStatusLine)
+    })
   }
 
   async loopPlay() {
diff --git a/src/pickers.js b/src/pickers.js
index 1afa0c2..41eed53 100644
--- a/src/pickers.js
+++ b/src/pickers.js
@@ -15,7 +15,8 @@ const _seedRandom = require('seed-random')
 // Uncertain on how to handle serialization of tracks.. some tracks may appear twice in the same playlist (or two tracks of the same name appear); in this case the serialized path to the two track appearances is the same, when they really refer to two separate instances of the track within the playlist. Could track serialization instead be index-based (rather than name-based)..?
 
 const {
-  flattenGrouplike, isGroup, updatePlaylistFormat, isSameTrack
+  flattenGrouplike, isGroup, updatePlaylistFormat, isSameTrack, oldSymbol,
+  getTrackIndexInParent
 } = require('./playlist-utils')
 
 class HistoryController {
@@ -169,6 +170,9 @@ function sortFlattenGrouplike(grouplike, sort, getRandom) {
 const playlistCache = Symbol('Cache of indexed playlist')
 
 function generalPicker(sourcePlaylist, lastTrack, options) {
+  // (Track 3/5 [2712])   -- Track (CUR/GROUP [ALL])
+  // (Track 3/2712)       -- Track (CUR/ALL)
+
   const { sort, loop } = options
 
   if (![
@@ -214,80 +218,98 @@ function generalPicker(sourcePlaylist, lastTrack, options) {
 
   let index
 
-  if (lastTrack !== null) {
-    // The "current" version of the last track (that is, the object
-    // representing this track which appears in the flattened/updated/cached
-    // playlist).
-    const currentLastTrack = playlist.items.find(
-      t => isSameTrack(t, lastTrack)
-    )
+  decideTrackIndex: {
+    if (lastTrack !== null) {
+      // The "current" version of the last track (that is, the object
+      // representing this track which appears in the flattened/updated/cached
+      // playlist).
+      const currentLastTrack = playlist.items.find(
+        t => isSameTrack(t, lastTrack)
+      )
 
-    index = playlist.items.indexOf(currentLastTrack)
-  } else {
-    index = -1
-  }
+      index = playlist.items.indexOf(currentLastTrack)
+    } else {
+      index = -1
+    }
 
-  if (index === -1) {
-    return playlist.items[0]
-  }
+    if (index === -1) {
+      index = 0
+      break decideTrackIndex
+    }
 
-  if (index + 1 === playlist.items.length) {
-    if (loop === 'loop-same-order' || loop === 'loop') {
-      return playlist.items[0]
+    if (index + 1 === playlist.items.length) {
+      if (loop === 'loop-same-order' || loop === 'loop') {
+        index = 0
+        break decideTrackIndex
+      }
+
+      if (loop === 'loop-regenerate') {
+        // Deletes the random number generation seed then starts over. Assigning
+        // a new RNG seed makes it so we get a new shuffle the next time, and
+        // clearing the lastTrack value makes generalPicker thinks we're
+        // starting over. We also need to destroy the playlistCache, or else it
+        // won't actually recalculate the list.
+        const newSeed = makeGetRandom(options.seed)()
+        options.seed = newSeed
+        delete options[playlistCache]
+        return generalPicker(sourcePlaylist, null, options)
+      }
+
+      if (loop === 'no-loop' || loop === 'no') {
+        // Returning null means the picker is done picking.
+        return null
+      }
     }
 
-    if (loop === 'loop-regenerate') {
-      // Deletes the random number generation seed then starts over. Assigning
-      // a new RNG seed makes it so we get a new shuffle the next time, and
-      // clearing the lastTrack value makes generalPicker thinks we're
-      // starting over. We also need to destroy the playlistCache, or else it
-      // won't actually recalculate the list.
-      const newSeed = makeGetRandom(options.seed)()
-      options.seed = newSeed
-      delete options[playlistCache]
-      return generalPicker(sourcePlaylist, null, options)
+    if (index + 1 > playlist.items.length) {
+      throw new Error(
+        "Picker index is greater than total item count?" +
+        `(${index + 1} > ${playlist.items.length}`
+      )
     }
 
-    if (loop === 'no-loop' || loop === 'no') {
-      // Returning null means the picker is done picking.
-      return null
+    if (index + 1 < playlist.items.length) {
+      // Pick-random is a special exception - in this case we don't actually
+      // care about the value of the index variable; instead we just pick a
+      // random track from the generated top level.
+      //
+      // Loop=pick-random is different from sort=shuffle. Sort=shuffle always
+      // ensures the same song doesn't play twice in a single shuffle. It's
+      // like how when you shuffle a deck of cards, you'll still never pick
+      // the same card twice, until you go all the way through the deck and
+      // re-shuffle the deck!
+      //
+      // Loop=pick-random instead picks a random track every time the picker
+      // is called. It's more like you reshuffle the complete deck every time
+      // you pick something.
+      //
+      // Now, how should pick-random work when dealing with groups, such as
+      // when using sort=shuffle-groups? (If I can't find a solution, I'd say
+      // that's alright.)
+      /*
+      if (loop === 'pick-random') {
+        const pickedIndex = Math.floor(Math.random() * topLevel.items.length)
+        return topLevel.items[pickedIndex]
+      }
+      */
+
+      index += 1
+      break decideTrackIndex
     }
   }
 
-  if (index + 1 > playlist.items.length) {
-    throw new Error(
-      "Picker index is greater than total item count?" +
-      `(${index + 1} > ${playlist.items.length}`
-    )
-  }
+  const oldItem = playlist.items[index]
+  const item = Object.assign({}, oldItem, {[oldSymbol]: oldItem})
 
-  if (index + 1 < playlist.items.length) {
-    // Pick-random is a special exception - in this case we don't actually
-    // care about the value of the index variable; instead we just pick a
-    // random track from the generated top level.
-    //
-    // Loop=pick-random is different from sort=shuffle. Sort=shuffle always
-    // ensures the same song doesn't play twice in a single shuffle. It's
-    // like how when you shuffle a deck of cards, you'll still never pick
-    // the same card twice, until you go all the way through the deck and
-    // re-shuffle the deck!
-    //
-    // Loop=pick-random instead picks a random track every time the picker
-    // is called. It's more like you reshuffle the complete deck every time
-    // you pick something.
-    //
-    // Now, how should pick-random work when dealing with groups, such as
-    // when using sort=shuffle-groups? (If I can't find a solution, I'd say
-    // that's alright.)
-    /*
-    if (loop === 'pick-random') {
-      const pickedIndex = Math.floor(Math.random() * topLevel.items.length)
-      return topLevel.items[pickedIndex]
-    }
-    */
+  item.overallTrackIndex = [index, playlist.items.length]
 
-    return playlist.items[index + 1]
+  if (
+    ['order', 'ordered', 'shuffle-groups', 'shuffled-groups'].includes(sort)
+  ) {
+    item.groupTrackIndex = getTrackIndexInParent(item)
   }
+
+  return item
 }
 
 module.exports = {HistoryController, generalPicker}
diff --git a/src/playlist-utils.js b/src/playlist-utils.js
index 013f56a..c2f6aae 100644
--- a/src/playlist-utils.js
+++ b/src/playlist-utils.js
@@ -453,6 +453,30 @@ function isSameTrack(track1, track2) {
   return false
 }
 
+function getTrackIndexInParent(track) {
+  if (parentSymbol in track === false) {
+    throw new Error(
+      'getTrackIndexInParent called with a track that has no parent!'
+    )
+  }
+
+  const parent = track[parentSymbol]
+
+  let i = 0, foundTrack = false;
+  for (; i < parent.items.length; i++) {
+    if (isSameTrack(track, parent.items[i])) {
+      foundTrack = true
+      break
+    }
+  }
+
+  if (foundTrack === false) {
+    return [-1, parent.items.length]
+  } else {
+    return [i, parent.items.length]
+  }
+}
+
 function isGroup(obj) {
   return !!(obj && obj.items)
 
@@ -501,7 +525,7 @@ async function safeUnlink(file, playlist) {
 }
 
 module.exports = {
-  parentSymbol,
+  parentSymbol, oldSymbol,
   updatePlaylistFormat, updateTrackFormat,
   flattenGrouplike,
   partiallyFlattenGrouplike, collapseGrouplike,
@@ -512,6 +536,7 @@ module.exports = {
   getItemPathString,
   parsePathString,
   isSameTrack,
+  getTrackIndexInParent,
   isGroup, isTrack,
   safeUnlink
 }