#!/usr/bin/python
"""Read an iTunes XML file and import it into our database."""
import datetime
import os
import re
import sys
import types
import database
import gflags
import track
FLAGS = gflags.FLAGS
gflags.DEFINE_string('remove_from_path', 'file://localhost/Volumes',
'Remove this string from MP3 paths')
_DATE_RE = re.compile('([0-9][0-9][0-9][0-9])-([0-9][0-9])-([0-9][0-9])T'
'([0-9][0-9]):([0-9][0-9]):([0-9][0-9])Z')
def ExtractValue(t, v):
if t == 'integer':
return int(v)
if t == 'date':
m = _DATE_RE.match(v)
if m:
year = int(m.group(1))
month = int(m.group(2))
day = int(m.group(3))
hour = int(m.group(4))
minute = int(m.group(5))
second = int(m.group(6))
# This is actually a UTC time, but I can't be bothered converting it
# to the local timezone at the moment
return datetime.datetime(year, month, day, hour, minute, second)
if t == 'string':
return v
return '<%s>%s%s>' %(t, v, t)
def Usage():
"""Print a usage message and exit."""
print """Unknown command line. Try one of:'
This is a helper script for importing exported iTunes libraries. Export your
iTunes library, and then run this command on the resulting XML file. The XML
file is passed on the command line.
"""
print '\n\nAdditionally, you can use these global flags:%s' % FLAGS
sys.exit(1)
if __name__ == '__main__':
# Parse flags
try:
argv = FLAGS(sys.argv)
except gflags.FlagsError, e:
Usage()
if len(argv) < 2:
Usage()
db = database.Database()
# I don't use the python XML parser, as it is too slow with my 22mb library
f = open(argv[1])
## 9585
##
## Track ID9585
## Name02Love_Me_Two_Times
## KindMPEG audio file
## Size5138206
## Total Time195291
## Date Modified2007-06-20T15:24:19Z
## Date Added2006-02-13T06:17:03Z
## Bit Rate210
## Sample Rate44100
## Skip Count1
## Skip Date2007-10-12T16:39:14Z
## Normalization4908
## Persistent ID79025B48C3EC9D04
## Track TypeFile
## Locationfile://localhost/Volumes/data/mp3/...
## symlinks/aerosmith/young_lust_the_aerosmith_ant/...
# 02love_me_two_times.mp3
## File Folder Count-1
## Library Folder Count-1
##
songkey_re = re.compile('[ \t]+([0-9]+)$')
string_re = re.compile('[ \t]+(.*)<(string)>(.*)')
int_re = re.compile('[ \t]+(.*)<(integer)>(.*)')
date_re = re.compile('[ \t]+(.*)<(date)>(.*)')
l = f.readline()
key = None
song = {}
preamble = []
songs = {}
postamble = []
while l:
handled = False
if postamble:
postamble.append(l.rstrip())
handled = True
else:
m = songkey_re.match(l)
if m:
key = m.group(1)
song = {}
handled = True
elif key:
for r in [string_re, int_re, date_re]:
m = r.match(l)
if m:
song[m.group(1)] = ExtractValue(m.group(2), m.group(3))
handled = True
break
if not handled:
if l.find('') != -1 and key:
location = song['Location']
if not songs.has_key(location):
song['keys'] = [key]
songs[location] = song
else:
songs[location]['keys'].append(key)
for k in ['Play Count', 'Skip Count']:
if k in song:
songs[location].setdefault(k, 0)
songs[location][k] += song[k]
key = None
elif l.find('Playlists') != -1:
postamble.append(l.rstrip())
elif not songs:
preamble.append(l.rstrip())
l = f.readline()
# We output by key, but stored by location -- build a reverse mapping,
# as well as a map of deleted keys
key_map = {}
old_keys = {}
for location in songs:
key_map[int(songs[location]['keys'][0])] = location
for key in songs[location]['keys'][1:]:
old_keys[key] = songs[location]['keys'][0]
keys = key_map.keys()
keys.sort()
for key in keys:
location = key_map[key]
try:
this_track = track.Track(db)
this_track.FromMeta(songs[location].get('Artist', ''),
songs[location].get('Album', ''),
songs[location].get('Track Number', -1),
songs[location].get('Name', ''))
for k in songs[location]:
actual_path = location
if FLAGS.remove_from_path:
actual_path = actual_path.replace(FLAGS.remove_from_path, '')
if os.path.exists(actual_path):
this_track.AddPath(actual_path)
this_track.AddPlays(songs[location].get('Play Count', 0))
this_track.AddSkips(songs[location].get('Skip Count', 0))
if 'Genre' in songs[location] and songs[location]['Genre'] != 'Unknown':
this_track.AddTag(songs[location]['Genre'])
this_track.Store()
except database.FormatException, e:
print 'Skipped: %s' % location
# Process playlists as tags
plname_re = re.compile('\s+Name(.*)')
pltrack_re = re.compile('\s+Track ID(.*)')
playlist = None
is_smart = False
playlists = {}
for l in postamble:
m = plname_re.match(l)
if m:
playlist = m.group(1)
is_smart = False
if l.find('Smart Info') != -1:
is_smart = True
if playlist != 'Library' and not is_smart:
m = pltrack_re.match(l)
if m:
tracks = playlists.get(playlist, [])
tracks.append(m.group(1))
playlists[playlist] = tracks
for playlist in playlists:
print 'Playlist %s: %s' %(playlist, len(playlists[playlist]))
for key in playlists[playlist]:
if int(key) in key_map:
location = key_map[int(key)]
this_track = track.Track(db)
this_track.FromPath(location.replace(FLAGS.remove_from_path, ''))
this_track.AddTag(playlist)
this_track.Store()