« get me outta code hell

http-music - Command-line music player + utils (not a server!)
about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--man/http-music-crawl-itunes.125
-rw-r--r--package.json6
-rw-r--r--src/crawl-itunes.js103
-rw-r--r--todo.txt3
-rw-r--r--yarn.lock10
5 files changed, 145 insertions, 2 deletions
diff --git a/man/http-music-crawl-itunes.1 b/man/http-music-crawl-itunes.1
new file mode 100644
index 0000000..9d21734
--- /dev/null
+++ b/man/http-music-crawl-itunes.1
@@ -0,0 +1,25 @@
+.TH http-music-crawl-itunes 1
+
+.SH NAME
+http-music-crawl-itunes - create a playlist file using an iTunes library
+
+.SH SYNOPSIS
+.B http-music-crawl-itunes
+[libraryPath]
+
+.SH DESCRIPTION
+\fBhttp-music-crawl-itunes\fR is a command line utility used to generate a
+playlist file for usage with \fBhttp-music\fR using the Apple iTunes library
+XML format.
+
+It requires the iTunes XML format; this is not the usual iTunes Library.itl format, but it is officially supported by Apple (though not used in recent Apple software).
+This iTunes XML file is automatically generated by iTunes if the \fB"Share iTunes Library XML with other applications"\fR option is checked in the advanced preferences section of iTunes.
+If this option is not checked (which is the case, by default), it must be enabled for crawl-itunes to function.
+
+The path for the iTunes XML library file may be passed as an argument; if not given, it is assumed to be \fB~/Music/iTunes/iTunes Music Library.xml\fR.
+
+The resulting playlist file is sorted as a hierarchial tree formed of a group for each artist and then a group for each of the artists' albums.
+See the \fBEXAMPLES\fR section of \fBhttp-music\fR (1) for information on filtering to a specific artist or album.
+
+.SH SEE ALSO
+For more information on the iTunes Library XML file: \fIhttps://support.apple.com/en-ca/HT201610\fR
diff --git a/package.json b/package.json
index f082d05..92f11ea 100644
--- a/package.json
+++ b/package.json
@@ -10,7 +10,8 @@
   "bin": {
     "http-music": "./src/http-music.js",
     "http-music-crawl-http": "./src/crawl-http.js",
-    "http-music-crawl-local": "./src/crawl-local.js"
+    "http-music-crawl-local": "./src/crawl-local.js",
+    "http-music-crawl-itunes": "./src/crawl-itunes.js"
   },
   "man": [
     "./man/http-music.1",
@@ -23,6 +24,7 @@
     "node-fetch": "^1.7.0",
     "node-natural-sort": "^0.8.6",
     "sanitize-filename": "^1.6.1",
-    "tempy": "^0.1.0"
+    "tempy": "^0.1.0",
+    "xmldoc": "^1.1.0"
   }
 }
diff --git a/src/crawl-itunes.js b/src/crawl-itunes.js
new file mode 100644
index 0000000..ec6c3ec
--- /dev/null
+++ b/src/crawl-itunes.js
@@ -0,0 +1,103 @@
+
+const fs = require('fs')
+const path = require('path')
+const xmldoc = require('xmldoc')
+
+const { promisify } = require('util')
+const readFile = promisify(fs.readFile)
+
+function getDictValue(dict, key) {
+  if (dict.name !== 'dict') {
+    throw new Error("Not a dict: " + dict.name)
+  }
+
+  for (let i = 0; i < dict.children.length; i++) {
+    const child = dict.children[i]
+    if (child.name === 'key') {
+      if (child.val === key) {
+        return dict.children.slice(i + 1).find(item => !item.text)
+      }
+    }
+  }
+
+  return null
+}
+
+async function crawl(libraryXML) {
+  const document = new xmldoc.XmlDocument(libraryXML)
+
+  const libraryDict = document.children.find(child => child.name === 'dict')
+
+  const tracksDict = getDictValue(libraryDict, 'Tracks')
+
+  const trackDicts = tracksDict.children.filter(child => child.name === 'dict')
+
+  const result = []
+
+  for (let trackDict of trackDicts) {
+    let kind = getDictValue(trackDict, 'Kind')
+    kind = kind && kind.val
+    kind = kind || ''
+
+    if (!kind.includes('audio file')) {
+      continue
+    }
+
+    let location = getDictValue(trackDict, 'Location')
+    location = location && location.val
+    location = location || ''
+
+    if (!location) {
+      continue
+    }
+
+    let name = getDictValue(trackDict, 'Name')
+    name = name && name.val
+    name = name || 'Unknown Name'
+
+    let album = getDictValue(trackDict, 'Album')
+    album = album && album.val
+    album = album || 'Unknown Album'
+
+    let artist = getDictValue(trackDict, 'Artist')
+    artist = artist && artist.val
+    artist = artist || 'Unknown Artist'
+
+    // console.log(`${artist} - ${name} (${album})`)
+
+    const group = (arr, title) => arr.find(g => g[0] === title)
+
+    let artistGroup = group(result, artist)
+
+    if (!artistGroup) {
+      artistGroup = [artist, []]
+      result.push(artistGroup)
+    }
+
+    let albumGroup = group(artistGroup[1], album)
+
+    if (!albumGroup) {
+      albumGroup = [album, []]
+      artistGroup[1].push(albumGroup)
+    }
+
+    albumGroup[1].push([name, location])
+  }
+
+  return result
+}
+
+async function main() {
+  const libraryPath = process.argv[2] || (
+    `${process.env.HOME}/Music/iTunes/iTunes Music Library.xml`
+  )
+
+  const library = await readFile(libraryPath)
+
+  const playlist = await crawl(library)
+
+  console.log(JSON.stringify(playlist, null, 2))
+}
+
+main()
+  .catch(err => console.error(err))
diff --git a/todo.txt b/todo.txt
index 9317d3e..e6c4c76 100644
--- a/todo.txt
+++ b/todo.txt
@@ -163,3 +163,6 @@ TODO: The results of pressing key commands aren't very clear currently. Useful
 TODO: Figure out a way to make the same mpv process be reused, so that options
       such as volume can be remembered. (At the moment volume is reset to the
       default whenever a new track is played!)
+
+TODO: Figure out how to stream audio data directly, or at least at a lower
+      level (and stupider, as in "man git" stupid).
diff --git a/yarn.lock b/yarn.lock
index 414cc7d..86bf07a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -180,6 +180,10 @@ sanitize-filename@^1.6.1:
   dependencies:
     truncate-utf8-bytes "^1.0.0"
 
+sax@^1.2.1:
+  version "1.2.4"
+  resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
+
 string_decoder@~1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.1.tgz#62e200f039955a6810d8df0a33ffc0f013662d98"
@@ -218,3 +222,9 @@ util-deprecate@~1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
 
+xmldoc:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/xmldoc/-/xmldoc-1.1.0.tgz#25c92f08f263f344dac8d0b32370a701ee9d0e93"
+  dependencies:
+    sax "^1.2.1"
+