diff --git a/src/calibre/devices/apple/driver.py b/src/calibre/devices/apple/driver.py index 61c4654a32..648d26c27a 100644 --- a/src/calibre/devices/apple/driver.py +++ b/src/calibre/devices/apple/driver.py @@ -9,8 +9,7 @@ import cStringIO, ctypes, datetime, os, re, shutil, sys, tempfile, time from calibre import fit_image, confirm_config_name, strftime as _strftime from calibre.constants import ( - __appname__, __version__, DEBUG as CALIBRE_DEBUG, isosx, iswindows, - cache_dir as _cache_dir) + __appname__, __version__, isosx, iswindows, cache_dir as _cache_dir) from calibre.devices.errors import OpenFeedback, UserFeedback from calibre.devices.usbms.deviceconfig import DeviceConfig from calibre.devices.interface import DevicePlugin @@ -19,8 +18,6 @@ from calibre.ebooks.metadata import (author_to_author_sort, authors_to_string, from calibre.ebooks.metadata.book.base import Metadata from calibre.utils.config_base import config_dir, prefs -DEBUG = CALIBRE_DEBUG - def strftime(fmt='%Y/%m/%d %H:%M:%S', dt=None): from calibre.utils.date import now @@ -130,12 +127,16 @@ class DriverBase(DeviceConfig, DevicePlugin): ':::' + _("

This setting should match your iTunes Preferences|Advanced setting.

" "

Disabling will store copies of books transferred to iTunes in your calibre configuration directory.

" - "

Enabling indicates that iTunes is configured to store copies in your iTunes Media folder.

") + "

Enabling indicates that iTunes is configured to store copies in your iTunes Media folder.

"), + _(u'Enable debug logging') + + ':::' + + _("Print driver debug messages to console"), ] EXTRA_CUSTOMIZATION_DEFAULT = [ True, True, False, + False, ] @classmethod @@ -170,7 +171,6 @@ class ITUNES(DriverBase): Delete: delete_books() remove_books_from_metadata() - use_plugboard_ext() set_plugboard() sync_booklists() card_prefix() @@ -186,12 +186,17 @@ class ITUNES(DriverBase): _add_library_book() _update_iTunes_metadata() add_books_to_metadata() - use_plugboard_ext() set_plugboard() set_progress_reporter() sync_booklists() card_prefix() free_space() + + self.manual_sync_mode is True when we're talking directly to iBooks through iTunes. + Determined in _discover_manual_sync_mode() + Special handling in: + _add_new_copy() + ''' name = 'Apple iTunes interface' @@ -209,6 +214,7 @@ class ITUNES(DriverBase): USE_SERIES_AS_CATEGORY = 0 CACHE_COVERS = 1 USE_ITUNES_STORAGE = 2 + DEBUG_LOGGING = 3 OPEN_FEEDBACK_MESSAGE = _( 'Apple iDevice detected, launching iTunes, please wait ...') @@ -308,6 +314,12 @@ class ITUNES(DriverBase): sources = None update_msg = None update_needed = False + verbose = False + + def __init__(self, path): + self.verbose = self.settings().extra_customization[self.DEBUG_LOGGING] + if self.verbose: + logger().info("%s.__init__():" % self.__class__.__name__) @property def cache_dir(self): @@ -329,20 +341,20 @@ class ITUNES(DriverBase): (L{books}(oncard=None), L{books}(oncard='carda'), L{books}(oncard='cardb')). ''' - if DEBUG: + if self.verbose: logger().info("%s.add_books_to_metadata()" % self.__class__.__name__) task_count = float(len(self.update_list)) # Delete any obsolete copies of the book from the booklist if self.update_list: - if False: + if False and self.verbose: self._dump_booklist(booklists[0], header='before', indent=2) self._dump_update_list(header='before', indent=2) self._dump_cached_books(header='before', indent=2) for (j, p_book) in enumerate(self.update_list): - if False: + if False and self.verbose: if isosx: logger().info(" looking for '%s' by %s uuid:%s" % (p_book['title'], p_book['author'], p_book['uuid'])) @@ -374,7 +386,7 @@ class ITUNES(DriverBase): if self.cached_books[cb]['uuid'] == p_book['uuid']: if self.cached_books[cb]['title'] == p_book['title'] and \ self.cached_books[cb]['author'] == p_book['author']: - if DEBUG: + if self.verbose: self._dump_cached_book(self.cached_books[cb], header="removing from self.cached_books:", indent=2) self.cached_books.pop(cb) break @@ -389,7 +401,7 @@ class ITUNES(DriverBase): # Charles thinks this should be # for new_book in metadata[0]: for new_book in locations[0]: - if DEBUG: + if self.verbose: logger().info(" adding '%s' by '%s' to booklists[0]" % (new_book.title, new_book.author)) booklists[0].append(new_book) @@ -415,7 +427,7 @@ class ITUNES(DriverBase): """ from calibre.utils.date import parse_date if not oncard: - if DEBUG: + if self.verbose: logger().info("%s.books():" % self.__class__.__name__) if self.settings().extra_customization[self.CACHE_COVERS]: logger().info(" Cover fetching/caching enabled") @@ -461,7 +473,7 @@ class ITUNES(DriverBase): } if self.report_progress is not None: - self.report_progress((i + 1) / book_count, + self.report_progress(float((i + 1)*100 / book_count)/100, _('%(num)d of %(tot)d') % dict(num=i + 1, tot=book_count)) self._purge_orphans(library_books, cached_books) @@ -502,7 +514,7 @@ class ITUNES(DriverBase): } if self.report_progress is not None: - self.report_progress((i + 1) / book_count, + self.report_progress(float((i + 1)*100 / book_count)/100, _('%(num)d of %(tot)d') % dict(num=i + 1, tot=book_count)) self._purge_orphans(library_books, cached_books) @@ -513,7 +525,7 @@ class ITUNES(DriverBase): if self.report_progress is not None: self.report_progress(1.0, _('finished')) self.cached_books = cached_books - if DEBUG: + if self.verbose: self._dump_booklist(booklist, 'returning from books()', indent=2) self._dump_cached_books('returning from books()', indent=2) return booklist @@ -546,12 +558,12 @@ class ITUNES(DriverBase): # Check for connected book-capable device self.sources = self._get_sources() if 'iPod' in self.sources and not self.ejected: - #if DEBUG: + #if self.verbose: #sys.stdout.write('.') #sys.stdout.flush() return True else: - if DEBUG: + if self.verbose: sys.stdout.write('-') sys.stdout.flush() return False @@ -559,7 +571,7 @@ class ITUNES(DriverBase): # Called at entry # We need to know if iTunes sees the iPad # It may have been ejected - if DEBUG: + if self.verbose: logger().info("%s.can_handle()" % self.__class__.__name__) self._launch_iTunes() @@ -572,15 +584,15 @@ class ITUNES(DriverBase): if (not 'iPod' in self.sources) or (self.sources['iPod'] == ''): attempts -= 1 time.sleep(1.0) - if DEBUG: + if self.verbose: logger().warning(" waiting for connected iDevice, attempt #%d" % (10 - attempts)) else: - if DEBUG: + if self.verbose: logger().info(' found connected iDevice') break else: # iTunes running, but not connected iPad - if DEBUG: + if self.verbose: logger().info(' self.ejected = True') self.ejected = True return False @@ -613,16 +625,16 @@ class ITUNES(DriverBase): pythoncom.CoInitialize() self.sources = self._get_sources() if 'iPod' in self.sources: - if DEBUG: + if self.verbose: sys.stdout.write('.') sys.stdout.flush() - if DEBUG: + if self.verbose: logger().info("%s.can_handle_windows:\n confirming connected iPad" % self.__class__.__name__) self.ejected = False self._discover_manual_sync_mode() return True else: - if DEBUG: + if self.verbose: logger().info("%s.can_handle_windows():\n device ejected" % self.__class__.__name__) self.ejected = True return False @@ -635,7 +647,7 @@ class ITUNES(DriverBase): pythoncom.CoUninitialize() else: - if DEBUG: + if self.verbose: logger().info("%s.can_handle_windows():\n Launching iTunes" % self.__class__.__name__) try: @@ -650,15 +662,15 @@ class ITUNES(DriverBase): if (not 'iPod' in self.sources) or (self.sources['iPod'] == ''): attempts -= 1 time.sleep(1.0) - if DEBUG: - logger().warning(" waiting for connected iDevice, attempt #%d" % (10 - attempts)) + if self.verbose: + logger().info(" waiting for connected iDevice, attempt #%d" % (10 - attempts)) else: - if DEBUG: + if self.verbose: logger().info(' found connected iPad in iTunes') break else: # iTunes running, but not connected iPad - if DEBUG: + if self.verbose: logger().info(' iDevice has been ejected') self.ejected = True return False @@ -709,7 +721,7 @@ class ITUNES(DriverBase): logger().info("%s.delete_books()" % self.__class__.__name__) for path in paths: if self.cached_books[path]['lib_book']: - if DEBUG: + if self.verbose: logger().info(" Deleting '%s' from iTunes library" % (path)) if isosx: @@ -741,9 +753,13 @@ class ITUNES(DriverBase): if not metadata.uuid: metadata.uuid = "unknown" + if self.verbose: + logger().info(" Deleting '%s' from iBooks" % (path)) + if isosx: self._remove_existing_copy(self.cached_books[path], metadata) elif iswindows: + import pythoncom, win32com.client try: pythoncom.CoInitialize() self.iTunes = win32com.client.Dispatch("iTunes.Application") @@ -760,7 +776,7 @@ class ITUNES(DriverBase): Un-mount / eject the device from the OS. This does not check if there are pending GUI jobs that need to communicate with the device. ''' - if DEBUG: + if self.verbose: logger().info("%s:eject(): ejecting '%s'" % (self.__class__.__name__, self.sources['iPod'])) if isosx: self.iTunes.eject(self.sources['iPod']) @@ -791,7 +807,7 @@ class ITUNES(DriverBase): In Windows, a sync-in-progress blocks this call until sync is complete """ - if DEBUG: + if self.verbose: logger().info("%s.free_space()" % self.__class__.__name__) free_space = 0 @@ -824,7 +840,7 @@ class ITUNES(DriverBase): Ask device for device information. See L{DeviceInfoQuery}. @return: (device name, device version, software version on device, mime type) """ - if DEBUG: + if self.verbose: logger().info("%s.get_device_information()" % self.__class__.__name__) return (self.sources['iPod'], 'hw v1.0', 'sw v1.0', 'unknown mime type') @@ -834,7 +850,7 @@ class ITUNES(DriverBase): Read the file at C{path} on the device and write it to outfile. @param outfile: file object like C{sys.stdout} or the result of an C{open} call ''' - if DEBUG: + if self.verbose: logger().info("%s.get_file(): exporting '%s'" % (self.__class__.__name__, path)) try: @@ -865,7 +881,7 @@ class ITUNES(DriverBase): if self.iTunes is None: raise OpenFeedback(self.ITUNES_SANDBOX_LOCKOUT_MESSAGE) - if DEBUG: + if self.verbose: vendor_id = "0x%x" % connected_device[0] product_id = "0x%x" % connected_device[1] bcd = "0x%x" % connected_device[2] @@ -887,17 +903,17 @@ class ITUNES(DriverBase): if dynamic.get(confirm_config_name(self.DISPLAY_DISABLE_DIALOG), True): raise AppleOpenFeedback(self) else: - if DEBUG: + if self.verbose: logger().info(" %s" % self.UNSUPPORTED_DIRECT_CONNECT_MODE_MESSAGE) # Log supported DEVICE_IDs and BCDs - if DEBUG: + if self.verbose: logger().info(" BCD: %s" % ['0x%x' % x for x in sorted(self.BCD)]) logger().info(" PRODUCT_ID: %s" % ['0x%x' % x for x in sorted(self.PRODUCT_ID)]) # Confirm/create thumbs archive if not os.path.exists(self.cache_dir): - if DEBUG: + if self.verbose: logger().info(" creating thumb cache at '%s'" % self.cache_dir) os.makedirs(self.cache_dir) @@ -907,18 +923,18 @@ class ITUNES(DriverBase): zfw.writestr("iTunes Thumbs Archive", '') zfw.close() else: - if DEBUG: + if self.verbose: logger().info(" existing thumb cache at '%s'" % self.archive_path) # If enabled in config options, create/confirm an iTunes storage folder if not self.settings().extra_customization[self.USE_ITUNES_STORAGE]: self.iTunes_local_storage = os.path.join(config_dir, 'iTunes storage') if not os.path.exists(self.iTunes_local_storage): - if DEBUG: + if self.verbose: logger()(" creating iTunes_local_storage at '%s'" % self.iTunes_local_storage) os.mkdir(self.iTunes_local_storage) else: - if DEBUG: + if self.verbose: logger()(" existing iTunes_local_storage at '%s'" % self.iTunes_local_storage) def remove_books_from_metadata(self, paths, booklists): @@ -933,73 +949,89 @@ class ITUNES(DriverBase): NB: This will not find books that were added by a different installation of calibre as uuids are different ''' - from calibre.utils.zipfile import ZipFile - if DEBUG: + if self.verbose: logger().info("%s.remove_books_from_metadata()" % self.__class__.__name__) + for path in paths: - if DEBUG: + if self.verbose: self._dump_cached_book(self.cached_books[path], indent=2) - logger().info(" looking for '%s' by '%s' uuid:%s" % + if False and self.verbose: + logger().info(" looking for '%s' by '%s' uuid:%s" % (self.cached_books[path]['title'], self.cached_books[path]['author'], repr(self.cached_books[path]['uuid']))) # Purge the booklist, self.cached_books, thumb cache for i, bl_book in enumerate(booklists[0]): - if False: + if False and self.verbose: logger().info(" evaluating '%s' by '%s' uuid:%s" % (bl_book.title, bl_book.author, bl_book.uuid)) found = False if bl_book.uuid and bl_book.uuid == self.cached_books[path]['uuid']: - if True: + if True and self.verbose: logger().info(" --matched uuid") - booklists[0].pop(i) found = True elif bl_book.title == self.cached_books[path]['title'] and \ bl_book.author == self.cached_books[path]['author']: - if True: + if True and self.verbose: logger().info(" --matched title + author") - booklists[0].pop(i) found = True if found: + # Remove from booklist[0] + popped = booklists[0].pop(i) + if False and self.verbose: + logger().info(" '%s' removed from booklists[0]" % popped.title) + # Remove from self.cached_books + if False and self.verbose: + logger().info("path: %s" % path) for cb in self.cached_books: + if False and self.verbose: + logger().info(" evaluating '%s' by '%s' uuid:%s" % + (self.cached_books[cb]['title'], + self.cached_books[cb]['author'], + self.cached_books[cb]['uuid'])) if (self.cached_books[cb]['uuid'] == self.cached_books[path]['uuid'] and self.cached_books[cb]['author'] == self.cached_books[path]['author'] and self.cached_books[cb]['title'] == self.cached_books[path]['title']): - self.cached_books.pop(cb) + popped = self.cached_books.pop(cb) + if False and self.verbose: + logger().info(" '%s' removed from self.cached_books" % popped['title']) break else: - logger().error(" '%s' not found in self.cached_books" % self.cached_books[path]['title']) + if self.verbose: + logger().info(" '%s' not found in self.cached_books" % self.cached_books[path]['title']) # Remove from thumb from thumb cache + from calibre.utils.zipfile import ZipFile thumb_path = path.rpartition('.')[0] + '.jpg' zf = ZipFile(self.archive_path, 'a') + fnames = zf.namelist() try: thumb = [x for x in fnames if thumb_path in x][0] except: thumb = None + if thumb: - if DEBUG: + if self.verbose: logger().info(" deleting '%s' from cover cache" % (thumb_path)) - zf.delete(thumb_path) - else: - if DEBUG: - logger().info(" '%s' not found in cover cache" % thumb_path) + zf.delete(thumb_path) + elif self.verbose: + logger().info(" '%s' not found in cover cache" % thumb_path) zf.close() break else: - if DEBUG: + if self.verbose: logger().error(" unable to find '%s' by '%s' (%s)" % (self.cached_books[path]['title'], self.cached_books[path]['author'], self.cached_books[path]['uuid'])) - if False: + if False and self.verbose: self._dump_booklist(booklists[0], indent=2) self._dump_cached_books(indent=2) @@ -1014,7 +1046,7 @@ class ITUNES(DriverBase): task does not have any progress information :detected_device: Device information from the device scanner """ - if DEBUG: + if self.verbose: logger().info("%s.reset()" % self.__class__.__name__) if report_progress: self.set_progress_reporter(report_progress) @@ -1026,22 +1058,22 @@ class ITUNES(DriverBase): If it is called with -1 that means that the task does not have any progress information ''' - if DEBUG: + if self.verbose: logger().info("%s.set_progress_reporter()" % self.__class__.__name__) self.report_progress = report_progress def set_plugboards(self, plugboards, pb_func): # This method is called with the plugboard that matches the format - # declared in use_plugboard_ext and a device name of ITUNES - if DEBUG: + # and a device name of ITUNES + if self.verbose: logger().info("%s.set_plugboard()" % self.__class__.__name__) #logger().info(' plugboard: %s' % plugboards) self.plugboards = plugboards self.plugboard_func = pb_func def shutdown(self): - if False and DEBUG: + if False and self.verbose: logger().info("%s.shutdown()\n" % self.__class__.__name__) def sync_booklists(self, booklists, end_session=True): @@ -1052,11 +1084,11 @@ class ITUNES(DriverBase): L{books}(oncard='cardb')). ''' - if DEBUG: + if self.verbose: logger().info("%s.sync_booklists()" % self.__class__.__name__) if self.update_needed: - if DEBUG: + if self.verbose: logger().info(' calling _update_device') self._update_device(msg=self.update_msg, wait=False) self.update_needed = False @@ -1079,7 +1111,7 @@ class ITUNES(DriverBase): @return: A 3 element list with total space in bytes of (1, 2, 3). If a particular device doesn't have any of these locations it should return 0. """ - if DEBUG: + if self.verbose: logger().info("%s.total_space()" % self.__class__.__name__) capacity = 0 if isosx: @@ -1117,7 +1149,7 @@ class ITUNES(DriverBase): self.problem_msg = _("Some cover art could not be converted.\n" "Click 'Show Details' for a list.") - if DEBUG: + if self.verbose: logger().info("%s.upload_books()" % self.__class__.__name__) if isosx: @@ -1134,7 +1166,7 @@ class ITUNES(DriverBase): self._update_iTunes_metadata(metadata[i], db_added, lb_added, this_book) # Add new_book to self.cached_books - if DEBUG: + if self.verbose: logger().info("%s.upload_books()" % self.__class__.__name__) logger().info(" adding '%s' by '%s' uuid:%s to self.cached_books" % (metadata[i].title, @@ -1181,7 +1213,7 @@ class ITUNES(DriverBase): self._update_iTunes_metadata(metadata[i], db_added, lb_added, this_book) # Add new_book to self.cached_books - if DEBUG: + if self.verbose: logger().info("%s.upload_books()" % self.__class__.__name__) logger().info(" adding '%s' by '%s' uuid:%s to self.cached_books" % (metadata[i].title, @@ -1221,7 +1253,8 @@ class ITUNES(DriverBase): ''' assumes pythoncom wrapper for windows ''' - logger().info(" %s._add_device_book()" % self.__class__.__name__) + if self.verbose: + logger().info(" %s._add_device_book()" % self.__class__.__name__) if isosx: import appscript if 'iPod' in self.sources: @@ -1231,7 +1264,7 @@ class ITUNES(DriverBase): if pl.special_kind() == appscript.k.Books: break else: - if DEBUG: + if self.verbose: logger().error(" Device|Books playlist not found") # Add the passed book to the Device|Books playlist @@ -1245,12 +1278,12 @@ class ITUNES(DriverBase): break except: attempts -= 1 - if DEBUG: + if self.verbose: logger().warning(" failed to add book, waiting %.1f seconds to try again (attempt #%d)" % (delay, (3 - attempts))) time.sleep(delay) else: - if DEBUG: + if self.verbose: logger().error(" failed to add '%s' to Device|Books" % metadata.title) raise UserFeedback("Unable to add '%s' in direct connect mode" % metadata.title, details=None, level=UserFeedback.ERROR) @@ -1268,7 +1301,7 @@ class ITUNES(DriverBase): pl.SpecialKind == self.PlaylistSpecialKind.index('Books'): break else: - if DEBUG: + if self.verbose: logger().info(" no Books playlist found") # Add the passed book to the Device|Books playlist @@ -1278,16 +1311,16 @@ class ITUNES(DriverBase): fa = FileArray(file_s) op_status = pl.AddFiles(fa) - if DEBUG: + if self.verbose: sys.stdout.write(" uploading '%s' to Device|Books ..." % metadata.title) sys.stdout.flush() while op_status.InProgress: time.sleep(0.5) - if DEBUG: + if self.verbose: sys.stdout.write('.') sys.stdout.flush() - if DEBUG: + if self.verbose: sys.stdout.write("\n") sys.stdout.flush() @@ -1298,16 +1331,16 @@ class ITUNES(DriverBase): This would be the preferred approach (as under OSX) It works in _add_library_book() ''' - if DEBUG: + if self.verbose: sys.stdout.write(" waiting for handle to added '%s' ..." % metadata.title) sys.stdout.flush() while not op_status.Tracks: time.sleep(0.5) - if DEBUG: + if self.verbose: sys.stdout.write('.') sys.stdout.flush() - if DEBUG: + if self.verbose: print added = op_status.Tracks[0] else: @@ -1330,7 +1363,7 @@ class ITUNES(DriverBase): ''' windows assumes pythoncom wrapper ''' - if DEBUG: + if self.verbose: logger().info(" %s._add_library_book()" % self.__class__.__name__) if isosx: import appscript @@ -1342,21 +1375,21 @@ class ITUNES(DriverBase): FileArray = ctypes.c_char_p * 1 fa = FileArray(file_s) op_status = lib.AddFiles(fa) - if DEBUG: + if self.verbose: logger().info(" file added to Library|Books") logger().info(" iTunes adding '%s'" % file) - if DEBUG: + if self.verbose: sys.stdout.write(" iTunes copying '%s' ..." % metadata.title) sys.stdout.flush() while op_status.InProgress: time.sleep(0.5) - if DEBUG: + if self.verbose: sys.stdout.write('.') sys.stdout.flush() - if DEBUG: + if self.verbose: sys.stdout.write("\n") sys.stdout.flush() @@ -1366,15 +1399,15 @@ class ITUNES(DriverBase): Originally disabled because op_status.Tracks never returned a value after adding file. Seems to be working with iTunes 9.2.1.5 06 Aug 2010 ''' - if DEBUG: + if self.verbose: sys.stdout.write(" waiting for handle to added '%s' ..." % metadata.title) sys.stdout.flush() while op_status.Tracks is None: time.sleep(0.5) - if DEBUG: + if self.verbose: sys.stdout.write('.') sys.stdout.flush() - if DEBUG: + if self.verbose: print added = op_status.Tracks[0] else: @@ -1398,7 +1431,7 @@ class ITUNES(DriverBase): fp = cached_book['lib_book'].location().path fp = cached_book['lib_book'].Location ''' - if DEBUG: + if self.verbose: logger().info(" %s._add_new_copy()" % self.__class__.__name__) if fpath.rpartition('.')[2].lower() == 'epub': @@ -1435,7 +1468,7 @@ class ITUNES(DriverBase): from PIL import Image as PILImage from calibre.utils.zipfile import ZipFile - if DEBUG: + if self.verbose: logger().info(" %s._cover_to_thumb()" % self.__class__.__name__) thumb = None @@ -1452,7 +1485,7 @@ class ITUNES(DriverBase): height = img.size[1] scaled, nwidth, nheight = fit_image(width, height, self.MAX_COVER_WIDTH, self.MAX_COVER_HEIGHT) if scaled: - if DEBUG: + if self.verbose: logger().info(" cover scaled from %sx%s to %sx%s" % (width, height, nwidth, nheight)) img = img.resize((nwidth, nheight), PILImage.ANTIALIAS) @@ -1479,20 +1512,52 @@ class ITUNES(DriverBase): Could also be a problem with the integrity of the cover data? ''' if lb_added: - try: - lb_added.artworks[1].data_.set(cover_data) - except: - if DEBUG: - logger().warning(" iTunes automation interface reported an error" - " adding artwork to '%s' in the iTunes Library" % metadata.title) - pass + delay = 2.0 + self._wait_for_writable_metadata(db_added, delay=delay) + + # Wait for updatable artwork + attempts = 9 + while attempts: + try: + lb_added.artworks[1].data_.set(cover_data) + except: + attempts -= 1 + time.sleep(delay) + if self.verbose: +# logger().warning(" iTunes automation interface reported an error" +# " adding artwork to '%s' in the iTunes Library" % metadata.title) + logger().info(" waiting %.1f seconds for artwork to become writable (attempt #%d)" % + (delay, (10 - attempts))) + else: + if self.verbose: + logger().info(" failed to write artwork") if db_added: + delay = 2.0 + self._wait_for_writable_metadata(db_added, delay=delay) + + # Wait for updatable artwork + attempts = 9 + while attempts: + try: + db_added.artworks[1].data_.set(cover_data) + break + except: + attempts -= 1 + time.sleep(delay) + if self.verbose: + logger().info(" waiting %.1f seconds for artwork to become writable (attempt #%d)" % + (delay, (10 - attempts))) + else: + if self.verbose: + logger().info(" failed to write artwork") + + """ try: db_added.artworks[1].data_.set(cover_data) logger().info(" writing '%s' cover to iDevice" % metadata.title) except: - if DEBUG: + if self.verbose: logger().warning(" iTunes automation interface reported an error" " adding artwork to '%s' on the iDevice" % metadata.title) #import traceback @@ -1500,6 +1565,7 @@ class ITUNES(DriverBase): #from calibre import ipython #ipython(user_ns=locals()) pass + """ elif iswindows: ''' Write the data to a real file for Windows iTunes ''' @@ -1514,19 +1580,36 @@ class ITUNES(DriverBase): else: lb_added.AddArtworkFromFile(tc) except: - if DEBUG: + if self.verbose: logger().warning(" iTunes automation interface reported an error" " when adding artwork to '%s' in the iTunes Library" % metadata.title) pass if db_added: - if db_added.Artwork.Count: - db_added.Artwork.Item(1).SetArtworkFromFile(tc) + delay = 2.0 + self._wait_for_writable_metadata(db_added, delay=delay) + + # Wait for updatable artwork + attempts = 9 + while attempts: + try: + if db_added.Artwork.Count: + db_added.Artwork.Item(1).SetArtworkFromFile(tc) + else: + db_added.AddArtworkFromFile(tc) + break + except: + attempts -= 1 + time.sleep(delay) + if self.verbose: + logger().info(" waiting %.1f seconds for artwork to become writable (attempt #%d)" % + (delay, (10 - attempts))) else: - db_added.AddArtworkFromFile(tc) + if self.verbose: + logger().info(" failed to write artwork") elif format == 'pdf': - if DEBUG: + if self.verbose: logger().info(" unable to set PDF cover via automation interface") try: @@ -1541,7 +1624,7 @@ class ITUNES(DriverBase): of.close() # Refresh the thumbnail cache - if DEBUG: + if self.verbose: logger().info(" refreshing cached thumb for '%s'" % metadata.title) zfw = ZipFile(self.archive_path, mode='a') thumb_path = path.rpartition('.')[0] + '.jpg' @@ -1555,7 +1638,7 @@ class ITUNES(DriverBase): except: pass else: - if DEBUG: + if self.verbose: logger().info(" no cover defined in metadata for '%s'" % metadata.title) return thumb @@ -1563,7 +1646,7 @@ class ITUNES(DriverBase): ''' ''' from calibre.utils.date import parse_date - if DEBUG: + if self.verbose: logger().info(" %s._create_new_book()" % self.__class__.__name__) this_book = Book(metadata.title, authors_to_string(metadata.authors)) @@ -1612,7 +1695,7 @@ class ITUNES(DriverBase): Assumes pythoncom for windows wait is passed when launching iTunes, as it seems to need a moment to come to its senses ''' - if DEBUG: + if self.verbose: logger().info(" %s._discover_manual_sync_mode()" % self.__class__.__name__) if wait: time.sleep(wait) @@ -1638,7 +1721,7 @@ class ITUNES(DriverBase): except: self.manual_sync_mode = False else: - if DEBUG: + if self.verbose: logger().info(" adding tracer to empty Books|Playlist") try: added = pl.add(appscript.mactypes.File(P('tracer.epub')), to=pl) @@ -1661,7 +1744,7 @@ class ITUNES(DriverBase): if dev_books is not None and dev_books.Count: first_book = dev_books.Item(1) - #if DEBUG: + #if self.verbose: #logger().info(" determing manual mode by modifying '%s' by %s" % (first_book.Name, first_book.Artist)) try: first_book.BPM = 0 @@ -1669,7 +1752,7 @@ class ITUNES(DriverBase): except: self.manual_sync_mode = False else: - if DEBUG: + if self.verbose: logger().info(" sending tracer to empty Books|Playlist") fpath = P('tracer.epub') mi = MetaInformation('Tracer', ['calibre']) @@ -1681,7 +1764,7 @@ class ITUNES(DriverBase): except: self.manual_sync_mode = False - if DEBUG: + if self.verbose: logger().info(" iTunes.manual_sync_mode: %s" % self.manual_sync_mode) def _dump_booklist(self, booklist, header=None, indent=0): @@ -1788,11 +1871,11 @@ class ITUNES(DriverBase): timestamp = ts['content'] if not title or not author: - if DEBUG: + if self.verbose: logger().error(" couldn't extract title/author from %s in %s" % (opf, fpath)) logger().error(" title: %s author: %s timestamp: %s" % (title, author, timestamp)) else: - if DEBUG: + if self.verbose: logger().error(" can't find .opf in %s" % fpath) zf.close() return (title, author, timestamp) @@ -1814,7 +1897,7 @@ class ITUNES(DriverBase): def _dump_library_books(self, library_books): ''' ''' - if DEBUG: + if self.verbose: logger().info("\n library_books:") for book in library_books: logger().info(" %s" % book) @@ -1841,21 +1924,21 @@ class ITUNES(DriverBase): ub['title'], ub['author'])) - def _find_device_book(self, search): + def _find_device_book(self, search, attempts=9): ''' Windows-only method to get a handle to device book in the current pythoncom session ''' if iswindows: dev_books = self._get_device_books_playlist() - if DEBUG: + if self.verbose: logger().info(" %s._find_device_book()" % self.__class__.__name__) logger().info(" searching for '%s' by '%s' (%s)" % (search['title'], search['author'], search['uuid'])) - attempts = 9 + while attempts: # Try by uuid - only one hit if 'uuid' in search and search['uuid']: - if DEBUG: + if self.verbose: logger().info(" searching by uuid '%s' ..." % search['uuid']) hits = dev_books.Search(search['uuid'], self.SearchField.index('All')) if hits: @@ -1865,24 +1948,24 @@ class ITUNES(DriverBase): # Try by author - there could be multiple hits if search['author']: - if DEBUG: + if self.verbose: logger().info(" searching by author '%s' ..." % search['author']) hits = dev_books.Search(search['author'], self.SearchField.index('Artists')) if hits: for hit in hits: if hit.Name == search['title']: - if DEBUG: + if self.verbose: logger().info(" found '%s' by %s (%s)" % (hit.Name, hit.Artist, hit.Composer)) return hit # Search by title if no author available - if DEBUG: + if self.verbose: logger().info(" searching by title '%s' ..." % search['title']) hits = dev_books.Search(search['title'], self.SearchField.index('All')) if hits: for hit in hits: if hit.Name == search['title']: - if DEBUG: + if self.verbose: logger().info(" found '%s'" % (hit.Name)) return hit @@ -1891,7 +1974,7 @@ class ITUNES(DriverBase): if search['format'] == 'pdf': title = re.sub(r'[^0-9a-zA-Z ]', '_', search['title']) author = re.sub(r'[^0-9a-zA-Z ]', '_', search['author']) - if DEBUG: + if self.verbose: logger().info(" searching by name: '%s - %s'" % (title, author)) hits = dev_books.Search('%s - %s' % (title, author), self.SearchField.index('All')) @@ -1900,15 +1983,15 @@ class ITUNES(DriverBase): logger().info(" found '%s' by %s (%s)" % (hit.Name, hit.Artist, hit.Composer)) return hit else: - if DEBUG: + if self.verbose: logger().info(" no PDF hits") attempts -= 1 time.sleep(0.5) - if DEBUG: - logger().warning(" attempt #%d" % (10 - attempts)) + if attempts and self.verbose: + logger().info(" attempt #%d" % (10 - attempts)) - if DEBUG: + if self.verbose: logger().error(" no hits") return None @@ -1917,7 +2000,7 @@ class ITUNES(DriverBase): Windows-only method to get a handle to a library book in the current pythoncom session ''' if iswindows: - if DEBUG: + if self.verbose: logger().info(" %s._find_library_book()" % self.__class__.__name__) ''' if 'uuid' in search: @@ -1931,11 +2014,11 @@ class ITUNES(DriverBase): for source in self.iTunes.sources: if source.Kind == self.Sources.index('Library'): lib = source - if DEBUG: + if self.verbose: logger().info(" Library source: '%s' kind: %s" % (lib.Name, self.Sources[lib.Kind])) break else: - if DEBUG: + if self.verbose: logger().info(" Library source not found") if lib is not None: @@ -1943,47 +2026,47 @@ class ITUNES(DriverBase): for pl in lib.Playlists: if pl.Kind == self.PlaylistKind.index('User') and \ pl.SpecialKind == self.PlaylistSpecialKind.index('Books'): - if DEBUG: + if self.verbose: logger().info(" Books playlist: '%s'" % (pl.Name)) lib_books = pl break else: - if DEBUG: + if self.verbose: logger().error(" no Books playlist found") attempts = 9 while attempts: # Find book whose Album field = search['uuid'] if 'uuid' in search and search['uuid']: - if DEBUG: + if self.verbose: logger().info(" searching by uuid '%s' ..." % search['uuid']) hits = lib_books.Search(search['uuid'], self.SearchField.index('All')) if hits: hit = hits[0] - if DEBUG: + if self.verbose: logger().info(" found '%s' by %s (%s)" % (hit.Name, hit.Artist, hit.Composer)) return hit # Search by author if known if search['author']: - if DEBUG: + if self.verbose: logger().info(" searching by author '%s' ..." % search['author']) hits = lib_books.Search(search['author'], self.SearchField.index('Artists')) if hits: for hit in hits: if hit.Name == search['title']: - if DEBUG: + if self.verbose: logger().info(" found '%s' by %s (%s)" % (hit.Name, hit.Artist, hit.Composer)) return hit # Search by title if no author available - if DEBUG: + if self.verbose: logger().info(" searching by title '%s' ..." % search['title']) hits = lib_books.Search(search['title'], self.SearchField.index('All')) if hits: for hit in hits: if hit.Name == search['title']: - if DEBUG: + if self.verbose: logger().info(" found '%s'" % (hit.Name)) return hit @@ -1992,7 +2075,7 @@ class ITUNES(DriverBase): if search['format'] == 'pdf': title = re.sub(r'[^0-9a-zA-Z ]', '_', search['title']) author = re.sub(r'[^0-9a-zA-Z ]', '_', search['author']) - if DEBUG: + if self.verbose: logger().info(" searching by name: %s - %s" % (title, author)) hits = lib_books.Search('%s - %s' % (title, author), self.SearchField.index('All')) @@ -2001,16 +2084,16 @@ class ITUNES(DriverBase): logger().info(" found '%s' by %s (%s)" % (hit.Name, hit.Artist, hit.Composer)) return hit else: - if DEBUG: + if self.verbose: logger().info(" no PDF hits") attempts -= 1 time.sleep(0.5) - if DEBUG: - logger().warning(" attempt #%d" % (10 - attempts)) + if self.verbose: + logger().info(" attempt #%d" % (10 - attempts)) - if DEBUG: - logger().error(" search for '%s' yielded no hits" % search['title']) + if self.verbose: + logger().info(" search for '%s' yielded no hits" % search['title']) return None def _generate_thumbnail(self, book_path, book): @@ -2049,7 +2132,7 @@ class ITUNES(DriverBase): logger().info(" returning thumb from cache for '%s'" % title) return thumb_data - if DEBUG: + if self.verbose: logger().info(" %s._generate_thumbnail('%s'):" % (self.__class__.__name__, title)) if isosx: @@ -2058,7 +2141,7 @@ class ITUNES(DriverBase): data = book.artworks[1].raw_data().data except: # If no artwork, write an empty marker to cache - if DEBUG: + if self.verbose: logger().error(" error fetching iTunes artwork for '%s'" % title) zfw.writestr(thumb_path, 'None') zfw.close() @@ -2080,8 +2163,8 @@ class ITUNES(DriverBase): # Cache the tagged thumb zfw.writestr(thumb_path, thumb_data) except: - if DEBUG: - logger().error(" error generating thumb for '%s', caching empty marker" % book.name()) + if self.verbose: + logger().info(" ERROR: error generating thumb for '%s', caching empty marker" % book.name()) self._dump_hex(data[:32]) thumb_data = None # Cache the empty cover @@ -2094,7 +2177,7 @@ class ITUNES(DriverBase): elif iswindows: if not book.Artwork.Count: - if DEBUG: + if self.verbose: logger().info(" no artwork available for '%s'" % book.Name) zfw.writestr(thumb_path, 'None') zfw.close() @@ -2119,7 +2202,7 @@ class ITUNES(DriverBase): # Cache the tagged thumb zfw.writestr(thumb_path, thumb_data) except: - if DEBUG: + if self.verbose: logger().error(" error generating thumb for '%s', caching empty marker" % book.Name) thumb_data = None # Cache the empty cover @@ -2154,7 +2237,7 @@ class ITUNES(DriverBase): ''' Assumes pythoncom wrapper for Windows ''' - if DEBUG: + if self.verbose: logger().info("\n %s._get_device_books()" % self.__class__.__name__) device_books = [] @@ -2167,7 +2250,7 @@ class ITUNES(DriverBase): dev_books = None for pl in device.playlists(): if pl.special_kind() == appscript.k.Books: - if DEBUG: + if self.verbose: logger().info(" Book playlist: '%s'" % (pl.name())) dev_books = pl.file_tracks() break @@ -2176,14 +2259,14 @@ class ITUNES(DriverBase): for book in dev_books: if book.kind() in self.Audiobooks: - if DEBUG: + if self.verbose: logger().info(" ignoring '%s' of type '%s'" % (book.name(), book.kind())) else: - if DEBUG: + if self.verbose: logger().info(" %-40.40s %-30.30s %-40.40s [%s]" % (book.name(), book.artist(), book.composer(), book.kind())) device_books.append(book) - if DEBUG: + if self.verbose: logger().info() elif iswindows: @@ -2199,23 +2282,23 @@ class ITUNES(DriverBase): for pl in device.Playlists: if pl.Kind == self.PlaylistKind.index('User') and \ pl.SpecialKind == self.PlaylistSpecialKind.index('Books'): - if DEBUG: + if self.verbose: logger().info(" Books playlist: '%s'" % (pl.Name)) dev_books = pl.Tracks break else: - if DEBUG: + if self.verbose: logger().info(" no Books playlist found") for book in dev_books: if book.KindAsString in self.Audiobooks: - if DEBUG: + if self.verbose: logger().info(" ignoring '%s' of type '%s'" % (book.Name, book.KindAsString)) else: - if DEBUG: + if self.verbose: logger().info(" %-40.40s %-30.30s %-40.40s [%s]" % (book.Name, book.Artist, book.Composer, book.KindAsString)) device_books.append(book) - if DEBUG: + if self.verbose: logger().info() finally: @@ -2238,7 +2321,7 @@ class ITUNES(DriverBase): pl.SpecialKind == self.PlaylistSpecialKind.index('Books'): break else: - if DEBUG: + if self.verbose: logger().error(" no iPad|Books playlist found") return pl @@ -2247,7 +2330,7 @@ class ITUNES(DriverBase): Populate a dict of paths from iTunes Library|Books Windows assumes pythoncom wrapper ''' - if DEBUG: + if self.verbose: logger().info("\n %s._get_library_books()" % self.__class__.__name__) library_books = {} @@ -2259,11 +2342,11 @@ class ITUNES(DriverBase): for source in self.iTunes.sources(): if source.kind() == appscript.k.library: lib = source - if DEBUG: + if self.verbose: logger().info(" Library source: '%s'" % (lib.name())) break else: - if DEBUG: + if self.verbose: logger().error(' Library source not found') if lib is not None: @@ -2271,18 +2354,18 @@ class ITUNES(DriverBase): if lib.playlists(): for pl in lib.playlists(): if pl.special_kind() == appscript.k.Books: - if DEBUG: + if self.verbose: logger().info(" Books playlist: '%s'" % (pl.name())) break else: - if DEBUG: + if self.verbose: logger().info(" no Library|Books playlist found") lib_books = pl.file_tracks() for book in lib_books: # This may need additional entries for international iTunes users if book.kind() in self.Audiobooks: - if DEBUG: + if self.verbose: logger().info(" ignoring '%s' of type '%s'" % (book.name(), book.kind())) else: # Collect calibre orphans - remnants of recipe uploads @@ -2295,18 +2378,18 @@ class ITUNES(DriverBase): if False: logger().info(" found iTunes PTF '%s' in Library|Books" % book.name()) except: - if DEBUG: + if self.verbose: logger().error(" iTunes returned an error returning .location() with %s" % book.name()) library_books[path] = book - if DEBUG: + if self.verbose: logger().info(" %-30.30s %-30.30s %-40.40s [%s]" % (book.name(), book.artist(), book.album(), book.kind())) else: - if DEBUG: + if self.verbose: logger().info(' no Library playlists') else: - if DEBUG: + if self.verbose: logger().info(' no Library found') elif iswindows: @@ -2325,22 +2408,22 @@ class ITUNES(DriverBase): for pl in lib.Playlists: if pl.Kind == self.PlaylistKind.index('User') and \ pl.SpecialKind == self.PlaylistSpecialKind.index('Books'): - if DEBUG: + if self.verbose: logger().info(" Books playlist: '%s'" % (pl.Name)) lib_books = pl.Tracks break else: - if DEBUG: + if self.verbose: logger().error(" no Library|Books playlist found") else: - if DEBUG: + if self.verbose: logger().error(" no Library playlists found") try: for book in lib_books: # This may need additional entries for international iTunes users if book.KindAsString in self.Audiobooks: - if DEBUG: + if self.verbose: logger().info(" ignoring %-30.30s of type '%s'" % (book.Name, book.KindAsString)) else: format = 'pdf' if book.KindAsString.startswith('PDF') else 'epub' @@ -2354,10 +2437,10 @@ class ITUNES(DriverBase): logger().info(" found iTunes PTF '%s' in Library|Books" % book.Name) library_books[path] = book - if DEBUG: + if self.verbose: logger().info(" %-30.30s %-30.30s %-40.40s [%s]" % (book.Name, book.Artist, book.Album, book.KindAsString)) except: - if DEBUG: + if self.verbose: logger().info(" no books in library") self.library_orphans = library_orphans @@ -2403,7 +2486,7 @@ class ITUNES(DriverBase): # If more than one connected iDevice, remove all from list to prevent driver initialization if kinds.count('iPod') > 1: - if DEBUG: + if self.verbose: logger().error(" %d connected iPod devices detected, calibre supports a single connected iDevice" % kinds.count('iPod')) while kinds.count('iPod'): index = kinds.index('iPod') @@ -2423,7 +2506,7 @@ class ITUNES(DriverBase): def _launch_iTunes(self): ''' ''' - if DEBUG: + if self.verbose: logger().info(" %s._launch_iTunes():\n Instantiating iTunes" % self.__class__.__name__) if isosx: @@ -2436,7 +2519,7 @@ class ITUNES(DriverBase): # Instantiate iTunes running_apps = appscript.app('System Events') if not 'iTunes' in running_apps.processes.name(): - if DEBUG: + if self.verbose: logger().info("%s:_launch_iTunes(): Launching iTunes" % self.__class__.__name__) try: self.iTunes = iTunes = appscript.app('iTunes', hide=True) @@ -2469,7 +2552,7 @@ class ITUNES(DriverBase): as_binding = "static" except: self.iTunes = None - if DEBUG: + if self.verbose: logger().info(" unable to communicate with iTunes via %s %s using any binding" % (as_name, as_version)) return @@ -2486,7 +2569,7 @@ class ITUNES(DriverBase): logger().error(" media_dir: %s" % media_dir) ''' - if DEBUG: + if self.verbose: import platform logger().info(" %s %s" % (__appname__, __version__)) logger().info(" [OSX %s, %s %s (%s), %s driver version %d.%d.%d]" % @@ -2522,7 +2605,7 @@ class ITUNES(DriverBase): raise OpenFeedback('Unable to launch iTunes.\n' + 'Try launching calibre as Administrator') - if not DEBUG: + if not self.verbose: self.iTunes.Windows[0].Minimized = True self.initial_status = 'launched' @@ -2556,7 +2639,7 @@ class ITUNES(DriverBase): logger().error(" no media dir found: string: %s" % string) ''' - if DEBUG: + if self.verbose: logger().info(" %s %s" % (__appname__, __version__)) logger().info(" [Windows %s - %s (%s), driver version %d.%d.%d]" % (self.iTunes.Windows[0].name, self.iTunes.Version, self.initial_status, @@ -2571,7 +2654,7 @@ class ITUNES(DriverBase): ''' PURGE_ORPHANS = False - if DEBUG: + if self.verbose: logger().info(" %s._purge_orphans()" % self.__class__.__name__) #self._dump_library_books(library_books) #logger().info(" cached_books:\n %s" % "\n ".join(cached_books.keys())) @@ -2581,7 +2664,7 @@ class ITUNES(DriverBase): if book not in cached_books and \ str(library_books[book].description()).startswith(self.description_prefix): if PURGE_ORPHANS: - if DEBUG: + if self.verbose: logger().info(" '%s' not found on iDevice, removing from iTunes" % book) btr = { 'title': library_books[book].name(), @@ -2589,14 +2672,14 @@ class ITUNES(DriverBase): 'lib_book': library_books[book]} self._remove_from_iTunes(btr) else: - if DEBUG: + if self.verbose: logger().info(" '%s' found in iTunes, but not on iDevice" % (book)) elif iswindows: if book not in cached_books and \ library_books[book].Description.startswith(self.description_prefix): if PURGE_ORPHANS: - if DEBUG: + if self.verbose: logger().info(" '%s' not found on iDevice, removing from iTunes" % book) btr = { 'title': library_books[book].Name, @@ -2604,28 +2687,28 @@ class ITUNES(DriverBase): 'lib_book': library_books[book]} self._remove_from_iTunes(btr) else: - if DEBUG: + if self.verbose: logger().info(" '%s' found in iTunes, but not on iDevice" % (book)) def _remove_existing_copy(self, path, metadata): ''' ''' - if DEBUG: + if self.verbose: logger().info(" %s._remove_existing_copy()" % self.__class__.__name__) if self.manual_sync_mode: # Delete existing from Device|Books, add to self.update_list - # for deletion from booklist[0] during add_books_to_metadata + # for deletion from booklist[0] during remove_books_to_metadata for book in self.cached_books: if (self.cached_books[book]['uuid'] == metadata.uuid or (self.cached_books[book]['title'] == metadata.title and self.cached_books[book]['author'] == metadata.author)): self.update_list.append(self.cached_books[book]) self._remove_from_device(self.cached_books[book]) - self._remove_from_iTunes(self.cached_books[book]) + #self._remove_from_iTunes(self.cached_books[book]) break else: - if DEBUG: + if self.verbose: logger().info(" '%s' not in cached_books" % metadata.title) else: # Delete existing from Library|Books, add to self.update_list @@ -2635,35 +2718,35 @@ class ITUNES(DriverBase): (self.cached_books[book]['title'] == metadata.title and self.cached_books[book]['author'] == metadata.author)): self.update_list.append(self.cached_books[book]) - if DEBUG: + if self.verbose: logger().info(" deleting library book '%s'" % metadata.title) self._remove_from_iTunes(self.cached_books[book]) break else: - if DEBUG: + if self.verbose: logger().info(" '%s' not found in cached_books" % metadata.title) def _remove_from_device(self, cached_book): ''' Windows assumes pythoncom wrapper ''' - if DEBUG: + if self.verbose: logger().info(" %s._remove_from_device()" % self.__class__.__name__) if isosx: - if DEBUG: + if self.verbose: logger().info(" deleting '%s' from iDevice" % cached_book['title']) try: cached_book['dev_book'].delete() except: logger().error(" error deleting '%s'" % cached_book['title']) elif iswindows: - hit = self._find_device_book(cached_book) + hit = self._find_device_book(cached_book, attempts=1) if hit: - if DEBUG: + if self.verbose: logger().info(" deleting '%s' from iDevice" % cached_book['title']) hit.Delete() else: - if DEBUG: + if self.verbose: logger().warning(" unable to remove '%s' by '%s' (%s) from device" % (cached_book['title'], cached_book['author'], cached_book['uuid'])) @@ -2671,14 +2754,14 @@ class ITUNES(DriverBase): ''' iTunes does not delete books from storage when removing from database via automation ''' - if DEBUG: + if self.verbose: logger().info(" %s._remove_from_iTunes():" % self.__class__.__name__) if isosx: ''' Manually remove the book from iTunes storage ''' try: fp = cached_book['lib_book'].location().path - if DEBUG: + if self.verbose: logger().info(" processing %s" % fp) if fp.startswith(prefs['library_path']): logger().info(" '%s' stored in calibre database, not removed" % cached_book['title']) @@ -2687,18 +2770,18 @@ class ITUNES(DriverBase): os.path.exists(fp): # Delete the copy in iTunes_local_storage os.remove(fp) - if DEBUG: + if self.verbose: logger()(" removing from iTunes_local_storage") else: # Delete from iTunes Media folder if os.path.exists(fp): os.remove(fp) - if DEBUG: + if self.verbose: logger().info(" deleting from iTunes storage") author_storage_path = os.path.split(fp)[0] try: os.rmdir(author_storage_path) - if DEBUG: + if self.verbose: logger().info(" removing empty author directory") except: author_files = os.listdir(author_storage_path) @@ -2706,24 +2789,24 @@ class ITUNES(DriverBase): author_files.pop(author_files.index('.DS_Store')) if not author_files: os.rmdir(author_storage_path) - if DEBUG: + if self.verbose: logger().info(" removing empty author directory") else: logger().info(" '%s' does not exist at storage location" % cached_book['title']) except: # We get here if there was an error with .location().path - if DEBUG: + if self.verbose: logger().info(" '%s' by %s not found in iTunes storage" % (cached_book['title'], cached_book['author'])) # Delete the book from the iTunes database try: self.iTunes.delete(cached_book['lib_book']) - if DEBUG: + if self.verbose: logger().info(" removing from iTunes database") except: - if DEBUG: + if self.verbose: logger().info(" unable to remove from iTunes database") elif iswindows: @@ -2741,7 +2824,7 @@ class ITUNES(DriverBase): fp = book.Location if book: - if DEBUG: + if self.verbose: logger().info(" processing %s" % fp) if fp.startswith(prefs['library_path']): logger().info(" '%s' stored in calibre database, not removed" % cached_book['title']) @@ -2750,34 +2833,34 @@ class ITUNES(DriverBase): os.path.exists(fp): # Delete the copy in iTunes_local_storage os.remove(fp) - if DEBUG: + if self.verbose: logger()(" removing from iTunes_local_storage") else: # Delete from iTunes Media folder if os.path.exists(fp): os.remove(fp) - if DEBUG: + if self.verbose: logger().info(" deleting from iTunes storage") author_storage_path = os.path.split(fp)[0] try: os.rmdir(author_storage_path) - if DEBUG: + if self.verbose: logger().info(" removing empty author directory") except: pass else: logger().info(" '%s' does not exist at storage location" % cached_book['title']) else: - if DEBUG: + if self.verbose: logger().info(" '%s' not found in iTunes storage" % cached_book['title']) # Delete the book from the iTunes database try: book.Delete() - if DEBUG: + if self.verbose: logger().info(" removing from iTunes database") except: - if DEBUG: + if self.verbose: logger().info(" unable to remove from iTunes database") def title_sorter(self, title): @@ -2791,7 +2874,7 @@ class ITUNES(DriverBase): from lxml import etree from calibre.utils.zipfile import ZipFile - if DEBUG: + if self.verbose: logger().info(" %s._update_epub_metadata()" % self.__class__.__name__) # Fetch plugboard updates @@ -2819,16 +2902,16 @@ class ITUNES(DriverBase): old_ts = parse_date(timestamp) metadata.timestamp = datetime.datetime(old_ts.year, old_ts.month, old_ts.day, old_ts.hour, old_ts.minute, old_ts.second, old_ts.microsecond + 1, old_ts.tzinfo) - if DEBUG: + if self.verbose: logger().info(" existing timestamp: %s" % metadata.timestamp) else: metadata.timestamp = now() - if DEBUG: + if self.verbose: logger().info(" add timestamp: %s" % metadata.timestamp) else: metadata.timestamp = now() - if DEBUG: + if self.verbose: logger().warning(" missing block in OPF file") logger().info(" add timestamp: %s" % metadata.timestamp) @@ -2859,7 +2942,7 @@ class ITUNES(DriverBase): ''' Trigger a sync, wait for completion ''' - if DEBUG: + if self.verbose: logger().info(" %s:_update_device():\n %s" % (self.__class__.__name__, msg)) if isosx: @@ -2867,11 +2950,11 @@ class ITUNES(DriverBase): if wait: # This works if iTunes has books not yet synced to iPad. - if DEBUG: + if self.verbose: sys.stdout.write(" waiting for iPad sync to complete ...") sys.stdout.flush() while len(self._get_device_books()) != (len(self._get_library_books()) + len(self._get_purchased_book_ids())): - if DEBUG: + if self.verbose: sys.stdout.write('.') sys.stdout.flush() time.sleep(2) @@ -2884,7 +2967,7 @@ class ITUNES(DriverBase): self.iTunes = win32com.client.Dispatch("iTunes.Application") self.iTunes.UpdateIPod() if wait: - if DEBUG: + if self.verbose: sys.stdout.write(" waiting for iPad sync to complete ...") sys.stdout.flush() while True: @@ -2892,7 +2975,7 @@ class ITUNES(DriverBase): lb_count = len(self._get_library_books()) pb_count = len(self._get_purchased_book_ids()) if db_count != lb_count + pb_count: - if DEBUG: + if self.verbose: #sys.stdout.write(' %d != %d + %d\n' % (db_count,lb_count,pb_count)) sys.stdout.write('.') sys.stdout.flush() @@ -2907,7 +2990,7 @@ class ITUNES(DriverBase): def _update_iTunes_metadata(self, metadata, db_added, lb_added, this_book): ''' ''' - if DEBUG: + if self.verbose: logger().info(" %s._update_iTunes_metadata()" % self.__class__.__name__) STRIP_TAGS = re.compile(r'<[^<]*?/?>') @@ -2959,7 +3042,7 @@ class ITUNES(DriverBase): # Otherwise iTunes grabs the first dc:subject from the opf metadata # If title_sort applied in plugboard, that overrides using series/index as title_sort if metadata_x.series and self.settings().extra_customization[self.USE_SERIES_AS_CATEGORY]: - if DEBUG: + if self.verbose: logger().info(" %s._update_iTunes_metadata()" % self.__class__.__name__) logger().info(" using Series name '%s' as Genre" % metadata_x.series) @@ -2985,7 +3068,8 @@ class ITUNES(DriverBase): break if db_added: - logger().warning(" waiting for db_added to become writeable ") + if self.verbose: + logger().info(" waiting for db_added to become writable ") time.sleep(1.0) # If no title_sort plugboard tweak, create sort_name from series/index if metadata.title_sort == metadata_x.title_sort: @@ -3003,7 +3087,7 @@ class ITUNES(DriverBase): break elif metadata_x.tags is not None: - if DEBUG: + if self.verbose: logger().info(" %susing Tag as Genre" % "no Series name available, " if self.settings().extra_customization[self.USE_SERIES_AS_CATEGORY] else '') for tag in metadata_x.tags: @@ -3027,7 +3111,8 @@ class ITUNES(DriverBase): lb_added.Year = metadata_x.pubdate.year if db_added: - logger().warning(" waiting for db_added to become writeable ") + if self.verbose: + logger().info(" waiting for db_added to become writable ") time.sleep(1.0) db_added.Name = metadata_x.title db_added.Album = metadata_x.title @@ -3053,7 +3138,7 @@ class ITUNES(DriverBase): if db_added: db_added.AlbumRating = (metadata_x.rating * 10) except: - if DEBUG: + if self.verbose: logger().warning(" iTunes automation interface reported an error" " setting AlbumRating on iDevice") @@ -3062,7 +3147,7 @@ class ITUNES(DriverBase): # iTunes balks on setting EpisodeNumber, but it sticks (9.1.1.12) if metadata_x.series and self.settings().extra_customization[self.USE_SERIES_AS_CATEGORY]: - if DEBUG: + if self.verbose: logger().info(" using Series name as Genre") # Format the index as a sort key index = metadata_x.series_index @@ -3078,13 +3163,13 @@ class ITUNES(DriverBase): try: lb_added.TrackNumber = metadata_x.series_index except: - if DEBUG: + if self.verbose: logger().warning(" iTunes automation interface reported an error" " setting TrackNumber in iTunes") try: lb_added.EpisodeNumber = metadata_x.series_index except: - if DEBUG: + if self.verbose: logger().warning(" iTunes automation interface reported an error" " setting EpisodeNumber in iTunes") @@ -3106,13 +3191,13 @@ class ITUNES(DriverBase): try: db_added.TrackNumber = metadata_x.series_index except: - if DEBUG: + if self.verbose: logger().warning(" iTunes automation interface reported an error" " setting TrackNumber on iDevice") try: db_added.EpisodeNumber = metadata_x.series_index except: - if DEBUG: + if self.verbose: logger().warning(" iTunes automation interface reported an error" " setting EpisodeNumber on iDevice") @@ -3126,7 +3211,7 @@ class ITUNES(DriverBase): break elif metadata_x.tags is not None: - if DEBUG: + if self.verbose: logger().info(" using Tag as Genre") for tag in metadata_x.tags: if self._is_alpha(tag[0]): @@ -3140,7 +3225,7 @@ class ITUNES(DriverBase): ''' Ensure iDevice metadata is writable. DC mode only ''' - if DEBUG: + if self.verbose: logger().info(" %s._wait_for_writable_metadata()" % self.__class__.__name__) attempts = 9 @@ -3154,23 +3239,23 @@ class ITUNES(DriverBase): except: attempts -= 1 time.sleep(delay) - if DEBUG: - logger().warning(" waiting %.1f seconds for iDevice metadata to become writable (attempt #%d)" % + if self.verbose: + logger().info(" waiting %.1f seconds for iDevice metadata to become writable (attempt #%d)" % (delay, (10 - attempts))) else: - if DEBUG: - logger().error(" failed to write device metadata") + if self.verbose: + logger().info(" ERROR: failed to write device metadata") def _xform_metadata_via_plugboard(self, book, format): ''' Transform book metadata from plugboard templates ''' - if DEBUG: + if self.verbose: logger().info(" %s._xform_metadata_via_plugboard()" % self.__class__.__name__) if self.plugboard_func: pb = self.plugboard_func(self.DEVICE_PLUGBOARD_NAME, format, self.plugboards) newmi = book.deepcopy_metadata() newmi.template_to_attribute(book, pb) - if pb is not None and DEBUG: + if pb is not None and self.verbose: #logger().info(" transforming %s using %s:" % (format, pb)) logger().info(" title: '%s' %s" % (book.title, ">>> '%s'" % newmi.title if book.title != newmi.title else '')) @@ -3187,7 +3272,7 @@ class ITUNES(DriverBase): logger().info(" tags: %s %s" % (book.tags, ">>> %s" % newmi.tags if book.tags != newmi.tags else '')) else: - if DEBUG: + if self.verbose: logger()(" matching plugboard not found") else: @@ -3211,8 +3296,9 @@ class ITUNES_ASYNC(ITUNES): connected = False def __init__(self, path): - if DEBUG: - logger().info("%s.__init__()" % self.__class__.__name__) + self.verbose = self.settings().extra_customization[3] + if self.verbose: + logger().info("%s.__init__():" % self.__class__.__name__) try: import appscript @@ -3262,7 +3348,7 @@ class ITUNES_ASYNC(ITUNES): """ from calibre.utils.date import parse_date if not oncard: - if DEBUG: + if self.verbose: logger().info("%s.books()" % self.__class__.__name__) if self.settings().extra_customization[self.CACHE_COVERS]: logger().info(" Cover fetching/caching enabled") @@ -3311,7 +3397,7 @@ class ITUNES_ASYNC(ITUNES): } if self.report_progress is not None: - self.report_progress((i + 1) / book_count, + self.report_progress(float((i + 1)*100 / book_count)/100, _('%(num)d of %(tot)d') % dict(num=i + 1, tot=book_count)) elif iswindows: @@ -3353,7 +3439,7 @@ class ITUNES_ASYNC(ITUNES): } if self.report_progress is not None: - self.report_progress((i + 1) / book_count, + self.report_progress(float((i + 1)*100 / book_count)/100, _('%(num)d of %(tot)d') % dict(num=i + 1, tot=book_count)) @@ -3363,7 +3449,7 @@ class ITUNES_ASYNC(ITUNES): if self.report_progress is not None: self.report_progress(1.0, _('finished')) self.cached_books = cached_books - if DEBUG: + if self.verbose: self._dump_booklist(booklist, 'returning from books()', indent=2) self._dump_cached_books('returning from books()', indent=2) return booklist @@ -3376,7 +3462,7 @@ class ITUNES_ASYNC(ITUNES): Un-mount / eject the device from the OS. This does not check if there are pending GUI jobs that need to communicate with the device. ''' - if DEBUG: + if self.verbose: logger().info("%s.eject()" % self.__class__.__name__) self.iTunes = None self.connected = False @@ -3391,7 +3477,7 @@ class ITUNES_ASYNC(ITUNES): @return: A 3 element list with free space in bytes of (1, 2, 3). If a particular device doesn't have any of these locations it should return -1. """ - if DEBUG: + if self.verbose: logger().info("%s.free_space()" % self.__class__.__name__) free_space = 0 if isosx: @@ -3408,7 +3494,7 @@ class ITUNES_ASYNC(ITUNES): Ask device for device information. See L{DeviceInfoQuery}. @return: (device name, device version, software version on device, mime type) """ - if DEBUG: + if self.verbose: logger().info("%s.get_device_information()" % self.__class__.__name__) return ('iTunes', 'hw v1.0', 'sw v1.0', 'mime type normally goes here') @@ -3435,13 +3521,13 @@ class ITUNES_ASYNC(ITUNES): if self.iTunes is None: raise OpenFeedback(self.ITUNES_SANDBOX_LOCKOUT_MESSAGE) - if DEBUG: + if self.verbose: logger().info("%s.open(connected_device: %s)" % (self.__class__.__name__, repr(connected_device))) # Confirm/create thumbs archive if not os.path.exists(self.cache_dir): - if DEBUG: + if self.verbose: logger().info(" creating thumb cache '%s'" % self.cache_dir) os.makedirs(self.cache_dir) @@ -3451,18 +3537,18 @@ class ITUNES_ASYNC(ITUNES): zfw.writestr("iTunes Thumbs Archive", '') zfw.close() else: - if DEBUG: + if self.verbose: logger().info(" existing thumb cache at '%s'" % self.archive_path) # If enabled in config options, create/confirm an iTunes storage folder if not self.settings().extra_customization[self.USE_ITUNES_STORAGE]: self.iTunes_local_storage = os.path.join(config_dir, 'iTunes storage') if not os.path.exists(self.iTunes_local_storage): - if DEBUG: + if self.verbose: logger()(" creating iTunes_local_storage at '%s'" % self.iTunes_local_storage) os.mkdir(self.iTunes_local_storage) else: - if DEBUG: + if self.verbose: logger()(" existing iTunes_local_storage at '%s'" % self.iTunes_local_storage) def sync_booklists(self, booklists, end_session=True): @@ -3473,7 +3559,7 @@ class ITUNES_ASYNC(ITUNES): L{books}(oncard='cardb')). ''' - if DEBUG: + if self.verbose: logger().info("%s.sync_booklists()" % self.__class__.__name__) # Inform user of any problem books @@ -3487,7 +3573,7 @@ class ITUNES_ASYNC(ITUNES): def unmount_device(self): ''' ''' - if DEBUG: + if self.verbose: logger().info("%s.unmount_device()" % self.__class__.__name__) self.connected = False diff --git a/src/calibre/devices/idevice/__init__.py b/src/calibre/devices/idevice/__init__.py new file mode 100644 index 0000000000..c705e32a66 --- /dev/null +++ b/src/calibre/devices/idevice/__init__.py @@ -0,0 +1,2 @@ +__license__ = 'GPL v3' +__copyright__ = '2008, Kovid Goyal ' \ No newline at end of file diff --git a/src/calibre/devices/idevice/libimobiledevice.py b/src/calibre/devices/idevice/libimobiledevice.py new file mode 100644 index 0000000000..25be1d9854 --- /dev/null +++ b/src/calibre/devices/idevice/libimobiledevice.py @@ -0,0 +1,1686 @@ +#!/usr/bin/env python +# coding: utf-8 + +__license__ = 'GPL v3' +__copyright__ = '2013, Gregory Riker' + +''' + Wrapper for libiMobileDevice library based on API documentation at + http://www.libimobiledevice.org/docs/html/globals.html +''' + +import os, sys + +from collections import OrderedDict +from ctypes import ( + c_int, c_long, c_void_p, c_char_p, Structure, POINTER, byref, cdll, c_char, c_ulonglong, + c_uint, c_ubyte, create_string_buffer, string_at) + +from calibre.constants import DEBUG, islinux, isosx, iswindows +from calibre.devices.idevice.parse_xml import XmlPropertyListParser +from calibre.devices.usbms.driver import debug_print + + +class libiMobileDeviceException(Exception): + def __init__(self, value): + self.value = value + + def __str__(self): + return repr(self.value) + + +class libiMobileDeviceIOException(Exception): + def __init__(self, value): + self.value = value + + def __str__(self): + return repr(self.value) + + +class AFC_CLIENT_T(Structure): + ''' + http://www.libimobiledevice.org/docs/html/structafc__client__private.html + ''' + _fields_ = [ + # afc_client_private (afc.h) + # service_client_private (service.h) + # idevice_connection_private (idevice.h) + ('connection_type', c_int), + ('data', c_void_p), + + # ssl_data_private (idevice.h) + ('session', c_void_p), + ('ctx', c_void_p), + ('bio', c_void_p), + + # afc_client_private (afc.h) + ('afc_packet', c_void_p), + ('file_handle', c_int), + ('lock', c_int), + + # mutex - (Windows only?) (WinNT.h) + ('LockCount', c_long), + ('RecursionCount', c_long), + ('OwningThread', c_void_p), + ('LockSemaphore', c_void_p), + ('SpinCount', c_void_p), + + # afc_client_private (afc.h) + ('free_parent', c_int)] + + +class HOUSE_ARREST_CLIENT_T(Structure): + ''' + http://www.libimobiledevice.org/docs/html/structhouse__arrest__client__private.html + ''' + _fields_ = [ + # property_list_service_client + # idevice_connection_private (idevice.h) + ('type', c_int), + ('data', c_void_p), + + # ssl_data_private (idevice.h) + ('session', c_void_p), + ('ctx', c_void_p), + ('bio', c_void_p), + + # (house_arrest.h) + ('mode', c_int) + ] + + +class IDEVICE_T(Structure): + ''' + http://www.libimobiledevice.org/docs/html/structidevice__private.html + ''' + _fields_ = [ + ("udid", c_char_p), + ("conn_type", c_int), + ("conn_data", c_void_p)] + + +class INSTPROXY_CLIENT_T(Structure): + ''' + http://www.libimobiledevice.org/docs/html/structinstproxy__client__private.html + ''' + _fields_ = [ + # instproxy_client_private (installation_proxy.h) + # idevice_connection_private (idevice.h) + ('connection_type', c_int), + ('data', c_void_p), + + # ssl_data_private (idevice.h) + ('session', c_void_p), + ('ctx', c_void_p), + ('bio', c_void_p), + + # mutex - Windows only (WinNT.h) + ('LockCount', c_long), + ('RecursionCount', c_long), + ('OwningThread', c_void_p), + ('LockSemaphore', c_void_p), + ('SpinCount', c_void_p), + ('status_updater', c_void_p) + ] + + +class LOCKDOWND_CLIENT_T(Structure): + ''' + http://www.libimobiledevice.org/docs/html/structlockdownd__client__private.html + ''' + _fields_ = [ + # lockdownd_client_private + # property_list_service_client + # idevice_connection_private + ('connection_type', c_int), + ('data', c_void_p), + + # ssl_data_private + ('session', c_char_p), + ('ctx', c_char_p), + ('bio', c_char_p), + + # lockdown_client_private + ('ssl_enabled', c_int), + ('session_id', c_char_p), + ('udid', c_char_p), + ('label', c_char_p)] + + +class LOCKDOWND_SERVICE_DESCRIPTOR(Structure): + ''' + from libimobiledevice/include/libimobiledevice/lockdown.h + ''' + _fields_ = [ + ('port', c_uint), + ('ssl_enabled', c_ubyte) + ] + + +class libiMobileDevice(): + ''' + Wrapper for libiMobileDevice + ''' + # AFC File operation enumerations + AFC_FOPEN_RDONLY = 1 + AFC_FOPEN_RW = 2 + AFC_FOPEN_WRONLY = 3 + AFC_FOPEN_WR = 4 + AFC_FOPEN_APPEND = 5 + AFC_FOPEN_RDAPPEND = 6 + + # Error reporting template + LIB_ERROR_TEMPLATE = "ERROR: {cls}:{func}(): {desc}" + + # Location reporting template + LOCATION_TEMPLATE = "{cls}:{func}({arg1}) {arg2}" + + # iDevice udid string + UDID_SIZE = 40 + + def __init__(self, log=debug_print, verbose=False): + self.log = log + self.verbose = verbose + + self._log_location() + self.afc = None + self.app_version = 0 + self.client_options = None + self.control = None + self.device = None + self.device_connected = None + self.device_info = None + self.device_mounted = False + self.device_name = None + self.file_stats = {} + self.house_arrest = None + self.installed_apps = None + self.instproxy = None + + self.load_library() + + # ~~~ Public methods ~~~ + def connect_idevice(self): + ''' + Convenience method to get iDevice ready to talk + ''' + self._log_location() + self.device_connected = False + try: + self.device = self._idevice_new() + self.control = self._lockdown_client_new_with_handshake() + self.device_name = self._lockdown_get_device_name() + self._lockdown_start_service("com.apple.mobile.installation_proxy") + self.device_connected = True + + except libiMobileDeviceException as e: + self.log(e.value) + self.disconnect_idevice() + + return self.device_connected + + def copy_to_iDevice(self, src, dst): + ''' + High-level convenience method to copy src on local filesystem to + dst on iDevice. + src: file on local filesystem + dst: file to be created on iOS filesystem + ''' + self._log_location("src='%s', dst='%s'" % (src, dst)) + with open(src) as f: + content = bytearray(f.read()) + mode = 'wb' + handle = self._afc_file_open(dst, mode=mode) + if handle is not None: + success = self._afc_file_write(handle, content, mode=mode) + if self.verbose: + self.log(" success: %s" % success) + self._afc_file_close(handle) + else: + if self.verbose: + self.log(" could not create copy") + + def copy_from_iDevice(self, src, dst): + ''' + High-level convenience method to copy from src on iDevice to + dst on local filesystem. + src: path to file on iDevice + dst: file object on local filesystem + ''' + self._log_location("src='%s', dst='%s'" % (src, dst.name)) + data = self.read(src, mode='rb') + dst.write(data) + dst.close() + + # Update timestamps to match + file_stats = self._afc_get_file_info(src) + os.utime(dst.name, (file_stats['st_mtime'], file_stats['st_mtime'])) + + def disconnect_idevice(self): + ''' + Convenience method to close connection + ''' + self._log_location(self.device_name) + if self.device_mounted: + self._afc_client_free() + self._house_arrest_client_free() + #self._lockdown_goodbye() + self._idevice_free() + self.device_mounted = False + else: + if self.verbose: + self.log(" device already disconnected") + + def dismount_ios_media_folder(self): + self._afc_client_free() + #self._lockdown_goodbye() + self._idevice_free() + self.device_mounted = False + + def exists(self, path): + ''' + Determine if path exists + + Returns file_info or {} + ''' + self._log_location("'%s'" % path) + return self._afc_get_file_info(path) + + def get_device_info(self): + ''' + Return device profile: + {'Model': 'iPad2,5', + 'FSTotalBytes': '14738952192', + 'FSFreeBytes': '11264917504', + 'FSBlockSize': '4096'} + ''' + self._log_location() + self.device_info = self._afc_get_device_info() + return self.device_info + + def get_device_list(self): + ''' + Return a list of connected udids + ''' + self._log_location() + + self.lib.idevice_get_device_list.argtypes = [POINTER(POINTER(POINTER(c_char * self.UDID_SIZE))), POINTER(c_long)] + + count = c_long(0) + udid = c_char * self.UDID_SIZE + devices = POINTER(POINTER(udid))() + device_list = [] + error = self.lib.idevice_get_device_list(byref(devices), byref(count)) + if error and self.verbose: + self.log(" ERROR: %s" % self._idevice_error(error)) + else: + index = 0 + while devices[index]: + device_list.append(devices[index].contents.value) + index += 1 + if self.verbose: + self.log(" %s" % repr(device_list)) + #self.lib.idevice_device_list_free() + return device_list + + def get_folder_size(self, path): + ''' + Recursively descend through a dir to add all file sizes in folder + ''' + def _calculate_folder_size(path, initial_folder_size): + ''' + Recursively calculate folder size + ''' + this_dir = self._afc_read_directory(path) + folder_size = 0 + for item in this_dir: + folder_size += int(this_dir[item]['st_size']) + if this_dir[item]['st_ifmt'] == 'S_IFDIR': + new_path = '/'.join([path, item]) + initial_folder_size += _calculate_folder_size(new_path, folder_size) + return folder_size + initial_folder_size + + self._log_location(path) + stats = self.stat(path) + cumulative_folder_size = _calculate_folder_size(path, int(stats['st_size'])) + return cumulative_folder_size + + def get_installed_apps(self, applist): + ''' + Generate a sorted dict of installed apps from applist + An empty applist returns all installed apps + + {: {'app_version': '1.2.3', 'app_id': 'com.apple.iBooks'}} + ''' + + # For apps in applist, get the details + self.instproxy = self._instproxy_client_new() + self.client_options = self._instproxy_client_options_new() + self._instproxy_client_options_add("ApplicationType", "User") + installed_apps = self._instproxy_browse(applist=applist) + self.installed_apps = OrderedDict() + for app in sorted(installed_apps, key=lambda s: s.lower()): + self.installed_apps[app] = installed_apps[app] + + # Free the resources + self._instproxy_client_options_free() + self._instproxy_client_free() + + def listdir(self, path): + ''' + Return a list containing the names of the entries in the iOS directory + given by path. + ''' + self._log_location("'%s'" % path) + return self._afc_read_directory(path) + + def load_library(self): + if islinux: + env = "linux" + self.lib = cdll.LoadLibrary('libimobiledevice.so.4') + self.plist_lib = cdll.LoadLibrary('libplist.so.1') + elif isosx: + env = "OS X" + + # Load libiMobileDevice + path = 'libimobiledevice.4.dylib' + if hasattr(sys, 'frameworks_dir'): + self.lib = cdll.LoadLibrary(os.path.join(getattr(sys, 'frameworks_dir'), path)) + else: + self.lib = cdll.LoadLibrary(path) + + # Load libplist + path = 'libplist.1.dylib' + if hasattr(sys, 'frameworks_dir'): + self.plist_lib = cdll.LoadLibrary(os.path.join(getattr(sys, 'frameworks_dir'), path)) + else: + self.plist_lib = cdll.LoadLibrary(path) + elif iswindows: + env = "Windows" + self.lib = cdll.LoadLibrary('libimobiledevice.dll') + self.plist_lib = cdll.LoadLibrary('libplist.dll') + + self._log_location(env) + self.log(" libimobiledevice loaded from '%s'" % self.lib._name) + self.log(" libplist loaded from '%s'" % self.plist_lib._name) + + if False: + self._idevice_set_debug_level(DEBUG) + + def mount_ios_app(self, app_name=None, app_id=None): + ''' + Convenience method to get iDevice ready to talk to app_name or app_id + app_name: + Check installed apps for app_name + If available, establish afc connection with app container + app_id: + establish afc connection with app container + ''' + self._log_location(app_name if app_name else app_id) + + self.device_mounted = False + + if app_name: + try: + self.device = self._idevice_new() + self.control = self._lockdown_client_new_with_handshake() + self.device_name = self._lockdown_get_device_name() + + # Get the installed apps + self._lockdown_start_service("com.apple.mobile.installation_proxy") + self.instproxy = self._instproxy_client_new() + self.client_options = self._instproxy_client_options_new() + self._instproxy_client_options_add("ApplicationType", "User") + self.installed_apps = self._instproxy_browse(applist=[app_name]) + self._instproxy_client_options_free() + self._instproxy_client_free() + + if not app_name in self.installed_apps: + self.log(" '%s' not installed on this iDevice" % app_name) + self.disconnect_idevice() + else: + # Mount the app's Container + self._lockdown_start_service("com.apple.mobile.house_arrest") + self.house_arrest = self._house_arrest_client_new() + self._house_arrest_send_command(command='VendContainer', + appid=self.installed_apps[app_name]['app_id']) + self._house_arrest_get_result() + self.afc = self._afc_client_new_from_house_arrest_client() + self._lockdown_client_free() + self.app_version = self.installed_apps[app_name]['app_version'] + self.device_mounted = True + + except libiMobileDeviceException as e: + self.log(e.value) + self.disconnect_idevice() + + elif app_id: + try: + self.device = self._idevice_new() + self.control = self._lockdown_client_new_with_handshake() + self.device_name = self._lockdown_get_device_name() + self._lockdown_start_service("com.apple.mobile.house_arrest") + self.house_arrest = self._house_arrest_client_new() + self._house_arrest_send_command(command='VendContainer', appid=app_id) + self._house_arrest_get_result() + self.afc = self._afc_client_new_from_house_arrest_client() + self._lockdown_client_free() + self.device_mounted = True + + except libiMobileDeviceException as e: + self.log(e.value) + self.disconnect_idevice() + + if self.device_mounted: + self._log_location("'%s' mounted" % (app_name if app_name else app_id)) + else: + self._log_location("unable to mount '%s'" % (app_name if app_name else app_id)) + return self.device_mounted + + def mount_ios_media_folder(self): + ''' + Mount the non-app folders: + AirFair + Airlock + ApplicationArchives + Books + DCIM + DiskAid + Downloads + PhotoData + Photos + Purchases + Safari + general_storage + iTunes_Control + ''' + self._log_location() + try: + self.device = self._idevice_new() + self.control = self._lockdown_client_new_with_handshake() + self._lockdown_start_service("com.apple.afc") + self.afc = self._afc_client_new() + + self._lockdown_client_free() + self.device_mounted = True + + except libiMobileDeviceException as e: + self.log(e.value) + self.dismount_ios_media_folder() + + def read(self, path, mode='r'): + ''' + Convenience method to read from path on iDevice + ''' + self._log_location("'%s', mode='%s'" % (path, mode)) + + data = None + handle = self._afc_file_open(path, mode) + if handle is not None: + file_stats = self._afc_get_file_info(path) + data = self._afc_file_read(handle, int(file_stats['st_size']), mode) + self._afc_file_close(handle) + else: + if self.verbose: + self.log(" could not open file") + raise libiMobileDeviceIOException("could not open file '%s' for reading" % path) + + return data + + def rename(self, from_name, to_name): + ''' + Renames a file or directory on the device + + client: (afc_client_t) The client to have rename + from_name: (const char *) The fully-qualified path to rename from + to_name: (const char *) The fully-qualified path to rename to + ''' + self._log_location("from: '%s' to: '%s'" % (from_name, to_name)) + + error = self.lib.afc_rename_path(byref(self.afc), + str(from_name), + str(to_name)) + if error and self.verbose: + self.log(" ERROR: %s" % self.afc_error(error)) + + def remove(self, path): + ''' + Deletes a file or directory + + client (afc_client_t) The client to use + path (const char *) The fully-qualified path to delete + ''' + self._log_location("'%s'" % path) + + error = self.lib.afc_remove_path(byref(self.afc), str(path)) + + if error and self.verbose: + self.log(" ERROR: %s" % self.afc_error(error)) + + def stat(self, path): + ''' + Return a stat dict for path + file_stats: + {'st_size': '12345', + 'st_blocks': '123', + 'st_nlink': '1', + 'st_ifmt': ['S_IFREG'|'S_IFDIR'], + 'st_mtime': xxx.yyy, + 'st_birthtime': xxx.yyy} + + ''' + self._log_location("'%s'" % path) + return self._afc_get_file_info(path) + + def write(self, content, destination, mode='w'): + ''' + Convenience method to write to path on iDevice + ''' + self._log_location(destination) + + handle = self._afc_file_open(destination, mode=mode) + if handle is not None: + success = self._afc_file_write(handle, content, mode=mode) + if self.verbose: + self.log(" success: %s" % success) + self._afc_file_close(handle) + else: + if self.verbose: + self.log(" could not open file for writing") + raise libiMobileDeviceIOException("could not open file for writing") + + # ~~~ AFC functions ~~~ + # http://www.libimobiledevice.org/docs/html/include_2libimobiledevice_2afc_8h.html + def _afc_client_free(self): + ''' + Frees up an AFC client. + If the connection was created by the client itself, the connection will be closed. + + Args: + client: (AFC_CLIENT_T) The client to free + + Result: + AFC client freed, connection closed + ''' + self._log_location() + + error = self.lib.afc_client_free(byref(self.afc)) & 0xFFFF + if error and self.verbose: + self.log(" ERROR: %s" % self.afc_error(error)) + + def _afc_client_new(self): + ''' + Makes a connection to the AFC service on the device + ''' + self._log_location() + self.afc = None + afc_client_t = POINTER(AFC_CLIENT_T)() + error = self.lib.afc_client_new(byref(self.device), + self.lockdown, + byref(afc_client_t)) & 0xFFFF + + if error: + error_description = self.LIB_ERROR_TEMPLATE.format( + cls=self.__class__.__name__, + func=sys._getframe().f_code.co_name, + desc=self._afc_error(error)) + raise libiMobileDeviceException(error_description) + else: + if afc_client_t.contents: + return afc_client_t.contents + else: + error_description = self.LIB_ERROR_TEMPLATE.format( + cls=self.__class__.__name__, + func=sys._getframe().f_code.co_name, + desc="AFC not initialized") + raise libiMobileDeviceException(error_description) + + def _afc_client_new_from_house_arrest_client(self): + ''' + Creates an AFC client using the given house_arrest client's connection, + allowing file access to a specific application directory requested by functions + like house_arrest_request_vendor_documents(). + (NB: this header is declared in house_arrest.h) + + Args: + house_arrest: (HOUSE_ARREST_CLIENT_T) The house_arrest client to use + afc_client: (AFC_CLIENT_T *) Pointer that will be set to a newly allocated + afc_client_t upon successful return + + Return: + error: AFC_E_SUCCESS if the afc client was successfuly created, AFC_E_INVALID_ARG + if client is invalid or was already used to create an afc client, or an + AFC_E_* error code returned by afc_client_new_from_connection() + + NOTE: + After calling this function the house_arrest client will go into an AFC mode that + will only allow calling house_arrest_client_free(). Only call + house_arrest_client_free() if all AFC operations have completed, since it will + close the connection. + ''' + self._log_location() + + self.afc = None + afc_client_t = POINTER(AFC_CLIENT_T)() + error = self.lib.afc_client_new_from_house_arrest_client(byref(self.house_arrest), + byref(afc_client_t)) & 0xFFFF + + if error: + error_description = self.LIB_ERROR_TEMPLATE.format( + cls=self.__class__.__name__, + func=sys._getframe().f_code.co_name, + desc=self._afc_error(error)) + raise libiMobileDeviceException(error_description) + else: + if afc_client_t.contents: + return afc_client_t.contents + else: + error_description = self.LIB_ERROR_TEMPLATE.format( + cls=self.__class__.__name__, + func=sys._getframe().f_code.co_name, + desc="AFC not initialized") + raise libiMobileDeviceException(error_description) + + def _afc_error(self, error): + ''' + Returns an error string based on a numeric error returned by an AFC function call + + Args: + error: (int) + + Result: + (str) describing error + + ''' + e = "UNKNOWN ERROR (%s)" % error + if not error: + e = "Success (0)" + elif error == 2: + e = "Header invalid (2)" + elif error == 3: + e = "No resources (3)" + elif error == 4: + e = "Read error (4)" + elif error == 5: + e = "Write error (5)" + elif error == 6: + e = "Unknown packet type (6)" + elif error == 7: + e = "Invalid arg (7)" + elif error == 8: + e = "Object not found (8)" + elif error == 9: + e = "Object is directory (9)" + elif error == 10: + e = "Permission denied (10)" + elif error == 11: + e = "Service not connected (11)" + elif error == 12: + e = "Operation timeout" + elif error == 13: + e = "Too much data" + elif error == 14: + e = "End of data" + elif error == 15: + e = "Operation not supported" + elif error == 16: + e = "Object exists" + elif error == 17: + e = "Object busy" + elif error == 18: + e = "No space left" + elif error == 19: + e = "Operation would block" + elif error == 20: + e = "IO error" + elif error == 21: + e = "Operation interrupted" + elif error == 22: + e = "Operation in progress" + elif error == 23: + e = "Internal error" + elif error == 30: + e = "MUX error" + elif error == 31: + e = "No memory" + elif error == 32: + e = "Not enough data" + elif error == 33: + e = "Directory not empty" + return e + + def _afc_file_close(self, handle): + ''' + Closes a file on the device + + Args: + client: (AFC_CLIENT_T) The client to close the file with + handle: (uint64) File handle of a previously opened file + + Result: + File closed + + ''' + self._log_location(handle.value) + + error = self.lib.afc_file_close(byref(self.afc), + handle) & 0xFFFF + if error and self.verbose: + self.log(" ERROR: %s" % self._afc_error(error)) + + def _afc_file_open(self, filename, mode='r'): + ''' + Opens a file on the device + + Args: + (wrapper convenience) + 'r' reading (default) + 'w' writing, replacing + 'b' binary + + (libiMobileDevice) + client: (AFC_CLIENT_T) The client to use to open the file + filename: (const char *) The file to open (must be a fully-qualified path) + file_mode: (AFC_FILE_MODE_T) The mode to use to open the file. Can be AFC_FILE_READ + or AFC_FILE_WRITE; the former lets you read and write, however, the + second one will create the file, destroying anything previously there. + handle: (uint64_t *) Pointer to a uint64_t that will hold the handle of the file + + Result: + error: (afc_error_t) AFC_E_SUCCESS (0) on success or AFC_E_* error value + + ''' + self._log_location("'%s', mode='%s'" % (filename, mode)) + + handle = c_ulonglong(0) + + if 'r' in mode: + error = self.lib.afc_file_open(byref(self.afc), + str(filename), + self.AFC_FOPEN_RDONLY, + byref(handle)) & 0xFFFF + elif 'w' in mode: + error = self.lib.afc_file_open(byref(self.afc), + str(filename), + self.AFC_FOPEN_WRONLY, + byref(handle)) & 0xFFFF + + if error: + if self.verbose: + self.log(" ERROR: %s" % self._afc_error(error)) + return None + else: + return handle + + def _afc_file_read(self, handle, size, mode): + ''' + Attempts to read the given number of bytes from the given file + + Args: + (wrapper) + mode: ['r'|'rb'] + + (libiMobileDevice) + client: (AFC_CLIENT_T) The relevant AFC client + handle: (uint64_t) File handle of a previously opened file + data: (char *) Pointer to the memory region to store the read data + length: (uint32_t) The number of bytes to read + bytes_read: (uint32_t *) The number of bytes actually read + + Result: + error (afc_error_t) AFC_E_SUCCESS (0) on success or AFC_E_* error value + + ''' + self._log_location("%s, size=%d, mode='%s'" % (handle.value, size, mode)) + + bytes_read = c_uint(0) + + if 'b' in mode: + data = bytearray(size) + datatype = c_char * size + error = self.lib.afc_file_read(byref(self.afc), + handle, + byref(datatype.from_buffer(data)), + size, + byref(bytes_read)) & 0xFFFF + if error: + if self.verbose: + self.log(" ERROR: %s" % self._afc_error(error)) + return data + else: + data = create_string_buffer(size) + error = self.lib.afc_file_read(byref(self.afc), handle, byref(data), size, byref(bytes_read)) + if error: + if self.verbose: + self.log(" ERROR: %s" % self._afc_error(error)) + return data.value + + def _afc_file_write(self, handle, content, mode='w'): + ''' + Writes a given number of bytes to a file + + Args: + (wrapper) + mode: ['w'|'wb'] + + (libiMobileDevice) + client: (AFC_CLIENT_T) The client to use to write to the file + handle: (uint64_t) File handle of previously opened file + data: (const char *) The data to write to the file + length: (uint32_t) How much data to write + bytes_written: (uint32_t *) The number of bytes actually written to the file + + Result: + error: (afc_error_t) AFC_E_SUCCESS (0) on success or AFC_E_* error value + + ''' + self._log_location("handle=%d, mode='%s'" % (handle.value, mode)) + + bytes_written = c_uint(0) + + if 'b' in mode: + # Content already contained in a bytearray() + data = content + datatype = c_char * len(content) + else: + data = bytearray(content, 'utf-8') + datatype = c_char * len(content) + + error = self.lib.afc_file_write(byref(self.afc), + handle, + byref(datatype.from_buffer(data)), + len(content), + byref(bytes_written)) & 0xFFFF + if error: + if self.verbose: + self.log(" ERROR: %s" % self._afc_error(error)) + return False + return True + + def _afc_get_device_info(self): + ''' + Get device information for a connected client + + Args: + client: (AFC_CLIENT_T) The client to get the device info for + infos: (char ***) A char ** list of parameters as returned by AFC or + None if there was an error + + Result: + error: (afc_error_t) AFC_E_SUCCESS (0) on success or AFC_E_* error value + device_info: + {'Model': 'iPad2,5', + 'FSTotalBytes': '14738952192', + 'FSFreeBytes': '11264917504', + 'FSBlockSize': '4096'} + + ''' + self._log_location() + + device_info = {} + if self.afc is not None: + info_raw_p = c_char_p + info_raw = POINTER(info_raw_p)() + + error = self.lib.afc_get_device_info(byref(self.afc), + byref(info_raw)) & 0xFFFF + if not error: + num_items = 0 + item_list = [] + while info_raw[num_items]: + item_list.append(info_raw[num_items]) + num_items += 1 + for i in range(0, len(item_list), 2): + device_info[item_list[i]] = item_list[i+1] + if self.verbose: + for key in device_info.keys(): + self.log("{0:>16}: {1}".format(key, device_info[key])) + else: + if self.verbose: + self.log(" ERROR: %s" % self._afc_error(error)) + else: + if self.verbose: + self.log(" ERROR: AFC not initialized, can't get device info") + return device_info + + def _afc_get_file_info(self, path): + ''' + Gets information about a specific file + + Args: + client: (AFC_CLIENT_T) The client to use to get the information of a file + path: (const char *) The fully qualified path to the file + infolist: (char ***) Pointer to a buffer that will be filled with a NULL-terminated + list of strings with the file information. Set to NULL before calling + this function + + Result: + error: (afc_error_t) AFC_E_SUCCESS (0) on success or AFC_E_* error value + file_stats: + {'st_size': '12345', + 'st_blocks': '123', + 'st_nlink': '1', + 'st_ifmt': ['S_IFREG'|'S_IFDIR'], + 'st_mtime': xxx.yyy, + 'st_birthtime': xxx.yyy} + + ''' + self._log_location("'%s'" % path) + + infolist_p = c_char * 1024 + infolist = POINTER(POINTER(infolist_p))() + error = self.lib.afc_get_file_info(byref(self.afc), + str(path), + byref(infolist)) & 0xFFFF + file_stats = {} + if error: + if self.verbose: + self.log(" ERROR: %s" % self._afc_error(error)) + else: + num_items = 0 + item_list = [] + while infolist[num_items]: + item_list.append(infolist[num_items]) + num_items += 1 + for i in range(0, len(item_list), 2): + if item_list[i].contents.value in ['st_mtime', 'st_birthtime']: + integer = item_list[i+1].contents.value[:10] + decimal = item_list[i+1].contents.value[10:] + value = float("%s.%s" % (integer, decimal)) + else: + value = item_list[i+1].contents.value + file_stats[item_list[i].contents.value] = value + + if False and self.verbose: + for key in file_stats.keys(): + self.log(" %s: %s" % (key, file_stats[key])) + return file_stats + + def _afc_read_directory(self, directory=''): + ''' + Gets a directory listing of the directory requested + + Args: + client: (AFC_CLIENT_T) The client to get a directory listing from + dir: (const char *) The directory to list (a fully-qualified path) + list: (char ***) A char list of files in that directory, terminated by + an empty string. NULL if there was an error. + + Result: + error: AFC_E_SUCCESS on success or an AFC_E_* error value + file_stats: + {'': {} ...} + + ''' + self._log_location("'%s'" % directory) + + file_stats = {} + dirs_p = c_char_p + dirs = POINTER(dirs_p)() + error = self.lib.afc_read_directory(byref(self.afc), + str(directory), + byref(dirs)) & 0xFFFF + if error: + if self.verbose: + self.log(" ERROR: %s" % self._afc_error(error)) + else: + num_dirs = 0 + dir_list = [] + while dirs[num_dirs]: + dir_list.append(dirs[num_dirs]) + num_dirs += 1 + + # Build a dict of the file_info stats + for i, this_item in enumerate(dir_list): + if this_item.startswith('.'): + continue + path = '/'.join([directory, this_item]) + file_stats[os.path.basename(path)] = self._afc_get_file_info(path) + self.current_dir = directory + return file_stats + + # ~~~ house_arrest functions ~~~ + # http://www.libimobiledevice.org/docs/html/include_2libimobiledevice_2house__arrest_8h.html + def _house_arrest_client_free(self): + ''' + Disconnects a house_arrest client from the device, frees up the + house_arrest client data + + Args: + client: (HOUSE_ARREST_CLIENT_T) The house_arrest client to disconnect and free + + Return: + error: HOUSE_ARREST_E_SUCCESS on success, + HOUSE_ARREST_E_INVALID_ARG when client is NULL, + HOUSE_ARREST_E_* error code otherwise + + NOTE: + After using afc_client_new_from_house_arrest_client(), make sure you call + afc_client_free() before calling this function to ensure a proper cleanup. Do + not call this function if you still need to perform AFC operations since it + will close the connection. + + ''' + + self._log_location() + + error = self.lib.house_arrest_client_free(byref(self.house_arrest)) & 0xFFFF + if error: + if self.verbose: + self.log(" ERROR: %s" % self._house_arrest_error(error)) + + def _house_arrest_client_new(self): + ''' + Connects to the house_arrest client on the specified device + + Args: + device: (IDEVICE_T) The device to connect to + port: (uint16_t) Destination port (usually given by lockdownd_start_service) + client: (HOUSE_ARREST_CLIENT_T *) Pointer that will point to a newly allocated + house_arrest_client_t upon successful return + + Return: + HOUSE_ARREST_E_SUCCESS on success + HOUSE_ARREST_E_INVALID_ARG when client is NULL + HOUSE_ARREST_E_* error code otherwise + + ''' + self._log_location() + + house_arrest_client_t = POINTER(HOUSE_ARREST_CLIENT_T)() + error = self.lib.house_arrest_client_new(byref(self.device), + self.lockdown, + byref(house_arrest_client_t)) & 0xFFFF + if error: + error_description = self.LIB_ERROR_TEMPLATE.format( + cls=self.__class__.__name__, + func=sys._getframe().f_code.co_name, + desc=self._house_arrest_error(error)) + raise libiMobileDeviceException(error_description) + else: + if not house_arrest_client_t: + if self.verbose: + self.log(" Could not start document sharing service") + self.log(" 1: Bad command") + self.log(" 2: Bad device") + self.log(" 3. Connection refused") + self.log(" 6. Bad version") + return None + else: + return house_arrest_client_t.contents + + def _house_arrest_error(self, error): + e = "UNKNOWN ERROR" + if not error: + e = "Success (0)" + elif error == -1: + e = "Invalid arg (-1)" + elif error == -2: + e = "plist error (-2)" + elif error == -3: + e = "connection failed (-3)" + elif error == -4: + e = "invalid mode (-4)" + + return e + + def _house_arrest_get_result(self): + ''' + Retrieves the result of a previously sent house_arrest_* request + + Args: + client: (HOUSE_ARREST_CLIENT_T) The house_arrest client to use + dict: (plist_t *) Pointer that will be set to a plist containing the result + of the last performed operation. It holds a key 'Status' with the + value 'Complete' on success, or 'a key 'Error' with an error + description as value. The caller is responsible for freeing the + returned plist. + + Return: + error: HOUSE_ARREST_E_SUCCESS if a result plist was retrieved, + HOUSE_ARREST_E_INVALID_ARG if client is invalid, + HOUSE_ARREST_E_INVALID_MODE if the client is not in the correct mode, or + HOUSE_ARREST_E_CONN_FAILED if a connection error occured. + + ''' + self._log_location() + + plist = c_char_p() + self.lib.house_arrest_get_result(byref(self.house_arrest), + byref(plist)) & 0xFFFF + plist = c_void_p.from_buffer(plist) + + # Convert the plist to xml + xml = POINTER(c_void_p)() + xml_len = c_long(0) + self.plist_lib.plist_to_xml(c_void_p.from_buffer(plist), byref(xml), byref(xml_len)) + result = XmlPropertyListParser().parse(string_at(xml, xml_len.value)) + self.plist_lib.plist_free(plist) + + # To determine success, we need to inspect the returned plist + if hasattr(result, 'Status'): + if self.verbose: + self.log(" STATUS: %s" % result['Status']) + elif hasattr(result, 'Error'): + if self.verbose: + self.log(" ERROR: %s" % result['Error']) + raise libiMobileDeviceException(result['Error']) + + def _house_arrest_send_command(self, command=None, appid=None): + ''' + Send a command to the connected house_arrest service + + Args: + client: (HOUSE_ARREST_CLIENT_T) The house_arrest client to use + command: (const char *) The command to send. Currently, only 'VendContainer' + and 'VendDocuments' are known + appid: (const char *) The application identifier + + Result: + error: HOUSE_ARREST_E_SUCCESS if the command was successfully sent, + HOUSE_ARREST_E_INVALID_ARG if client, command, or appid is invalid, + HOUSE_ARREST_E_INVALID_MODE if the client is not in the correct mode, or + HOUSE_ARREST_E_CONN_FAILED if a connection error occured. + + NOTE: If the function returns HOUSE_ARREST_E_SUCCESS it does not mean that + the command was successful. To check for success or failure you need + to call house_arrest_get_result(). + + ''' + self._log_location("command='%s' appid='%s'" % (command, appid)) + + commands = ['VendContainer', 'VendDocuments'] + + if command not in commands: + if self.verbose: + self.log(" ERROR: available commands: %s" % ', '.join(commands)) + return + + _command = create_string_buffer(command) + _appid = create_string_buffer(appid) + + error = self.lib.house_arrest_send_command(byref(self.house_arrest), + _command, + _appid) & 0xFFFF + if error: + error_description = self.LIB_ERROR_TEMPLATE.format( + cls=self.__class__.__name__, + func=sys._getframe().f_code.co_name, + desc=self._house_arrest_error(error)) + raise libiMobileDeviceException(error_description) + + # ~~~ idevice functions ~~~ + # http://www.libimobiledevice.org/docs/html/libimobiledevice_8h.html + def _idevice_error(self, error): + e = "UNKNOWN ERROR" + if not error: + e = "Success" + elif error == -1: + e = "INVALID_ARG" + elif error == -2: + e = "UNKNOWN_ERROR" + elif error == -3: + e = "NO_DEVICE" + elif error == -4: + e = "NOT_ENOUGH_DATA" + elif error == -5: + e = "BAD_HEADER" + elif error == -6: + e = "SSL_ERROR" + return e + + def _idevice_free(self): + ''' + Cleans up an idevice structure, then frees the structure itself. + + Args: + device: (IDEVICE_T) idevice to free + + Return: + error: IDEVICE_E_SUCCESS if ok, otherwise an error code. + ''' + self._log_location() + + error = self.lib.idevice_free(byref(self.device)) & 0xFFFF + + if error: + if self.verbose: + self.log(" ERROR: %s" % self._idevice_error(error)) + + def _idevice_new(self): + ''' + Creates an IDEVICE_T structure for the device specified by udid, if the + device is available. + + Args: + device: (IDEVICE_T) On successful return, a pointer to a populated IDEVICE_T structure. + udid: (const char *) The UDID to match. If NULL, use connected device. + + Return: + error: IDEVICE_E_SUCCESS if ok, otherwise an error code + + ''' + self._log_location() + + idevice_t = POINTER(IDEVICE_T)() + error = self.lib.idevice_new(byref(idevice_t), + c_void_p()) & 0xFFFF + + if error: + error_description = self.LIB_ERROR_TEMPLATE.format( + cls=self.__class__.__name__, + func=sys._getframe().f_code.co_name, + desc=self._idevice_error(error)) + raise libiMobileDeviceException(error_description) + else: + if self.verbose: + if idevice_t.contents.conn_type == 1: + self.log(" conn_type: CONNECTION_USBMUXD") + else: + self.log(" conn_type: Unknown (%d)" % idevice_t.contents.conn_type) + self.log(" udid: %s" % idevice_t.contents.udid) + return idevice_t.contents + + def _idevice_set_debug_level(self, debug): + ''' + Sets the level of debugging + + Args: + level (int) Set to 0 for no debugging, 1 for debugging + + ''' + self._log_location(debug) + self.lib.idevice_set_debug_level(debug) + + # ~~~ instproxy functions ~~~ + # http://www.libimobiledevice.org/docs/html/include_2libimobiledevice_2installation__proxy_8h.html + def _instproxy_browse(self, applist=[]): + ''' + Fetch the app list + ''' + self._log_location(applist) + + apps = c_void_p() + error = self.lib.instproxy_browse(byref(self.instproxy), + self.client_options, + byref(apps)) & 0xFFFF + + if error: + error_description = self.LIB_ERROR_TEMPLATE.format( + cls=self.__class__.__name__, + func=sys._getframe().f_code.co_name, + desc=self._instproxy_error(error)) + raise libiMobileDeviceException(error_description) + else: + # Get the number of apps + #app_count = self.lib.plist_array_get_size(apps) + #self.log(" app_count: %d" % app_count) + + # Convert the app plist to xml + xml = POINTER(c_void_p)() + xml_len = c_long(0) + self.plist_lib.plist_to_xml(c_void_p.from_buffer(apps), byref(xml), byref(xml_len)) + app_list = XmlPropertyListParser().parse(string_at(xml, xml_len.value)) + installed_apps = {} + for app in app_list: + if not applist: + try: + installed_apps[app['CFBundleName']] = {'app_id': app['CFBundleIdentifier'], 'app_version': app['CFBundleVersion']} + except: + installed_apps[app['CFBundleDisplayName']] = {'app_id': app['CFBundleDisplayName'], 'app_version': app['CFBundleDisplayName']} + else: + if 'CFBundleName' in app: + if app['CFBundleName'] in applist: + installed_apps[app['CFBundleName']] = {'app_id': app['CFBundleIdentifier'], 'app_version': app['CFBundleVersion']} + if len(installed_apps) == len(app_list): + break + elif 'CFBundleDisplayName' in app: + if app['CFBundleDisplayName'] in applist: + installed_apps[app['CFBundleDisplayName']] = {'app_id': app['CFBundleIdentifier'], 'app_version': app['CFBundleVersion']} + if len(installed_apps) == len(app_list): + break + else: + self.log(" unable to find app name") + for key in sorted(app.keys()): + print(" %s \t %s" % (key, app[key])) + continue + + if self.verbose: + for app in sorted(installed_apps, key=lambda s: s.lower()): + attrs = {'app_name': app, 'app_id': installed_apps[app]['app_id'], 'app_version': installed_apps[app]['app_version']} + self.log(" {app_name:<30} {app_id:<40} {app_version}".format(**attrs)) + + self.plist_lib.plist_free(apps) + return installed_apps + + def _instproxy_client_new(self): + ''' + Create an instproxy_client + ''' + self._log_location() + + instproxy_client_t = POINTER(INSTPROXY_CLIENT_T)() + error = self.lib.instproxy_client_new(byref(self.device), + self.lockdown, + byref(instproxy_client_t)) & 0xFFFF + if error: + error_description = self.LIB_ERROR_TEMPLATE.format( + cls=self.__class__.__name__, + func=sys._getframe().f_code.co_name, + desc=self._instproxy_error(error)) + raise libiMobileDeviceException(error_description) + else: + return instproxy_client_t.contents + + def _instproxy_client_free(self): + ''' + ''' + self._log_location() + + error = self.lib.instproxy_client_free(byref(self.instproxy)) & 0xFFFF + if error: + error_description = self.LIB_ERROR_TEMPLATE.format( + cls=self.__class__.__name__, + func=sys._getframe().f_code.co_name, + desc=self._instproxy_error(error)) + raise libiMobileDeviceException(error_description) + + def _instproxy_client_options_add(self, app_type, domain): + ''' + Specify the type of apps we want to browse + ''' + self._log_location("'%s', '%s'" % (app_type, domain)) + + self.lib.instproxy_client_options_add(self.client_options, + app_type, domain, None) + + def _instproxy_client_options_free(self): + ''' + ''' + self._log_location() + self.lib.instproxy_client_options_free(self.client_options) + + def _instproxy_client_options_new(self): + ''' + Create a client options plist + ''' + self._log_location() + + self.lib.instproxy_client_options_new.restype = c_char * 8 + client_options = self.lib.instproxy_client_options_new() + client_options = c_void_p.from_buffer(client_options) + return client_options + + def _instproxy_error(self, error): + ''' + Return a string version of the error code + ''' + e = "UNKNOWN ERROR" + if not error: + e = "Success" + elif error == -1: + e = "Invalid arg (-1)" + elif error == -2: + e = "Plist error (-2)" + elif error == -3: + e = "Connection failed (-3)" + elif error == -4: + e = "Operation in progress (-4)" + elif error == -5: + e = "Operation failed (-5)" + return e + + # ~~~ lockdown functions ~~~ + # http://www.libimobiledevice.org/docs/html/include_2libimobiledevice_2lockdown_8h.html + def _lockdown_client_free(self): + ''' + Close the lockdownd client session if one is running, free up the lockdown_client struct + + Args: + client: (LOCKDOWN_CLIENT_T) The lockdownd client to free + + Return: + error: LOCKDOWN_E_SUCCESS on success, NP_E_INVALID_ARG when client is NULL + + ''' + self._log_location() + + error = self.lib.lockdownd_client_free(byref(self.control)) & 0xFFFF + if error: + error_description = self.LIB_ERROR_TEMPLATE.format( + cls=self.__class__.__name__, + func=sys._getframe().f_code.co_name, + desc=self._lockdown_error(error)) + raise libiMobileDeviceException(error_description) + + self.control = None + + def _lockdown_client_new_with_handshake(self): + ''' + Create a new lockdownd client for the device, starts initial handshake. + + Args: + device: (IDEVICE_T) The device to create a lockdownd client for + client: (LOCKDOWN_CLIENT_D *) The pointer to the location of the new lockdownd client + label: (const char *) The label to use for communication, usually the program name. + Pass NULL to disable sending the label in requests to lockdownd. + + Return: + error: LOCKDOWN_E_SUCCESS on success, + NP_E_INVALID_ARG when client is NULL, + LOCKDOWN_E_INVALID_CONF if configuration data is wrong + locked_down: [True|False] + + NOTE: + The device disconnects automatically if the lockdown connection idles for more + than 10 seconds. Make sure to call lockdownd_client_free() as soon as the + connection is no longer needed. + + ''' + self._log_location() + + lockdownd_client_t = POINTER(LOCKDOWND_CLIENT_T)() + SERVICE_NAME = create_string_buffer('calibre') + error = self.lib.lockdownd_client_new_with_handshake(byref(self.device), + byref(lockdownd_client_t), + SERVICE_NAME) & 0xFFFF + + if error: + error_description = self.LIB_ERROR_TEMPLATE.format( + cls=self.__class__.__name__, + func=sys._getframe().f_code.co_name, + desc=self._lockdown_error(error)) + raise libiMobileDeviceException(error_description) + else: + return lockdownd_client_t.contents + + def _lockdown_error(self, error): + e = "UNKNOWN ERROR" + if not error: + e = "Success" + elif error == -1: + e = "INVALID_ARG" + elif error == -2: + e = "INVALID_CONF" + elif error == -3: + e = "PLIST_ERROR" + elif error == -4: + e = "PAIRING_FAILED" + elif error == -5: + e = "SSL_ERROR" + elif error == -6: + e = "DICT_ERROR" + elif error == -7: + e = "START_SERVICE_FAILED" + elif error == -8: + e = "NOT_ENOUGH_DATA" + elif error == -9: + e = "SET_VALUE_PROHIBITED" + elif error == -10: + e = "GET_VALUE_PROHIBITED" + elif error == -11: + e = "REMOVE_VALUE_PROHIBITED" + elif error == -12: + e = "MUX_ERROR" + elif error == -13: + e = "ACTIVATION_FAILED" + elif error == -14: + e = "PASSWORD_PROTECTED" + elif error == -15: + e = "NO_RUNNING_SESSION" + elif error == -16: + e = "INVALID_HOST_ID" + elif error == -17: + e = "INVALID_SERVICE" + elif error == -18: + e = "INVALID_ACTIVATION_RECORD" + elif error == -256: + e = "UNKNOWN_ERROR" + return e + + def _lockdown_get_device_name(self): + ''' + Retrieves the name of the device as set by user + + Args: + client: (LOCKDOWND_CLIENT_T) An initialized lockdownd client + device_name: (char **) Holds the name of the device. + + Return: + error: LOCKDOWN_E_SUCCESS on success + device_name: Name of iDevice + + ''' + self._log_location() + + device_name_b = c_char * 32 + device_name_p = POINTER(device_name_b)() + device_name = None + error = self.lib.lockdownd_get_device_name(byref(self.control), + byref(device_name_p)) & 0xFFFF + if error: + error_description = self.LIB_ERROR_TEMPLATE.format( + cls=self.__class__.__name__, + func=sys._getframe().f_code.co_name, + desc=self._lockdown_error(error)) + raise libiMobileDeviceException(error_description) + else: + device_name = device_name_p.contents.value + if self.verbose: + self.log(" device_name: %s" % device_name) + return device_name + + def _lockdown_get_value(self): + ''' + Retrieves a preferences plist using an optional domain and/or key name. + + Args: + client: (LOCKDOWND_CLIENT_T) An initialized lockdown client + domain: (const char *) The domain to query on or NULL for global domain + key: (const char *) The key name to request or NULL to query for all keys + value: (PLIST_T *) A plist node representing the result value code + + Return: + error: LOCKDOWN_E_SUCCESS on success, + NP_E_INVALID_ARG when client is NULL + ''' + self._log_location() + + preferences = c_char_p() + profiles_preferences = ['SerialNumber', 'ModelNumber', 'DeviceColor', 'ProductType', + 'TimeZone', 'DeviceName', 'UniqueDeviceID', 'TimeZoneOffsetFromUTC', + 'DeviceClass', 'HardwareModel', 'TimeIntervalSince1970', + 'FirmwareVersion', 'PasswordProtected', 'ProductVersion'] + preferences_dict = {} + + error = self.lib.lockdownd_get_value(byref(self.control), + None, + None, + byref(preferences)) & 0xFFFF + if error: + error_description = self.LIB_ERROR_TEMPLATE.format( + cls=self.__class__.__name__, + func=sys._getframe().f_code.co_name, + desc=self._lockdown_error(error)) + raise libiMobileDeviceException(error_description) + else: + xml = POINTER(c_char_p)() + xml_len = c_uint(0) + self.plist_lib.plist_to_xml(c_char_p.from_buffer(preferences), byref(xml), byref(xml_len)) + preferences_list = XmlPropertyListParser().parse(string_at(xml, xml_len.value)) + for pref in sorted(profiles_preferences): + #self.log(" {0:21}: {1}".format(pref, preferences_list[pref])) + preferences_dict[pref] = preferences_list[pref] + + self.plist_lib.plist_free(preferences) + return preferences_dict + + def _lockdown_goodbye(self): + ''' + Sends a Goodbye request lockdownd, signaling the end of communication + + Args: + client: (LOCKDOWND_CLIENT_T) The lockdown client + + Return: + error: LOCKDOWN_E_SUCCESS on success, + LOCKDOWN_E_INVALID_ARG when client is NULL, + LOCKDOWN_E_PLIST_ERROR if the device did not acknowledge the request + + ''' + self._log_location() + + if self.control: + error = self.lib.lockdownd_goodbye(byref(self.control)) & 0xFFFF + if self.verbose: + self.log(" ERROR: %s" % self.error_lockdown(error)) + else: + if self.verbose: + self.log(" connection already closed") + + def _lockdown_start_service(self, service_name): + ''' + Request to start service + + Args: + client: (LOCKDOWND_CLIENT_T) The lockdownd client + service: (const char *) The name of the service to start + port: (unit16_t *) The port number the service was started on + + Return: + error: LOCKDOWN_E_SUCCESS on success, + NP_E_INVALID_ARG if a parameter is NULL, + LOCKDOWN_E_INVALID_SERVICE if the requested service is not known by the device, + LOCKDOWN_E_START_SERVICE_FAILED if the service could not because started by the device + + ''' + self._log_location(service_name) + + SERVICE_NAME = create_string_buffer(service_name) + self.lockdown = POINTER(LOCKDOWND_SERVICE_DESCRIPTOR)() + error = self.lib.lockdownd_start_service(byref(self.control), + SERVICE_NAME, + byref(self.lockdown)) & 0xFFFF + if error: + error_description = self.LIB_ERROR_TEMPLATE.format( + cls=self.__class__.__name__, + func=sys._getframe().f_code.co_name, + desc=self._lockdown_error(error)) + raise libiMobileDeviceException(error_description) + + # ~~~ logging ~~~ + def _log_location(self, *args): + ''' + ''' + if not self.verbose: + return + + arg1 = arg2 = '' + + if len(args) > 0: + arg1 = args[0] + if len(args) > 1: + arg2 = args[1] + + self.log(self.LOCATION_TEMPLATE.format(cls=self.__class__.__name__, + func=sys._getframe(1).f_code.co_name, arg1=arg1, arg2=arg2)) diff --git a/src/calibre/devices/idevice/parse_xml.py b/src/calibre/devices/idevice/parse_xml.py new file mode 100755 index 0000000000..8da68756e6 --- /dev/null +++ b/src/calibre/devices/idevice/parse_xml.py @@ -0,0 +1,297 @@ +#!/usr/bin/env python +""" +https://github.com/ishikawa/python-plist-parser/blob/master/plist_parser.py + +A `Property Lists`_ is a data representation used in Apple's Mac OS X as +a convenient way to store standard object types, such as string, number, +boolean, and container object. + +This file contains a class ``XmlPropertyListParser`` for parse +a property list file and get back a python native data structure. + + :copyright: 2008 by Takanori Ishikawa + :license: MIT (See LICENSE file for more details) + +.. _Property Lists: http://developer.apple.com/documentation/Cocoa/Conceptual/PropertyLists/ +""" + + +class PropertyListParseError(Exception): + """Raised when parsing a property list is failed.""" + pass + + +class XmlPropertyListParser(object): + """ + The ``XmlPropertyListParser`` class provides methods that + convert `Property Lists`_ objects from xml format. + Property list objects include ``string``, ``unicode``, + ``list``, ``dict``, ``datetime``, and ``int`` or ``float``. + + :copyright: 2008 by Takanori Ishikawa + :license: MIT License + + .. _Property List: http://developer.apple.com/documentation/Cocoa/Conceptual/PropertyLists/ + """ + + def _assert(self, test, message): + if not test: + raise PropertyListParseError(message) + + # ------------------------------------------------ + # SAX2: ContentHandler + # ------------------------------------------------ + def setDocumentLocator(self, locator): + pass + + def startPrefixMapping(self, prefix, uri): + pass + + def endPrefixMapping(self, prefix): + pass + + def startElementNS(self, name, qname, attrs): + pass + + def endElementNS(self, name, qname): + pass + + def ignorableWhitespace(self, whitespace): + pass + + def processingInstruction(self, target, data): + pass + + def skippedEntity(self, name): + pass + + def startDocument(self): + self.__stack = [] + self.__plist = self.__key = self.__characters = None + # For reducing runtime type checking, + # the parser caches top level object type. + self.__in_dict = False + + def endDocument(self): + self._assert(self.__plist is not None, "A top level element must be .") + self._assert( + len(self.__stack) is 0, + "multiple objects at top level.") + + def startElement(self, name, attributes): + if name in XmlPropertyListParser.START_CALLBACKS: + XmlPropertyListParser.START_CALLBACKS[name](self, name, attributes) + if name in XmlPropertyListParser.PARSE_CALLBACKS: + self.__characters = [] + + def endElement(self, name): + if name in XmlPropertyListParser.END_CALLBACKS: + XmlPropertyListParser.END_CALLBACKS[name](self, name) + if name in XmlPropertyListParser.PARSE_CALLBACKS: + # Creates character string from buffered characters. + content = ''.join(self.__characters) + # For compatibility with ``xml.etree`` and ``plistlib``, + # convert text string to ascii, if possible + try: + content = content.encode('ascii') + except (UnicodeError, AttributeError): + pass + XmlPropertyListParser.PARSE_CALLBACKS[name](self, name, content) + self.__characters = None + + def characters(self, content): + if self.__characters is not None: + self.__characters.append(content) + + # ------------------------------------------------ + # XmlPropertyListParser private + # ------------------------------------------------ + def _push_value(self, value): + if not self.__stack: + self._assert(self.__plist is None, "Multiple objects at top level") + self.__plist = value + else: + top = self.__stack[-1] + #assert isinstance(top, (dict, list)) + if self.__in_dict: + k = self.__key + if k is None: + raise PropertyListParseError("Missing key for dictionary.") + top[k] = value + self.__key = None + else: + top.append(value) + + def _push_stack(self, value): + self.__stack.append(value) + self.__in_dict = isinstance(value, dict) + + def _pop_stack(self): + self.__stack.pop() + self.__in_dict = self.__stack and isinstance(self.__stack[-1], dict) + + def _start_plist(self, name, attrs): + self._assert(not self.__stack and self.__plist is None, " more than once.") + self._assert(attrs.get('version', '1.0') == '1.0', + "version 1.0 is only supported, but was '%s'." % attrs.get('version')) + + def _start_array(self, name, attrs): + v = list() + self._push_value(v) + self._push_stack(v) + + def _start_dict(self, name, attrs): + v = dict() + self._push_value(v) + self._push_stack(v) + + def _end_array(self, name): + self._pop_stack() + + def _end_dict(self, name): + if self.__key is not None: + raise PropertyListParseError("Missing value for key '%s'" % self.__key) + self._pop_stack() + + def _start_true(self, name, attrs): + self._push_value(True) + + def _start_false(self, name, attrs): + self._push_value(False) + + def _parse_key(self, name, content): + if not self.__in_dict: + print("XmlPropertyListParser() WARNING: ignoring %s ( elements must be contained in element)" % content) + #raise PropertyListParseError(" element '%s' must be in element." % content) + else: + self.__key = content + + def _parse_string(self, name, content): + self._push_value(content) + + def _parse_data(self, name, content): + import base64 + self._push_value(base64.b64decode(content)) + + # http://www.apple.com/DTDs/PropertyList-1.0.dtd says: + # + # Contents should conform to a subset of ISO 8601 + # (in particular, YYYY '-' MM '-' DD 'T' HH ':' MM ':' SS 'Z'. + # Smaller units may be omitted with a loss of precision) + import re + DATETIME_PATTERN = re.compile(r"(?P\d\d\d\d)(?:-(?P\d\d)(?:-(?P\d\d)(?:T(?P\d\d)(?::(?P\d\d)(?::(?P\d\d))?)?)?)?)?Z$") + + def _parse_date(self, name, content): + import datetime + + units = ('year', 'month', 'day', 'hour', 'minute', 'second', ) + pattern = XmlPropertyListParser.DATETIME_PATTERN + match = pattern.match(content) + if not match: + raise PropertyListParseError("Failed to parse datetime '%s'" % content) + + groups, components = match.groupdict(), [] + for key in units: + value = groups[key] + if value is None: + break + components.append(int(value)) + while len(components) < 3: + components.append(1) + + d = datetime.datetime(*components) + self._push_value(d) + + def _parse_real(self, name, content): + self._push_value(float(content)) + + def _parse_integer(self, name, content): + self._push_value(int(content)) + + START_CALLBACKS = { + 'plist': _start_plist, + 'array': _start_array, + 'dict': _start_dict, + 'true': _start_true, + 'false': _start_false, + } + + END_CALLBACKS = { + 'array': _end_array, + 'dict': _end_dict, + } + + PARSE_CALLBACKS = { + 'key': _parse_key, + 'string': _parse_string, + 'data': _parse_data, + 'date': _parse_date, + 'real': _parse_real, + 'integer': _parse_integer, + } + + # ------------------------------------------------ + # XmlPropertyListParser + # ------------------------------------------------ + def _to_stream(self, io_or_string): + if isinstance(io_or_string, basestring): + # Creates a string stream for in-memory contents. + from cStringIO import StringIO + return StringIO(io_or_string) + elif hasattr(io_or_string, 'read') and callable(getattr(io_or_string, 'read')): + return io_or_string + else: + raise TypeError('Can\'t convert %s to file-like-object' % type(io_or_string)) + + def _parse_using_etree(self, xml_input): + from xml.etree.cElementTree import iterparse + + parser = iterparse(self._to_stream(xml_input), events=('start', 'end')) + self.startDocument() + try: + for action, element in parser: + name = element.tag + if action == 'start': + if name in XmlPropertyListParser.START_CALLBACKS: + XmlPropertyListParser.START_CALLBACKS[name](self, element.tag, element.attrib) + elif action == 'end': + if name in XmlPropertyListParser.END_CALLBACKS: + XmlPropertyListParser.END_CALLBACKS[name](self, name) + if name in XmlPropertyListParser.PARSE_CALLBACKS: + XmlPropertyListParser.PARSE_CALLBACKS[name](self, name, element.text or "") + element.clear() + except SyntaxError, e: + raise PropertyListParseError(e) + + self.endDocument() + return self.__plist + + def _parse_using_sax_parser(self, xml_input): + from xml.sax import make_parser, xmlreader, SAXParseException + source = xmlreader.InputSource() + source.setByteStream(self._to_stream(xml_input)) + reader = make_parser() + reader.setContentHandler(self) + try: + reader.parse(source) + except SAXParseException, e: + raise PropertyListParseError(e) + + return self.__plist + + def parse(self, xml_input): + """ + Parse the property list (`.plist`, `.xml, for example) ``xml_input``, + which can be either a string or a file-like object. + + >>> parser = XmlPropertyListParser() + >>> parser.parse(r'' + ... r'Python.py' + ... r'') + {'Python': '.py'} + """ + try: + return self._parse_using_etree(xml_input) + except ImportError: + # No xml.etree.ccElementTree found. + return self._parse_using_sax_parser(xml_input)