« get me outta code hell

WIP(?) metadata processing tool - http-music - Command-line music player + utils (not a server!)
about summary refs log tree commit diff
diff options
context:
space:
mode:
authorFlorrie <towerofnix@gmail.com>2018-01-05 23:20:58 -0400
committerFlorrie <towerofnix@gmail.com>2018-01-05 23:20:58 -0400
commit18435f58f82849dcc86ab2042491828b2873b39a (patch)
tree07c912b688ba979b2ba96bdb3f17f7b390642cec
parent6f640a0b8e8e5b26a266f4680a626a629d3c7944 (diff)
WIP(?) metadata processing tool
-rwxr-xr-xsrc/cli.js1
-rwxr-xr-xsrc/download-playlist.js9
-rw-r--r--src/general-util.js16
-rw-r--r--src/process-metadata.js108
-rw-r--r--todo.txt12
5 files changed, 140 insertions, 6 deletions
diff --git a/src/cli.js b/src/cli.js
index 9021cc6..095757f 100755
--- a/src/cli.js
+++ b/src/cli.js
@@ -23,6 +23,7 @@ async function main(args) {
     switch (args[0]) {
       case 'play': script = require('./play'); break
       case 'download-playlist': script = require('./download-playlist'); break
+      case 'process-metadata': script = require('./process-metadata'); break
       case 'smart-playlist': script = require('./smart-playlist'); break
       case 'setup': script = require('./setup'); break
 
diff --git a/src/download-playlist.js b/src/download-playlist.js
index b41c240..852cb64 100755
--- a/src/download-playlist.js
+++ b/src/download-playlist.js
@@ -12,6 +12,7 @@ const {
 } = require('./playlist-utils')
 
 const { getDownloaderFor, makePowerfulDownloader } = require('./downloaders')
+const { showTrackProcessStatus } = require('./general-util')
 const { promisify } = require('util')
 const { spawn } = require('child_process')
 
@@ -24,12 +25,8 @@ async function downloadCrawl(playlist, topOut = './out/') {
   const flat = flattenGrouplike(playlist)
   let doneCount = 0
 
-  const showStatus = function() {
-    const total = flat.items.length
-    const percent = Math.trunc(doneCount / total * 10000) / 100
-    console.log(
-      `\x1b[1mDownload crawler - ${percent}% completed ` +
-      `(${doneCount}/${total} tracks)\x1b[0m`)
+  const showStatus = () => {
+    showTrackProcessStatus(flat.items.length, doneCount)
   }
 
   // First off, we go through all tracks and see which are already downloaded.
diff --git a/src/general-util.js b/src/general-util.js
new file mode 100644
index 0000000..67f53e3
--- /dev/null
+++ b/src/general-util.js
@@ -0,0 +1,16 @@
+module.exports.showTrackProcessStatus = function(
+  total, doneCount, noLineBreak = false
+) {
+  // Log a status line which tells how many tracks are processed and what
+  // percent is completed. (Uses non-specific language: it doesn't say
+  // "how many tracks downloaded" or "how many tracks processed", but
+  // rather, "how many tracks completed".) Pass noLineBreak = true to skip
+  // the \n character (you'll probably also want to log \r after).
+
+  const percent = Math.trunc(doneCount / total * 10000) / 100
+  process.stdout.write(
+    `\x1b[1m${percent}% completed ` +
+    `(${doneCount}/${total} tracks)\x1b[0m` +
+    (noLineBreak ? '' : '\n')
+  )
+}
diff --git a/src/process-metadata.js b/src/process-metadata.js
new file mode 100644
index 0000000..43ffe62
--- /dev/null
+++ b/src/process-metadata.js
@@ -0,0 +1,108 @@
+const fs = require('fs')
+const processArgv = require('./process-argv')
+const promisifyProcess = require('./promisify-process')
+const { spawn } = require('child_process')
+const { promisify } = require('util')
+const { showTrackProcessStatus } = require('./general-util')
+const { updatePlaylistFormat, flattenGrouplike } = require('./playlist-utils')
+
+const readFile = promisify(fs.readFile)
+const writeFile = promisify(fs.writeFile)
+
+async function probe(filePath) {
+  const ffprobe = spawn('ffprobe', [
+    '-print_format', 'json',
+    '-show_entries', 'stream=codec_name:format',
+    '-select_streams', 'a:0',
+    '-v', 'quiet',
+    filePath
+  ])
+
+  let probeDataString = ''
+
+  ffprobe.stdout.on('data', data => {
+    probeDataString += data
+  })
+
+  await promisifyProcess(ffprobe, false)
+
+  return JSON.parse(probeDataString)
+}
+
+async function main(args) {
+  if (args.length < 2) {
+    console.error('Usage: http-music process-metadata <in> <out> (..args..)')
+    console.error('See \x1b[1mman http-music-process-metadata\x1b[0m!')
+    return false
+  }
+
+  const inFile = args[0]
+  const outFile = args[1]
+
+  // Whether or not to save actual audio tag data. (This includes things like
+  // genre, track #, and album, as well as any non-standard data set on the
+  // file.)
+  let saveTags = false
+
+  // Whether or not to skip tracks which have already been processed.
+  let skipCompleted = true
+
+  await processArgv(args.slice(1), {
+    '-save-tags': function() {
+      saveTags = true
+    },
+
+    '-tags': util => util.alias('-save-tags'),
+    't': util => util.alias('-save-tags'),
+
+    '-skip-completed': function() {
+      skipCompleted = true
+    },
+
+    '-skip-done': util => util.alias('-skip-completed'),
+    '-faster': util => util.alias('-skip-completed'),
+
+    '-no-skip-completed': function() {
+      skipCompleted = false
+    },
+
+    '-no-skip-done': util => util.alias('-no-skip-completed'),
+    '-slower': util => util.alias('-no-skip-completed')
+  })
+
+  let doneCount = 0
+
+  const playlist = updatePlaylistFormat(JSON.parse(await readFile(args[0])))
+
+  const flattened = flattenGrouplike(playlist)
+  for (const item of flattened.items) {
+    if (!(skipCompleted && 'metadata' in item)) {
+      const probeData = await probe(item.downloaderArg)
+
+      item.metadata = Object.assign(item.metadata || {}, {
+        duration: parseInt(probeData.format.duration),
+        size: parseInt(probeData.format.size),
+        bitrate: parseInt(probeData.format.bit_rate)
+      })
+
+      if (saveTags) {
+        item.metadata.tags = probeData.tags
+      }
+    }
+
+    doneCount++
+    showTrackProcessStatus(flattened.items.length, doneCount, true)
+    process.stdout.write('   \r')
+  }
+
+  await writeFile(outFile, JSON.stringify(playlist, null, 2))
+
+  console.log(`\nDone! Processed ${flattened.items.length} tracks.`)
+}
+
+module.exports = main
+
+if (require.main === module) {
+  main(process.argv.slice(2))
+    .catch(err => console.error(err))
+}
diff --git a/todo.txt b/todo.txt
index 1c9d314..d2def51 100644
--- a/todo.txt
+++ b/todo.txt
@@ -408,3 +408,15 @@ TODO: Case-insensitive checking with command keybindings - I think this is
 TODO: Handle empty (active) playlists. Showing an error message and stopping
       is best, I think.
       (Done!)
+
+TODO: A way to switch between what information is displayed in the status bar.
+      I think using ">" and "<" as default keybindings would work.
+      Make one set be (track # in group) / (# of tracks in group); one be
+      (total track #) / (total # of tracks).
+
+TODO: Adding onto the last one, show the total amount of time in the group/all
+      groups together. Requires a track metadata tool, though...
+
+TODO: Make process-metadata work with non-local tracks, somehow...
+
+TODO: Make process-metadata work nicely with smart playlists, somehow...