#!/usr/bin/env python from UserDict import DictMixin from xml.parsers import expat from StringIO import StringIO import os import sys from urllib import unquote from urlparse import urlparse from shutil import copy HOME = os.environ.get('HOME', os.curdir) ITUNES = os.path.join(HOME, 'Music/iTunes/iTunes Music Library.xml') class PropertyList(DictMixin): """Mount XML plist as a dict May be initialized as you would a dict, with a file object to read from, or a string. If the item is a string, and contains plist data, it will parse that, otherwise will treat it as a file path. If plist is binary format, it will load it into memory and convert to XML format before parsing. This only works on OSX, by using the Cocoa API. """ _struct_types = {'dict': dict, 'array': list} _data_types = ('key', 'integer', 'string', 'date', 'data', 'real') _static_types = {'true': True, 'false': False} _rev_struct_types = dict((y, x) for x, y in _struct_types.items()) _indent = '\t' def __init__(self, init=None): """Initialize plist with optional default data""" self.version = '1.0' if init is None: init = {} if isinstance(init, dict): self.__stack = [init] elif hasattr(init, 'read'): self.load_file(init) elif isinstance(init, str): if init.startswith('bplist00') or '' in init: self.load(init) else: self.load_file(init) else: raise TypeError, 'unsupported initializer' def load(self, string): """Update plist from string""" if string.startswith('bplist00'): string = self._convert_binary_plist(string) self._load('Parse', string) def load_file(self, file): """Update plist from file object""" if isinstance(file, str): file = open(file, 'rb') magic = file.read(8) file.seek(0) if magic == 'bplist00': self.load(file.read()) else: self._load('ParseFile', file) def _load(self, func, arg): parser = expat.ParserCreate() parser.StartElementHandler = self._on_element_start parser.EndElementHandler = self._on_element_end parser.CharacterDataHandler = self._on_data self.indata = False self.buffer = [] self.key = None self.__stack = None try: getattr(parser, func)(arg) finally: del self.indata, self.buffer, self.key assert len(self.__stack) == 1, 'unbalanced tags' def _on_element_start(self, name, attrs): if name in self._struct_types: data = self._struct_types[name]() if self.__stack is None: self.__stack = [data] else: self._set_data(data) self.__stack.insert(0, data) elif name in self._data_types: self.indata = True elif name in self._static_types: self._set_data(self._static_types[name]) elif name == 'plist' and 'version' in attrs: self.version = attrs['version'] def _on_element_end(self, name): if name in self._data_types: data = self._get_data() if name == 'key': self.key = data else: if name == 'integer': data = int(data) elif name == 'real': data = float(data) self._set_data(data) elif name in self._struct_types: self.__stack.pop(0) def _on_data(self, data): if self.indata: self.buffer.append(data) def _get_data(self): data = ''.join(self.buffer).encode('ascii', 'replace') self.indata = False self.buffer = [] return data def _set_data(self, data): if isinstance(self._struct, dict): self._struct[self.key] = data elif isinstance(self._struct, list): self._struct.append(data) def save(self, indent=True): """Return plist string""" buf = StringIO() self.save_file(buf, indent) return buf.getvalue() def save_file(self, file, indent=True): """Write plist string to file""" self.level = 0 self.indent = indent try: file.write('' + self._eol) file.write('' + self._eol) file.write('%s' % (self.version, self._eol)) self._dump_struct(self._struct, file) file.write('' + self._eol) finally: del self.level, self.indent def _dump_struct(self, struct, file): struct_type = type(struct) attr = (self._pad, self._rev_struct_types[struct_type], self._eol) file.write('%s<%s>%s' % attr) self.level += 1 for key in struct: if struct_type is dict: file.write('%s%s' % (self._pad, self._escape(key))) val = struct[key] elif struct_type is list: val = key else: raise TypeError, 'unknown struct: %s' % struct_type val_type = type(val) if val_type in self._rev_struct_types: if struct_type is dict: file.write(self._eol) self._dump_struct(val, file) else: if struct_type is not dict: file.write(self._pad) if val_type in (int, long): file.write('%d%s' % (val, self._eol)) elif val is True: file.write('' + self._eol) elif val is False: file.write('' + self._eol) elif val_type is str: file.write('%s%s' % ( self._escape(val), self._eol)) else: raise ValueError, 'unknown value: %s' % val_type self.level -= 1 file.write('%s%s' % attr) def __getitem__(x, y): return x._struct.__getitem__(y) def __setitem__(x, i, y): return x._struct.__setitem__(i, y) def __delitem__(x, y): return x._struct.__delitem__(y) def keys(D): return D._struct.keys() def __contains__(D, k): return D._struct.__contains__(k) def __iter__(x): return x._struct.__iter__() def iteritems(D): return D._struct.iteritems() for func in (__getitem__, __setitem__, __delitem__, keys, __contains__, keys, __contains__, __iter__, iteritems): func.__doc__ = getattr(dict, func.__name__).__doc__ del func @property def _struct(self): return self.__stack[0] @property def _pad(self): return self._indent * self.level if self.indent else '' @property def _eol(self): return '\n' if self.indent else '' @staticmethod def _escape(val): val = val.replace('&', '&') val = val.replace('>', '>') val = val.replace('<', '<') return val @staticmethod def _convert_binary_plist(data): """Convert a binary plist into XML string (OSX only)""" from Foundation import ( NSPropertyListSerialization as Serializer, NSPropertyListXMLFormat_v1_0 as XMLFormat, NSPropertyListImmutable, NSData) import warnings warnings.simplefilter('ignore', DeprecationWarning) nsdata = NSData.dataWithBytes_length_(data, len(data)) decode = getattr( Serializer, 'propertyListFromData_mutability' 'Option_format_errorDescription_') plist = decode(nsdata, NSPropertyListImmutable)[0] return Serializer.dataFromPropertyList_format_errorDescription_( plist, XMLFormat)[0] def __str__(self): return repr(self._struct) def __repr__(self): return '<%s object at 0x%x: %s>' % ( self.__class__.__name__, id(self), self._struct) def main(args=None): print >> sys.stderr, 'reading XML...' plist = PropertyList(ITUNES) for i, playlist in enumerate(plist['Playlists']): print i, playlist['Name'] playlist_id = int(raw_input('Enter playlist to copy: ')) for item in plist['Playlists'][playlist_id]['Playlist Items']: track_id = str(item['Track ID']) track = plist['Tracks'][track_id] uri = urlparse(track['Location']) path = unquote(uri.path) filename = os.path.basename(path) print >> sys.stderr, 'Copying %s' % filename copy(path, filename) return 0 if __name__ == '__main__': sys.exit(main())