From e9ccfa2fd4221ddff4950d5180ee5c8fb0bf8117 Mon Sep 17 00:00:00 2001 From: Florrie Date: Mon, 25 Feb 2019 12:06:27 -0400 Subject: Metadata (stored, throttle, status, and more) --- general-util.js | 6 ++++- index.js | 9 +++++++ metadata-readers.js | 24 +++++++++++++++-- todo.txt | 11 +++++++- ui.js | 75 +++++++++++++++++++++++++++++++++++++++++++++-------- 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) { -- cgit 1.3.0-6-gf8a5