« get me outta code hell

Metadata (stored, throttle, status, and more) - mtui - Music Text User Interface - user-friendly command line music player
about summary refs log tree commit diff
diff options
context:
space:
mode:
authorFlorrie <towerofnix@gmail.com>2019-02-25 12:06:27 -0400
committerFlorrie <towerofnix@gmail.com>2019-02-25 12:06:27 -0400
commite9ccfa2fd4221ddff4950d5180ee5c8fb0bf8117 (patch)
tree917fb878b8ba0166bb324f3781a756d516a639ca
parent75251bb2309505c20dc7500117a17649d41412d8 (diff)
Metadata (stored, throttle, status, and more)
-rw-r--r--general-util.js6
-rwxr-xr-xindex.js9
-rw-r--r--metadata-readers.js24
-rw-r--r--todo.txt11
-rw-r--r--ui.js75
5 files changed, 110 insertions, 15 deletions
diff --git a/general-util.js b/general-util.js
index 708e150..a7bfb11 100644
--- a/general-util.js
+++ b/general-util.js
@@ -111,7 +111,7 @@ module.exports.throttlePromise = function(maximumAtOneTime = 10) {
     })
   }
 
-  return function(callback) {
+  const enqueue = function(callback) {
     if (activeCount >= maximumAtOneTime) {
       return new Promise((resolve, reject) => {
         queue.push(function() {
@@ -122,6 +122,10 @@ module.exports.throttlePromise = function(maximumAtOneTime = 10) {
       return execute(callback)
     }
   }
+
+  enqueue.queue = queue
+
+  return enqueue
 }
 
 module.exports.getTimeStringsFromSec = function(curSecTotal, lenSecTotal) {
diff --git a/index.js b/index.js
index e46bfa0..6a71611 100755
--- a/index.js
+++ b/index.js
@@ -22,7 +22,16 @@ process.stdout.setMaxListeners(Infinity)
 process.stderr.setMaxListeners(Infinity)
 
 process.on('unhandledRejection', error => {
+  console.error(ansi.setForeground(ansi.C_RED) + "** There was an uncatched error! **" + ansi.resetAttributes())
+  console.error("Don't worry, your music files are all okay.")
+  console.error("This just means there was a bug in mtui.")
+  console.error("In order to verify that the program won't run weirdly, it has stopped.")
+  console.error(ansi.setForeground(ansi.C_RED) + "Error stack:" + ansi.resetAttributes())
   console.error(error.stack)
+  console.error(ansi.setForeground(ansi.C_RED) + "Error object:" + ansi.resetAttributes())
+  console.error(error)
+  console.error("(End of error log.)")
+  process.stdout.write(ansi.cleanCursor())
   process.exit(1)
 })
 
diff --git a/metadata-readers.js b/metadata-readers.js
index 1e6eb1b..64f413a 100644
--- a/metadata-readers.js
+++ b/metadata-readers.js
@@ -1,8 +1,28 @@
 const { promisifyProcess } = require('./general-util')
 const { spawn } = require('child_process')
 
+// Some probers are sorta inconsistent; this function lets them try again if
+// they fail the first time.
+const tryAgain = function(times, func) {
+  return async function(...args) {
+    let n = 0
+    let ret
+    while (!ret && n < times) {
+      try {
+        ret = await func(...args)
+      } catch (error) {
+        if (n + 1 === times) {
+          throw error
+        }
+      }
+      n++
+    }
+    return ret
+  }
+}
+
 const metadataReaders = {
-  ffprobe: async filePath => {
+  ffprobe: tryAgain(6, async filePath => {
     const ffprobe = spawn('ffprobe', [
       '-print_format', 'json',
       '-show_entries', 'stream=codec_name:format',
@@ -36,7 +56,7 @@ const metadataReaders = {
       fileSize: parseInt(data.format.size),
       bitrate: parseInt(data.format.bit_rate)
     }
-  },
+  }),
 
   getMetadataReaderFor: arg => {
     return metadataReaders.ffprobe
diff --git a/todo.txt b/todo.txt
index c47b815..c0a00e1 100644
--- a/todo.txt
+++ b/todo.txt
@@ -199,10 +199,19 @@ TODO: Metadata, in memory.
       (Done!)
 
 TODO: Load metadata from storage.
+      (Done!)
 
-TODO: Restore metadata, if it's recognized as similar to an old path?
+TODO: Restore metadata, if it's recognized as similar to an old path.
+      (Basically: How do we deal with moving files around? We'll also want some
+      sort of a manager to get rid of unused metadata, if wanted..... on the
+      one hand, it'll be saving precious kilobytes, but on the other, people
+      might not want to keep a record of moved or deleted tracks at all, so it
+      could actually be useful.)
 
 TODO: Don't store duplicate metadata entries (prereq for tags, custom metadata,
       etc) for the same track. Do it symlink-style -- map downloader arg to
       actual key used for metadata (downloaded file path).
       (Done!)
+
+TODO: Metadata process status bar.
+      (Done!)
diff --git a/ui.js b/ui.js
index 3fbaa51..90fb13a 100644
--- a/ui.js
+++ b/ui.js
@@ -32,6 +32,7 @@ const {
 
 const fs = require('fs')
 const { promisify } = require('util')
+const readFile = promisify(fs.readFile)
 const writeFile = promisify(fs.writeFile)
 
 class AppElement extends FocusElement {
@@ -48,6 +49,9 @@ class AppElement extends FocusElement {
     this.editMode = false
 
     this.rootDirectory = process.env.HOME + '/.mtui'
+    this.metadataPath = this.rootDirectory + '/track-metadata.json'
+
+    this.loadMetadata()
 
     this.paneLeft = new Pane()
     this.addChild(this.paneLeft)
@@ -58,6 +62,10 @@ class AppElement extends FocusElement {
     this.tabber = new Tabber()
     this.paneLeft.addChild(this.tabber)
 
+    this.metadataStatusLabel = new Label()
+    this.metadataStatusLabel.visible = false
+    this.paneLeft.addChild(this.metadataStatusLabel)
+
     this.newGrouplikeListing()
 
     this.queueListingElement = new QueueListingElement(this)
@@ -287,7 +295,8 @@ class AppElement extends FocusElement {
         {label: 'Queue!', action: emitControls(false)},
         {divider: true},
 
-        {label: 'Process metadata', action: () => this.processMetadata(item)},
+        {label: 'Process metadata (new entries)', action: () => this.processMetadata(item, false)},
+        {label: 'Process metadata (reprocess)', action: () => this.processMetadata(item, true)},
         {label: 'Remove from queue', action: () => this.unqueueGrouplikeItem(item)}
       ]
     }
@@ -391,7 +400,14 @@ class AppElement extends FocusElement {
     this.playbackPane.h = this.contentH - this.playbackPane.y
 
     this.tabber.fillParent()
+
+    if (this.metadataStatusLabel.visible) {
+      this.tabber.h--
+      this.metadataStatusLabel.y = this.paneLeft.contentH - 1
+    }
+
     this.tabber.fixLayout()
+
     this.queueListingElement.fillParent()
     this.playbackInfoElement.fillParent()
   }
@@ -872,19 +888,56 @@ class AppElement extends FocusElement {
     this.playbackInfoElement.clearInfo()
   }
 
-  processMetadata(item) {
-    if (isGroup(item)) {
-      return Promise.all(item.items.map(x => this.processMetadata(x)))
+  async readMetadata() {
+    try {
+      return JSON.parse(await readFile(this.metadataPath))
+    } catch (error) {
+      // Just stop. It's okay to fail to load metadata.
+      return null
     }
+  }
 
-    return this.throttleMetadata(async () => {
-      const filePath = await this.downloadGrouplikeItem(item)
-      const metadataReader = getMetadataReaderFor(filePath)
-      const data = await metadataReader(filePath)
+  async loadMetadata() {
+    Object.assign(this.metadataDictionary, await this.readMetadata())
+  }
 
-      this.metadataDictionary[item.downloaderArg] = filePath
-      this.metadataDictionary[filePath] = data
-    })
+  async saveMetadata() {
+    const newData = Object.assign({}, await this.readMetadata(), this.metadataDictionary)
+    await writeFile(this.metadataPath, JSON.stringify(newData))
+  }
+
+  async processMetadata(item, reprocess = false, top = true) {
+    if (top) {
+      this.metadataStatusLabel.text = 'Processing metadata...'
+      this.metadataStatusLabel.visible = true
+      this.fixLayout()
+    }
+
+    if (isGroup(item)) {
+      await Promise.all(item.items.map(x => this.processMetadata(x, reprocess, false)))
+    } else {
+      if (!reprocess && this.getMetadataFor(item)) {
+        return
+      }
+
+      await 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
+      })
+
+      this.metadataStatusLabel.text = `Processing metadata - ${this.throttleMetadata.queue.length} to go.`
+    }
+
+    if (top) {
+      this.metadataStatusLabel.text = ''
+      this.metadataStatusLabel.visible = false
+      this.fixLayout()
+      await this.saveMetadata()
+    }
   }
 
   getMetadataFor(item) {