1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
|
#!/usr/bin/env node
'use strict'
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
}
function findChild(grouplike, name) {
return grouplike.items.find(x => x.name === name)
}
let NO_LIBRARY_SYMBOL = Symbol('No library')
async function crawl(
libraryPath = `${process.env.HOME}/Music/iTunes/iTunes Music Library.xml`
) {
let libraryXML
try {
libraryXML = await readFile(libraryPath)
} catch (err) {
if (err.code === 'ENOENT') {
throw NO_LIBRARY_SYMBOL
} else {
throw err
}
}
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 resultGroup = {items: []}
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, 'Album Artist')
artist = artist || getDictValue(trackDict, 'Artist')
artist = artist && artist.val
artist = artist || 'Unknown Artist'
// console.log(`${artist} - ${name} (${album})`)
let artistGroup = findChild(resultGroup, artist)
if (!artistGroup) {
artistGroup = {name: artist, items: []}
resultGroup.items.push(artistGroup)
}
let albumGroup = findChild(artistGroup, album)
if (!albumGroup) {
albumGroup = {name: album, items: []}
artistGroup.items.push(albumGroup)
}
albumGroup.items.push({name, downloaderArg: location})
}
return resultGroup
}
async function main(args, shouldReturn = false) {
let playlist
try {
playlist = await crawl(args[0])
} catch(err) {
if (err === NO_LIBRARY_SYMBOL) {
console.error(
"It looks like you aren't sharing the iTunes Library XML file."
)
console.error(
"To do that, just open up iTunes, select iTunes > Preferences from " +
"the menu bar, select the Advanced section, enable the " +
"\"Share iTunes Library XML with other applications\" checkbox, and " +
"click on OK."
)
console.error("Then run the crawl-itunes command again.")
console.error(
"(Or, if you're certain it *is* being shared, you could try " +
"entering the path to the file as an argument to crawl-itunes.)"
)
process.exit(1)
return
} else {
throw err
}
}
const str = JSON.stringify(playlist, null, 2)
if (shouldReturn) {
return str
} else {
console.log(str)
}
}
module.exports = {main, crawl}
if (require.main === module) {
main(process.argv.slice(2))
.catch(err => console.error(err))
}
|