« get me outta code hell

better hiding track numbering in queue - mtui - Music Text User Interface - user-friendly command line music player
about summary refs log tree commit diff
diff options
context:
space:
mode:
author(quasar) nebula <towerofnix@gmail.com>2020-09-15 15:51:42 -0300
committer(quasar) nebula <towerofnix@gmail.com>2020-09-15 15:51:42 -0300
commit5d58951225dba66402fc113d390ec602043d263e (patch)
tree4ca919c4e1fd671258d94b44d442170d8cf4ce66
parentba828b4f12779c1ce356ea2c0d99bcb891a09aa0 (diff)
better hiding track numbering in queue
-rw-r--r--playlist-utils.js80
-rw-r--r--todo.txt2
-rw-r--r--ui.js7
3 files changed, 66 insertions, 23 deletions
diff --git a/playlist-utils.js b/playlist-utils.js
index 452b705..68cba56 100644
--- a/playlist-utils.js
+++ b/playlist-utils.js
@@ -520,36 +520,70 @@ function getTrackIndexInParent(track) {
 
 const nameWithoutTrackNumberSymbol = Symbol('Cached name without track number')
 function getNameWithoutTrackNumber(track) {
-  // Be lazy and reuse an old value if possible! Don't do this if the track's
-  // name has changed.
-  const [oldName, cachedValue] = track[nameWithoutTrackNumberSymbol] || []
-  if (cachedValue && track.name === oldName) {
-    return cachedValue
+  // A "part" is a series of numeric digits, separated from other parts by
+  // whitespace and dashes, always preceding either the first non-numeric/
+  // separator character or (if there are no such characters) the first word
+  // (i.e. last whitespace).
+  const getNumberOfParts = ({ name }) => {
+    const match = name.match(/[^0-9\-\s]/)
+    if (match) {
+      name = name.slice(0, match.index)
+    } else if (name.includes(' ')) {
+      name = name.slice(0, name.lastIndexOf(' '))
+    } else {
+      return 0
+    }
+    name = name.replace(/[\-\s]+$/, '')
+    return name.split(/[\-\s]+/g).length
+  }
+
+  const removeParts = (name, numParts) => {
+    const regex = new RegExp(`([0-9]+[\\-\\s]+){${numParts},${numParts}}`)
+    return track.name.replace(regex, '')
   }
 
-  // This is the expression that matches a track number at the start of
-  // a track's title.
-  const regex = /^[0-9\-\s]+/
+  // Despite this function returning a single string for one track, that value
+  // depends on the names of all other tracks under the same parent. We still
+  // store individual track -> name data on the track object, but the parent
+  // gets an additional cache for the names of its children tracks as well as
+  // the number of "parts" (the value directly based upon those names, and
+  // useful in computing the name data for other children tracks).
 
-  // First we need to determine whether every track in the group even starts
-  // with a track number.
   const parent = track[parentSymbol]
   if (parent) {
-    const names = parent.items.filter(isTrack).map(t => t.name)
-    if (names.some(name => !regex.test(name))) {
-      // If any of the names don't match the track number regex, just return
-      // the track name unmodified.
-      return track.name
+    const [trackNames, cachedNumParts] = parent[nameWithoutTrackNumberSymbol] || []
+    const tracks = parent.items.filter(isTrack)
+    if (trackNames && tracks.length === trackNames.length && tracks.every((t, i) => t.name === trackNames[i])) {
+      const [, oldName, oldNumParts, cachedValue] = track[nameWithoutTrackNumberSymbol] || []
+      if (cachedValue && track.name === oldName && cachedNumParts === oldNumParts) {
+        return cachedValue
+      } else {
+        // Individual track cache outdated.
+        const value = removeParts(track.name, cachedNumParts)
+        track[nameWithoutTrackNumberSymbol] = [true, track.name, cachedNumParts, value]
+        return value
+      }
+    } else {
+      // Group (parent) cache outdated.
+      const numParts = Math.min(...tracks.map(getNumberOfParts))
+      parent[nameWithoutTrackNumberSymbol] = [tracks.map(t => t.name), numParts]
+      // Parent changed so track cache changed is outdated too.
+      const value = removeParts(track.name, numParts)
+      track[nameWithoutTrackNumberSymbol] = [true, track.name, numParts, value]
+      return value
+    }
+  } else {
+    const [oldHadParent, oldName, , cachedValue] = track[nameWithoutTrackNumberSymbol] || []
+    if (cachedValue && !oldHadParent && track.name === oldName) {
+      return cachedValue
+    } else {
+      // Track cache outdated.
+      const numParts = getNumberOfParts(track)
+      const value = removeParts(track.name, numParts)
+      track[nameWithoutTrackNumberSymbol] = [false, track.name, numParts, value]
+      return value
     }
   }
-
-  // Now actually perform the replacement to get rid of the track number!
-  const value = track.name.replace(regex, '')
-
-  // Cache the value, so we don't need to do this whole process again.
-  track[nameWithoutTrackNumberSymbol] = [track.name, value]
-
-  return value
 }
 
 function isGroup(obj) {
diff --git a/todo.txt b/todo.txt
index c58c1cb..b210799 100644
--- a/todo.txt
+++ b/todo.txt
@@ -462,9 +462,11 @@ TODO: Only count *consistently* formatted text, across all tracks in a group,
       it is the only track in the group which is formatted '## # <text>'.
       It does follow the formatting '## <text>' as all other tracks do, so only
       the first digits, and following whitespace, should be removed.
+      (Done!)
 
 TODO: Related to the above - always keep at least one word. See CANSLP's new
       release "untitled folder", with tracks named "01 1", "02 2", etc.
+      (Done!)
 
 TODO: Update to work with IPC server mpv (and socat).
       (Done!)
diff --git a/ui.js b/ui.js
index de0b214..5e6ef6e 100644
--- a/ui.js
+++ b/ui.js
@@ -60,6 +60,7 @@ const TuiTextEditor = require('tui-text-editor')
 
 const { promisify } = require('util')
 const { spawn } = require('child_process')
+const { orderBy } = require('natural-orderby')
 const fs = require('fs')
 const open = require('open')
 const path = require('path')
@@ -317,6 +318,7 @@ class AppElement extends FocusElement {
       {value: 'shuffle-groups', label: 'Shuffle order of groups'},
       {value: 'reverse', label: 'Reverse all'},
       {value: 'reverse-groups', label: 'Reverse order of groups'},
+      {value: 'alphabetic', label: 'Alphabetically'},
       {value: 'normal', label: 'In order'}
     ], this.showContextMenu)
 
@@ -1475,6 +1477,11 @@ class AppElement extends FocusElement {
         item = {items: flattenGrouplike(item).items.reverse()}
       } else if (order === 'reverse-groups') {
         item = reverseOrderOfGroups(item)
+      } else if (order === 'alphabetic') {
+        item = {
+          name: `${oldName} (alphabetic)`,
+          items: orderBy(flattenGrouplike(item).items, getNameWithoutTrackNumber)
+        }
       }
     } else {
       // Make it into a grouplike that just contains itself.