« get me outta code hell

mtui - Music Text User Interface - user-friendly command line music player
about summary refs log tree commit diff
path: root/ui.js
diff options
context:
space:
mode:
Diffstat (limited to 'ui.js')
-rw-r--r--ui.js85
1 files changed, 80 insertions, 5 deletions
diff --git a/ui.js b/ui.js
index 12ef4b2..3fbaa51 100644
--- a/ui.js
+++ b/ui.js
@@ -1,8 +1,9 @@
 const { getAllCrawlersForArg } = require('./crawlers')
+const { getMetadataReaderFor } = require('./metadata-readers')
 const { getDownloaderFor } = require('./downloaders')
 const { getPlayer } = require('./players')
 const { parentSymbol, isGroup, isTrack, getItemPath, getItemPathString, flattenGrouplike, countTotalItems, shuffleOrderOfGroups, cloneGrouplike } = require('./playlist-utils')
-const { shuffleArray } = require('./general-util')
+const { shuffleArray, throttlePromise, getTimeStringsFromSec } = require('./general-util')
 const processSmartPlaylist = require('./smart-playlist')
 const UndoManager = require('./undo-manager')
 const RecordStore = require('./record-store')
@@ -40,6 +41,8 @@ class AppElement extends FocusElement {
     this.player = null
     this.recordStore = new RecordStore()
     this.undoManager = new UndoManager()
+    this.throttleMetadata = throttlePromise(10)
+    this.metadataDictionary = {}
     this.queueGrouplike = {name: 'Queue', isTheQueue: true, items: []}
     this.markGrouplike = {name: 'Marked', items: []}
     this.editMode = false
@@ -284,6 +287,7 @@ class AppElement extends FocusElement {
         {label: 'Queue!', action: emitControls(false)},
         {divider: true},
 
+        {label: 'Process metadata', action: () => this.processMetadata(item)},
         {label: 'Remove from queue', action: () => this.unqueueGrouplikeItem(item)}
       ]
     }
@@ -867,6 +871,26 @@ class AppElement extends FocusElement {
     this.stopPlaying()
     this.playbackInfoElement.clearInfo()
   }
+
+  processMetadata(item) {
+    if (isGroup(item)) {
+      return Promise.all(item.items.map(x => this.processMetadata(x)))
+    }
+
+    return this.throttleMetadata(async () => {
+      const filePath = await this.downloadGrouplikeItem(item)
+      const metadataReader = getMetadataReaderFor(filePath)
+      const data = await metadataReader(filePath)
+
+      this.metadataDictionary[item.downloaderArg] = filePath
+      this.metadataDictionary[filePath] = data
+    })
+  }
+
+  getMetadataFor(item) {
+    const key = this.metadataDictionary[item.downloaderArg]
+    return this.metadataDictionary[key] || null
+  }
 }
 
 class GrouplikeListingElement extends Form {
@@ -1033,6 +1057,10 @@ class GrouplikeListingElement extends Form {
         const itemElement = new InteractiveGrouplikeItemElement(item, this.app)
         this.addEventListeners(itemElement)
         form.addInput(itemElement)
+
+        if (this.grouplike.isTheQueue) {
+          itemElement.hideMetadata = true
+        }
       }
     } else if (!this.grouplike.isTheQueue) {
       form.addInput(new BasicGrouplikeItemElement('(This group is empty)'))
@@ -1213,7 +1241,10 @@ class BasicGrouplikeItemElement extends Button {
   constructor(text) {
     super()
 
+    this._text = this._rightText = ''
+
     this.text = text
+    this.rightText = ''
     this.drawText = ''
   }
 
@@ -1224,11 +1255,45 @@ class BasicGrouplikeItemElement extends Button {
     this.computeText()
   }
 
+  set text(val) {
+    if (this._text !== val) {
+      this._text = val
+      this.computeText()
+    }
+  }
+
+  get text() {
+    return this._text
+  }
+
+  set rightText(val) {
+    if (this._rightText !== val) {
+      this._rightText = val
+      this.computeText()
+    }
+  }
+
+  get rightText() {
+    return this._rightText
+  }
+
   computeText() {
     let text = ''
     let done = false
     let heckingWatchOut = false
 
+    // TODO: Hide right text if there's not enough columns (plus some padding)
+
+    // 3 = width of status line, basically
+    let w = this.w - this.x - 3
+
+    // Also make space for the right text - if we choose to show it.
+    const rightTextCols = ansi.measureColumns(this.rightText)
+    const showRightText = (w - rightTextCols > 12)
+    if (showRightText) {
+      w -= rightTextCols
+    }
+
     const writable = {
       write: characters => {
         if (heckingWatchOut && done) {
@@ -1237,8 +1302,7 @@ class BasicGrouplikeItemElement extends Button {
 
         for (const char of characters) {
           if (heckingWatchOut) {
-            // 3 = width of status line, basically
-            if (ansi.measureColumns(text + char) + 3 <= this.w - this.x) {
+            if (ansi.measureColumns(text + char) <= w) {
               text += char
             } else {
               done = true
@@ -1258,8 +1322,10 @@ class BasicGrouplikeItemElement extends Button {
     heckingWatchOut = false
 
     const width = ansi.measureColumns(this.text)
-    // again, 3 = width of status bar
-    writable.write(' '.repeat(Math.max(0, this.w - width - 3)))
+    writable.write(' '.repeat(Math.max(0, w - width)))
+    if (showRightText) {
+      writable.write(this.rightText)
+    }
     writable.write(ansi.resetAttributes())
 
     this.drawText = text
@@ -1422,9 +1488,18 @@ class InteractiveGrouplikeItemElement extends BasicGrouplikeItemElement {
     super(item.name)
     this.item = item
     this.app = app
+    this.hideMetadata = false
   }
 
   drawTo(writable) {
+    if (!this.hideMetadata) {
+      const metadata = this.app.getMetadataFor(this.item)
+      if (metadata) {
+        const durationString = getTimeStringsFromSec(0, metadata.duration).duration
+        this.rightText = ` (${durationString}) `
+      }
+    }
+
     this.text = this.item.name
     super.drawTo(writable)
   }