« get me outta code hell

http-music - Command-line music player + utils (not a server!)
about summary refs log tree commit diff
diff options
context:
space:
mode:
-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
 }