From 3ca3252b9d66afd25fa84997a674bfe813b9eccf Mon Sep 17 00:00:00 2001 From: GRiker Date: Fri, 29 Mar 2013 11:33:03 -0700 Subject: [PATCH 01/10] Updated flow of control summary and some comments --- src/calibre/devices/apple/driver.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/calibre/devices/apple/driver.py b/src/calibre/devices/apple/driver.py index 5251e701b5..f82899ade2 100644 --- a/src/calibre/devices/apple/driver.py +++ b/src/calibre/devices/apple/driver.py @@ -170,7 +170,6 @@ class ITUNES(DriverBase): Delete: delete_books() remove_books_from_metadata() - use_plugboard_ext() set_plugboard() sync_booklists() card_prefix() @@ -186,7 +185,6 @@ class ITUNES(DriverBase): _add_library_book() _update_iTunes_metadata() add_books_to_metadata() - use_plugboard_ext() set_plugboard() set_progress_reporter() sync_booklists() @@ -1029,7 +1027,7 @@ class ITUNES(DriverBase): 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 + # and a device name of ITUNES if DEBUG: logger().info("%s.set_plugboard()" % self.__class__.__name__) #logger().info(' plugboard: %s' % plugboards) From 907a585584a6131ab8d351691c19834c0869c4bc Mon Sep 17 00:00:00 2001 From: GRiker Date: Fri, 26 Apr 2013 05:25:43 -0600 Subject: [PATCH 02/10] Added libimobiledevice.py glue code --- .../devices/idevice/libimobiledevice.py | 1488 +++++++++++++++++ 1 file changed, 1488 insertions(+) create mode 100644 src/calibre/devices/idevice/libimobiledevice.py diff --git a/src/calibre/devices/idevice/libimobiledevice.py b/src/calibre/devices/idevice/libimobiledevice.py new file mode 100644 index 0000000000..e60e20dfc9 --- /dev/null +++ b/src/calibre/devices/idevice/libimobiledevice.py @@ -0,0 +1,1488 @@ +#!/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 binascii, os, plistlib, sys, time + +from collections import OrderedDict +from ctypes import * +from datetime import datetime + +from calibre.constants import DEBUG, islinux, isosx, iswindows +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_ = [ + ('connection', c_long), + ('afc_packet', c_void_p), + ('file_handle', c_int), + ('lock', c_int), + ('mutex', c_long), + ('own_connection', c_int)] + +class HOUSE_ARREST_CLIENT_T(Structure): + ''' + http://www.libimobiledevice.org/docs/html/structhouse__arrest__client__private.html + ''' + _fields_ = [ + ("parent", c_long), + ("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_ = [ + ("parent", c_long), + ("mutex", c_long), + ("status_updater", c_long)] + +class LOCKDOWND_CLIENT_T(Structure): + ''' + http://www.libimobiledevice.org/docs/html/structlockdownd__client__private.html + ''' + _fields_ = [ + ('parent', c_long), + ('ssl_enabled', c_int), + ('session_id', c_char_p), + ('udid', c_char_p), + ('label', c_char_p)] + + +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 [True|False] or file_info + ''' + self._log_location("'%s'" % path) + return self._afc_get_file_info(path) + + def get_device_info(self): + ''' + Return device profile + ''' + 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)) + return device_list + + 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).keys() + + 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 for reading") + + return data + + 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") + + # ~~~ lib helpers ~~~ + 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)) + 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.port, byref(afc_client_t)) + + 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)) + + 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) + 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)) + elif 'w' in mode: + error = self.lib.afc_file_open(byref(self.afc), str(filename), self.AFC_FOPEN_WRONLY, byref(handle)) + + 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)) + 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)) + 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)) + 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(" %s: %s" % (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)) + 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 + item_type = None + 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)) + 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 + + + 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)) + 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.port, byref(house_arrest_client_t)) + 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: + if self.verbose: + self.log(" parent: %s" % house_arrest_client_t.contents.parent) + self.log(" mode: %s" % house_arrest_client_t.contents.mode) + 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() + error = self.lib.house_arrest_get_result(byref(self.house_arrest), byref(plist)) + plist = c_void_p.from_buffer(plist) + + # Convert the plist to xml + xml = POINTER(c_void_p)() + xml_len = c_long(0) + self.lib.plist_to_xml(c_void_p.from_buffer(plist), byref(xml), byref(xml_len)) + result = plistlib.readPlistFromString(string_at(xml, xml_len.value)) + self.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) + 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) + + + 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)) + 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()) + + 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(" conn_data: %s" % idevice_t.contents.conn_data) + 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) + + + 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)) + + 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.lib.plist_to_xml(c_void_p.from_buffer(apps), byref(xml), byref(xml_len)) + app_list = plistlib.readPlistFromString(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.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.port.value, byref(instproxy_client_t)) + error = self.lib.instproxy_client_new(byref(self.device), self.port, byref(instproxy_client_t)) + 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: + if self.verbose: + self.log(" parent: %s" % instproxy_client_t.contents.parent) + self.log(" mutex: %s" % instproxy_client_t.contents.mutex) + self.log(" status_updater: %s" % instproxy_client_t.contents.status_updater) + return instproxy_client_t.contents + + def _instproxy_client_free(self): + ''' + ''' + self._log_location() + + error = self.lib.instproxy_client_free(byref(self.instproxy)) + 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 + + + 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)) + 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) + + 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: + if self.verbose: + self.log(" parent: %s" % lockdownd_client_t.contents.parent) + self.log(" ssl_enabled: %s" % lockdownd_client_t.contents.ssl_enabled) + self.log(" session_id: %s" % lockdownd_client_t.contents.session_id) + self.log(" udid: %s" % lockdownd_client_t.contents.udid) + self.log(" label: %s" % lockdownd_client_t.contents.label) + 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)) + 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_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)) + 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.port = POINTER(c_uint)() + error = self.lib.lockdownd_start_service(byref(self.control), SERVICE_NAME, byref(self.port)) + 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: + if self.verbose: + self.log(" port: %s" % self.port.contents.value) + + + 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)) + From 76e80cea8f43307aa4420006094e291213b74030 Mon Sep 17 00:00:00 2001 From: GRiker Date: Fri, 26 Apr 2013 11:56:27 -0600 Subject: [PATCH 03/10] Added parse_xml.py to code tree, changed libiMobileDevice to use it instead of plistlib. --- .../devices/idevice/libimobiledevice.py | 9 +- src/calibre/devices/idevice/parse_xml.py | 290 ++++++++++++++++++ 2 files changed, 296 insertions(+), 3 deletions(-) create mode 100755 src/calibre/devices/idevice/parse_xml.py diff --git a/src/calibre/devices/idevice/libimobiledevice.py b/src/calibre/devices/idevice/libimobiledevice.py index e60e20dfc9..86062dad76 100644 --- a/src/calibre/devices/idevice/libimobiledevice.py +++ b/src/calibre/devices/idevice/libimobiledevice.py @@ -9,7 +9,7 @@ __copyright__ = '2013, Gregory Riker' http://www.libimobiledevice.org/docs/html/globals.html ''' -import binascii, os, plistlib, sys, time +import binascii, os, sys, time from collections import OrderedDict from ctypes import * @@ -18,6 +18,9 @@ from datetime import datetime from calibre.constants import DEBUG, islinux, isosx, iswindows from calibre.devices.usbms.driver import debug_print +from calibre_plugins.marvin.parse_xml import XmlPropertyListParser +#from calibre.devices.idevice.parse_xml import XmlPropertyListParser + class libiMobileDeviceException(Exception): def __init__(self, value): self.value = value @@ -999,7 +1002,7 @@ class libiMobileDevice(): xml = POINTER(c_void_p)() xml_len = c_long(0) self.lib.plist_to_xml(c_void_p.from_buffer(plist), byref(xml), byref(xml_len)) - result = plistlib.readPlistFromString(string_at(xml, xml_len.value)) + result = XmlPropertyListParser().parse(string_at(xml, xml_len.value)) self.lib.plist_free(plist) # To determine success, we need to inspect the returned plist @@ -1158,7 +1161,7 @@ class libiMobileDevice(): xml = POINTER(c_void_p)() xml_len = c_long(0) self.lib.plist_to_xml(c_void_p.from_buffer(apps), byref(xml), byref(xml_len)) - app_list = plistlib.readPlistFromString(string_at(xml, xml_len.value)) + app_list = XmlPropertyListParser().parse(string_at(xml, xml_len.value)) installed_apps = {} for app in app_list: if not applist: diff --git a/src/calibre/devices/idevice/parse_xml.py b/src/calibre/devices/idevice/parse_xml.py new file mode 100755 index 0000000000..562ce22025 --- /dev/null +++ b/src/calibre/devices/idevice/parse_xml.py @@ -0,0 +1,290 @@ +#!/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, handler, 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) \ No newline at end of file From bf329f533ad34abaeb8612ac46385f110a6bd704 Mon Sep 17 00:00:00 2001 From: GRiker Date: Sat, 27 Apr 2013 04:52:48 -0600 Subject: [PATCH 04/10] Masked all libiMobileDevice return values with 0xFFFF --- .../devices/idevice/libimobiledevice.py | 193 +++++++++++++----- 1 file changed, 139 insertions(+), 54 deletions(-) diff --git a/src/calibre/devices/idevice/libimobiledevice.py b/src/calibre/devices/idevice/libimobiledevice.py index 86062dad76..07e01836f8 100644 --- a/src/calibre/devices/idevice/libimobiledevice.py +++ b/src/calibre/devices/idevice/libimobiledevice.py @@ -40,20 +40,50 @@ class AFC_CLIENT_T(Structure): http://www.libimobiledevice.org/docs/html/structafc__client__private.html ''' _fields_ = [ - ('connection', c_long), + # 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', c_long), - ('own_connection', 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_ = [ - ("parent", c_long), - ("mode", c_int)] + # 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): ''' @@ -69,21 +99,56 @@ class INSTPROXY_CLIENT_T(Structure): http://www.libimobiledevice.org/docs/html/structinstproxy__client__private.html ''' _fields_ = [ - ("parent", c_long), - ("mutex", c_long), - ("status_updater", c_long)] + # 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_ = [ - ('parent', c_long), + # 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(): ''' @@ -245,6 +310,7 @@ class libiMobileDevice(): index += 1 if self.verbose: self.log(" %s" % repr(device_list)) + #self.lib.idevice_device_list_free() return device_list def get_installed_apps(self, applist): @@ -475,7 +541,7 @@ class libiMobileDevice(): ''' self._log_location() - error = self.lib.afc_client_free(byref(self.afc)) + error = self.lib.afc_client_free(byref(self.afc)) & 0xFFFF if error and self.verbose: self.log(" ERROR: %s" % self.afc_error(error)) @@ -486,7 +552,9 @@ class libiMobileDevice(): self._log_location() self.afc = None afc_client_t = POINTER(AFC_CLIENT_T)() - error = self.lib.afc_client_new(byref(self.device), self.port, byref(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( @@ -531,7 +599,8 @@ class libiMobileDevice(): 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)) + 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( @@ -631,7 +700,8 @@ class libiMobileDevice(): ''' self._log_location(handle.value) - error = self.lib.afc_file_close(byref(self.afc), handle) + error = self.lib.afc_file_close(byref(self.afc), + handle) & 0xFFFF if error and self.verbose: self.log(" ERROR: %s" % self._afc_error(error)) @@ -662,9 +732,15 @@ class libiMobileDevice(): 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)) + 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)) + error = self.lib.afc_file_open(byref(self.afc), + str(filename), + self.AFC_FOPEN_WRONLY, + byref(handle)) & 0xFFFF if error: if self.verbose: @@ -699,7 +775,11 @@ class libiMobileDevice(): 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)) + 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)) @@ -743,7 +823,11 @@ class libiMobileDevice(): 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)) + 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)) @@ -775,7 +859,8 @@ class libiMobileDevice(): 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)) + error = self.lib.afc_get_device_info(byref(self.afc), + byref(info_raw)) & 0xFFFF if not error: num_items = 0 item_list = [] @@ -821,7 +906,9 @@ class libiMobileDevice(): infolist_p = c_char * 1024 infolist = POINTER(POINTER(infolist_p))() - error = self.lib.afc_get_file_info(byref(self.afc), str(path), byref(infolist)) + error = self.lib.afc_get_file_info(byref(self.afc), + str(path), + byref(infolist)) & 0xFFFF file_stats = {} if error: if self.verbose: @@ -868,7 +955,9 @@ class libiMobileDevice(): file_stats = {} dirs_p = c_char_p dirs = POINTER(dirs_p)() - error = self.lib.afc_read_directory(byref(self.afc), str(directory), byref(dirs)) + 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)) @@ -912,7 +1001,7 @@ class libiMobileDevice(): self._log_location() - error = self.lib.house_arrest_client_free(byref(self.house_arrest)) + 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)) @@ -936,7 +1025,9 @@ class libiMobileDevice(): self._log_location() house_arrest_client_t = POINTER(HOUSE_ARREST_CLIENT_T)() - error = self.lib.house_arrest_client_new(byref(self.device), self.port, byref(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__, @@ -953,9 +1044,6 @@ class libiMobileDevice(): self.log(" 6. Bad version") return None else: - if self.verbose: - self.log(" parent: %s" % house_arrest_client_t.contents.parent) - self.log(" mode: %s" % house_arrest_client_t.contents.mode) return house_arrest_client_t.contents def _house_arrest_error(self, error): @@ -995,7 +1083,8 @@ class libiMobileDevice(): self._log_location() plist = c_char_p() - error = self.lib.house_arrest_get_result(byref(self.house_arrest), byref(plist)) + error = 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 @@ -1047,7 +1136,9 @@ class libiMobileDevice(): _command = create_string_buffer(command) _appid = create_string_buffer(appid) - error = self.lib.house_arrest_send_command(byref(self.house_arrest), _command, _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__, @@ -1086,7 +1177,8 @@ class libiMobileDevice(): ''' self._log_location() - error = self.lib.idevice_free(byref(self.device)) + error = self.lib.idevice_free(byref(self.device)) & 0xFFFF + if error: if self.verbose: self.log(" ERROR: %s" % self._idevice_error(error)) @@ -1107,7 +1199,8 @@ class libiMobileDevice(): self._log_location() idevice_t = POINTER(IDEVICE_T)() - error = self.lib.idevice_new(byref(idevice_t), c_void_p()) + error = self.lib.idevice_new(byref(idevice_t), + c_void_p()) & 0xFFFF if error: error_description = self.LIB_ERROR_TEMPLATE.format( @@ -1121,7 +1214,6 @@ class libiMobileDevice(): self.log(" conn_type: CONNECTION_USBMUXD") else: self.log(" conn_type: Unknown (%d)" % idevice_t.contents.conn_type) - self.log(" conn_data: %s" % idevice_t.contents.conn_data) self.log(" udid: %s" % idevice_t.contents.udid) return idevice_t.contents @@ -1144,7 +1236,9 @@ class libiMobileDevice(): self._log_location(applist) apps = c_void_p() - error = self.lib.instproxy_browse(byref(self.instproxy), self.client_options, byref(apps)) + error = self.lib.instproxy_browse(byref(self.instproxy), + self.client_options, + byref(apps)) & 0xFFFF if error: error_description = self.LIB_ERROR_TEMPLATE.format( @@ -1201,8 +1295,9 @@ class libiMobileDevice(): self._log_location() instproxy_client_t = POINTER(INSTPROXY_CLIENT_T)() - #error = self.lib.instproxy_client_new(byref(self.device), self.port.value, byref(instproxy_client_t)) - error = self.lib.instproxy_client_new(byref(self.device), self.port, byref(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__, @@ -1210,10 +1305,6 @@ class libiMobileDevice(): desc=self._instproxy_error(error)) raise libiMobileDeviceException(error_description) else: - if self.verbose: - self.log(" parent: %s" % instproxy_client_t.contents.parent) - self.log(" mutex: %s" % instproxy_client_t.contents.mutex) - self.log(" status_updater: %s" % instproxy_client_t.contents.status_updater) return instproxy_client_t.contents def _instproxy_client_free(self): @@ -1221,7 +1312,7 @@ class libiMobileDevice(): ''' self._log_location() - error = self.lib.instproxy_client_free(byref(self.instproxy)) + error = self.lib.instproxy_client_free(byref(self.instproxy)) & 0xFFFF if error: error_description = self.LIB_ERROR_TEMPLATE.format( cls=self.__class__.__name__, @@ -1288,7 +1379,7 @@ class libiMobileDevice(): ''' self._log_location() - error = self.lib.lockdownd_client_free(byref(self.control)) + error = self.lib.lockdownd_client_free(byref(self.control)) & 0xFFFF if error: error_description = self.LIB_ERROR_TEMPLATE.format( cls=self.__class__.__name__, @@ -1325,8 +1416,8 @@ class libiMobileDevice(): 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) + byref(lockdownd_client_t), + SERVICE_NAME) & 0xFFFF if error: error_description = self.LIB_ERROR_TEMPLATE.format( @@ -1335,12 +1426,6 @@ class libiMobileDevice(): desc=self._lockdown_error(error)) raise libiMobileDeviceException(error_description) else: - if self.verbose: - self.log(" parent: %s" % lockdownd_client_t.contents.parent) - self.log(" ssl_enabled: %s" % lockdownd_client_t.contents.ssl_enabled) - self.log(" session_id: %s" % lockdownd_client_t.contents.session_id) - self.log(" udid: %s" % lockdownd_client_t.contents.udid) - self.log(" label: %s" % lockdownd_client_t.contents.label) return lockdownd_client_t.contents def _lockdown_error(self, error): @@ -1405,7 +1490,8 @@ class libiMobileDevice(): 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)) + 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__, @@ -1434,7 +1520,7 @@ class libiMobileDevice(): self._log_location() if self.control: - error = self.lib.lockdownd_goodbye(byref(self.control)) + error = self.lib.lockdownd_goodbye(byref(self.control)) & 0xFFFF if self.verbose: self.log(" ERROR: %s" % self.error_lockdown(error)) else: @@ -1460,17 +1546,16 @@ class libiMobileDevice(): self._log_location(service_name) SERVICE_NAME = create_string_buffer(service_name) - self.port = POINTER(c_uint)() - error = self.lib.lockdownd_start_service(byref(self.control), SERVICE_NAME, byref(self.port)) + 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) - else: - if self.verbose: - self.log(" port: %s" % self.port.contents.value) def _log_location(self, *args): From 0139667cb6e7a620bc587807538ef62cb8b23def Mon Sep 17 00:00:00 2001 From: GRiker Date: Sat, 27 Apr 2013 05:17:29 -0600 Subject: [PATCH 05/10] Fixed plist_to_xml(), plist_free() references. --- src/calibre/devices/idevice/libimobiledevice.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/calibre/devices/idevice/libimobiledevice.py b/src/calibre/devices/idevice/libimobiledevice.py index 07e01836f8..f293ef89a6 100644 --- a/src/calibre/devices/idevice/libimobiledevice.py +++ b/src/calibre/devices/idevice/libimobiledevice.py @@ -871,7 +871,7 @@ class libiMobileDevice(): device_info[item_list[i]] = item_list[i+1] if self.verbose: for key in device_info.keys(): - self.log(" %s: %s" % (key, device_info[key])) + self.log("{0:>16}: {1}".format(key, device_info[key])) else: if self.verbose: self.log(" ERROR: %s" % self._afc_error(error)) @@ -1090,9 +1090,9 @@ class libiMobileDevice(): # Convert the plist to xml xml = POINTER(c_void_p)() xml_len = c_long(0) - self.lib.plist_to_xml(c_void_p.from_buffer(plist), byref(xml), byref(xml_len)) + 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.lib.plist_free(plist) + self.plist_lib.plist_free(plist) # To determine success, we need to inspect the returned plist if hasattr(result, 'Status'): @@ -1254,7 +1254,7 @@ class libiMobileDevice(): # Convert the app plist to xml xml = POINTER(c_void_p)() xml_len = c_long(0) - self.lib.plist_to_xml(c_void_p.from_buffer(apps), byref(xml), byref(xml_len)) + 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: @@ -1285,7 +1285,7 @@ class libiMobileDevice(): 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.lib.plist_free(apps) + self.plist_lib.plist_free(apps) return installed_apps def _instproxy_client_new(self): From eb9194b7e7fcafece1788ebc36ccd114903b3ed1 Mon Sep 17 00:00:00 2001 From: GRiker Date: Fri, 3 May 2013 05:03:10 -0600 Subject: [PATCH 06/10] Updates to libimobiledevice.py --- src/calibre/devices/apple/driver.py | 446 +++++++++--------- .../devices/idevice/libimobiledevice.py | 347 +++++++++++++- 2 files changed, 572 insertions(+), 221 deletions(-) diff --git a/src/calibre/devices/apple/driver.py b/src/calibre/devices/apple/driver.py index f82899ade2..f9724dc5fd 100644 --- a/src/calibre/devices/apple/driver.py +++ b/src/calibre/devices/apple/driver.py @@ -9,8 +9,7 @@ import cStringIO, ctypes, datetime, os, platform, re, shutil, sys, tempfile, tim 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 @@ -21,8 +20,6 @@ from calibre.utils.config import config_dir, dynamic, prefs from calibre.utils.date import now, parse_date from calibre.utils.zipfile import ZipFile -DEBUG = CALIBRE_DEBUG - def strftime(fmt='%Y/%m/%d %H:%M:%S', dt=None): if not hasattr(dt, 'timetuple'): @@ -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 @@ -306,6 +307,12 @@ class ITUNES(DriverBase): sources = None update_msg = None update_needed = False + verbose = False + + def __init__(self, path): + self.verbose = self.settings().extra_customization[3] + if self.verbose: + logger().info("%s.__init__():" % self.__class__.__name__) @property def cache_dir(self): @@ -327,7 +334,7 @@ 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)) @@ -372,7 +379,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 @@ -387,7 +394,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) @@ -412,7 +419,7 @@ class ITUNES(DriverBase): """ 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") @@ -458,7 +465,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) @@ -499,7 +506,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) @@ -510,7 +517,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 @@ -543,12 +550,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 @@ -556,7 +563,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() @@ -569,15 +576,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 @@ -610,16 +617,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 @@ -632,7 +639,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: @@ -647,15 +654,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 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 @@ -706,7 +713,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: @@ -757,7 +764,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']) @@ -788,7 +795,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 @@ -821,7 +828,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') @@ -831,7 +838,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: @@ -861,7 +868,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] @@ -882,17 +889,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) @@ -902,18 +909,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): @@ -928,10 +935,10 @@ class ITUNES(DriverBase): NB: This will not find books that were added by a different installation of calibre as uuids are different ''' - 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" % (self.cached_books[path]['title'], @@ -977,17 +984,17 @@ class ITUNES(DriverBase): 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: + if 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'], @@ -1008,7 +1015,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) @@ -1020,7 +1027,7 @@ 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 @@ -1028,7 +1035,7 @@ class ITUNES(DriverBase): def set_plugboards(self, plugboards, pb_func): # This method is called with the plugboard that matches the format # and a device name of ITUNES - if DEBUG: + if self.verbose: logger().info("%s.set_plugboard()" % self.__class__.__name__) #logger().info(' plugboard: %s' % plugboards) self.plugboards = plugboards @@ -1046,11 +1053,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 @@ -1073,7 +1080,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: @@ -1111,7 +1118,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: @@ -1128,7 +1135,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, @@ -1175,7 +1182,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, @@ -1225,7 +1232,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 @@ -1239,12 +1246,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) @@ -1262,7 +1269,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 @@ -1272,16 +1279,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() @@ -1292,16 +1299,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: @@ -1324,7 +1331,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 @@ -1336,21 +1343,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() @@ -1360,15 +1367,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: @@ -1392,7 +1399,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': @@ -1428,7 +1435,7 @@ class ITUNES(DriverBase): ''' from PIL import Image as PILImage - if DEBUG: + if self.verbose: logger().info(" %s._cover_to_thumb()" % self.__class__.__name__) thumb = None @@ -1445,7 +1452,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) @@ -1475,7 +1482,7 @@ class ITUNES(DriverBase): try: lb_added.artworks[1].data_.set(cover_data) except: - if DEBUG: + if self.verbose: logger().warning(" iTunes automation interface reported an error" " adding artwork to '%s' in the iTunes Library" % metadata.title) pass @@ -1485,7 +1492,7 @@ class ITUNES(DriverBase): 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 @@ -1507,7 +1514,7 @@ 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 @@ -1519,7 +1526,7 @@ class ITUNES(DriverBase): db_added.AddArtworkFromFile(tc) elif format == 'pdf': - if DEBUG: + if self.verbose: logger().info(" unable to set PDF cover via automation interface") try: @@ -1534,7 +1541,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' @@ -1548,14 +1555,14 @@ class ITUNES(DriverBase): except: pass else: - if DEBUG: + if self.verbose: logger().info(" no cover defined in metadata for '%s'" % metadata.title) return thumb def _create_new_book(self, fpath, metadata, path, db_added, lb_added, thumb, format): ''' ''' - if DEBUG: + if self.verbose: logger().info(" %s._create_new_book()" % self.__class__.__name__) this_book = Book(metadata.title, authors_to_string(metadata.authors)) @@ -1604,7 +1611,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) @@ -1630,7 +1637,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) @@ -1653,7 +1660,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 @@ -1661,7 +1668,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']) @@ -1673,7 +1680,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): @@ -1779,11 +1786,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) @@ -1805,7 +1812,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) @@ -1838,7 +1845,7 @@ class ITUNES(DriverBase): ''' 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'])) @@ -1846,7 +1853,7 @@ class ITUNES(DriverBase): 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: @@ -1856,24 +1863,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 @@ -1882,7 +1889,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')) @@ -1891,15 +1898,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: + if self.verbose: logger().warning(" attempt #%d" % (10 - attempts)) - if DEBUG: + if self.verbose: logger().error(" no hits") return None @@ -1908,7 +1915,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: @@ -1922,11 +1929,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: @@ -1934,47 +1941,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 @@ -1983,7 +1990,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')) @@ -1992,15 +1999,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: + if self.verbose: logger().warning(" attempt #%d" % (10 - attempts)) - if DEBUG: + if self.verbose: logger().error(" search for '%s' yielded no hits" % search['title']) return None @@ -2039,7 +2046,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: @@ -2048,7 +2055,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() @@ -2070,7 +2077,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()) self._dump_hex(data[:32]) thumb_data = None @@ -2084,7 +2091,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() @@ -2109,7 +2116,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 @@ -2143,7 +2150,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 = [] @@ -2156,7 +2163,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 @@ -2165,14 +2172,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: @@ -2188,23 +2195,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: @@ -2227,7 +2234,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 @@ -2236,7 +2243,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 = {} @@ -2248,11 +2255,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: @@ -2260,18 +2267,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 @@ -2284,18 +2291,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: @@ -2314,22 +2321,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' @@ -2343,10 +2350,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 @@ -2392,7 +2399,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') @@ -2412,7 +2419,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: @@ -2425,7 +2432,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) @@ -2458,7 +2465,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 @@ -2475,7 +2482,7 @@ class ITUNES(DriverBase): logger().error(" media_dir: %s" % media_dir) ''' - if DEBUG: + if self.verbose: logger().info(" %s %s" % (__appname__, __version__)) logger().info(" [OSX %s, %s %s (%s), %s driver version %d.%d.%d]" % (platform.mac_ver()[0], @@ -2544,7 +2551,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, @@ -2559,7 +2566,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())) @@ -2569,7 +2576,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(), @@ -2577,14 +2584,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, @@ -2592,13 +2599,13 @@ 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: @@ -2613,7 +2620,7 @@ class ITUNES(DriverBase): 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 @@ -2623,22 +2630,22 @@ 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() @@ -2647,11 +2654,11 @@ class ITUNES(DriverBase): elif iswindows: hit = self._find_device_book(cached_book) 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'])) @@ -2659,14 +2666,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']) @@ -2675,18 +2682,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) @@ -2694,24 +2701,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: @@ -2729,7 +2736,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']) @@ -2738,34 +2745,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): @@ -2777,7 +2784,7 @@ class ITUNES(DriverBase): from calibre.ebooks.metadata.epub import set_metadata from lxml import etree - if DEBUG: + if self.verbose: logger().info(" %s._update_epub_metadata()" % self.__class__.__name__) # Fetch plugboard updates @@ -2805,16 +2812,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) @@ -2845,7 +2852,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: @@ -2853,11 +2860,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) @@ -2870,7 +2877,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: @@ -2878,7 +2885,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() @@ -2893,7 +2900,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'<[^<]*?/?>') @@ -2945,7 +2952,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) @@ -2989,7 +2996,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: @@ -3039,7 +3046,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") @@ -3048,7 +3055,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 @@ -3064,13 +3071,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") @@ -3092,13 +3099,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") @@ -3112,7 +3119,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]): @@ -3126,7 +3133,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 @@ -3140,16 +3147,16 @@ class ITUNES(DriverBase): except: attempts -= 1 time.sleep(delay) - if DEBUG: + if self.verbose: logger().warning(" waiting %.1f seconds for iDevice metadata to become writable (attempt #%d)" % (delay, (10 - attempts))) else: - if DEBUG: + if self.verbose: logger().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: @@ -3173,7 +3180,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: @@ -3197,8 +3204,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 @@ -3247,7 +3255,7 @@ class ITUNES_ASYNC(ITUNES): """ 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") @@ -3296,7 +3304,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: @@ -3338,7 +3346,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)) @@ -3348,7 +3356,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 @@ -3361,7 +3369,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 @@ -3376,7 +3384,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: @@ -3393,7 +3401,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') @@ -3419,13 +3427,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) @@ -3435,18 +3443,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): @@ -3457,7 +3465,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 @@ -3471,7 +3479,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/libimobiledevice.py b/src/calibre/devices/idevice/libimobiledevice.py index f293ef89a6..5256e70215 100644 --- a/src/calibre/devices/idevice/libimobiledevice.py +++ b/src/calibre/devices/idevice/libimobiledevice.py @@ -18,8 +18,284 @@ from datetime import datetime from calibre.constants import DEBUG, islinux, isosx, iswindows from calibre.devices.usbms.driver import debug_print -from calibre_plugins.marvin.parse_xml import XmlPropertyListParser + #from calibre.devices.idevice.parse_xml import XmlPropertyListParser +# *** Temporarily here until added to the code tree + +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, handler, 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) + +# *** End temporary addition class libiMobileDeviceException(Exception): def __init__(self, value): @@ -313,6 +589,28 @@ class libiMobileDevice(): #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 @@ -340,7 +638,7 @@ class libiMobileDevice(): given by path. ''' self._log_location("'%s'" % path) - return self._afc_read_directory(path).keys() + return self._afc_read_directory(path) def load_library(self): if islinux: @@ -1504,6 +1802,51 @@ class libiMobileDevice(): 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 From 782ab4e9383c5273c9a2f3974d252195a4a0eaa7 Mon Sep 17 00:00:00 2001 From: GRiker Date: Fri, 10 May 2013 07:09:30 -0600 Subject: [PATCH 07/10] API updates --- .../devices/idevice/libimobiledevice.py | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/src/calibre/devices/idevice/libimobiledevice.py b/src/calibre/devices/idevice/libimobiledevice.py index 5256e70215..a760f5e345 100644 --- a/src/calibre/devices/idevice/libimobiledevice.py +++ b/src/calibre/devices/idevice/libimobiledevice.py @@ -789,10 +789,40 @@ class libiMobileDevice(): else: if self.verbose: self.log(" could not open file") - raise libiMobileDeviceIOException("could not open file for reading") + 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 From 4e5b54a98a9b961d0877b84292d0e8d18795405d Mon Sep 17 00:00:00 2001 From: GRiker Date: Wed, 15 May 2013 05:12:14 -0600 Subject: [PATCH 08/10] Fixes to apple driver in response to lp:1179737, initial submit of libimobiledevice glue and parse_xml.py --- src/calibre/devices/apple/driver.py | 173 ++++++--- .../devices/idevice/libimobiledevice.py | 336 ++---------------- src/calibre/devices/idevice/parse_xml.py | 15 +- 3 files changed, 174 insertions(+), 350 deletions(-) diff --git a/src/calibre/devices/apple/driver.py b/src/calibre/devices/apple/driver.py index 81810455ad..993a1180a6 100644 --- a/src/calibre/devices/apple/driver.py +++ b/src/calibre/devices/apple/driver.py @@ -7,7 +7,7 @@ __docformat__ = 'restructuredtext en' import cStringIO, ctypes, datetime, os, re, shutil, sys, tempfile, time -from calibre import fit_image, confirm_config_name, strftime as _strftime +from calibre import fit_image, confirm_config_name, osx_version, strftime as _strftime from calibre.constants import ( __appname__, __version__, isosx, iswindows, cache_dir as _cache_dir) from calibre.devices.errors import OpenFeedback, UserFeedback @@ -191,6 +191,12 @@ class ITUNES(DriverBase): 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' @@ -208,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 ...') @@ -310,7 +317,7 @@ class ITUNES(DriverBase): verbose = False def __init__(self, path): - self.verbose = self.settings().extra_customization[3] + self.verbose = self.settings().extra_customization[self.DEBUG_LOGGING] if self.verbose: logger().info("%s.__init__():" % self.__class__.__name__) @@ -341,13 +348,13 @@ class ITUNES(DriverBase): # 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'])) @@ -656,7 +663,7 @@ class ITUNES(DriverBase): attempts -= 1 time.sleep(1.0) if self.verbose: - logger().warning(" waiting for connected iDevice, attempt #%d" % (10 - attempts)) + logger().info(" waiting for connected iDevice, attempt #%d" % (10 - attempts)) else: if self.verbose: logger().info(' found connected iPad in iTunes') @@ -746,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") @@ -940,59 +951,76 @@ class ITUNES(DriverBase): ''' if self.verbose: logger().info("%s.remove_books_from_metadata()" % self.__class__.__name__) + for path in paths: 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 self.verbose: logger().info(" deleting '%s' from cover cache" % (thumb_path)) - zf.delete(thumb_path) - else: - if self.verbose: - 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 @@ -1003,7 +1031,7 @@ class ITUNES(DriverBase): 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) @@ -1045,7 +1073,7 @@ class ITUNES(DriverBase): 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): @@ -1225,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: @@ -1483,15 +1512,47 @@ 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: + 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().warning(" iTunes automation interface reported an error" - " adding artwork to '%s' in the iTunes Library" % metadata.title) - pass + 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) @@ -1504,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 ''' @@ -1524,10 +1586,28 @@ class ITUNES(DriverBase): 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 self.verbose: @@ -1844,7 +1924,7 @@ 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 ''' @@ -1854,7 +1934,7 @@ class ITUNES(DriverBase): 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']: @@ -1908,8 +1988,8 @@ class ITUNES(DriverBase): attempts -= 1 time.sleep(0.5) - if self.verbose: - logger().warning(" attempt #%d" % (10 - attempts)) + if attempts and self.verbose: + logger().info(" attempt #%d" % (10 - attempts)) if self.verbose: logger().error(" no hits") @@ -2010,10 +2090,10 @@ class ITUNES(DriverBase): attempts -= 1 time.sleep(0.5) if self.verbose: - logger().warning(" attempt #%d" % (10 - attempts)) + logger().info(" attempt #%d" % (10 - attempts)) if self.verbose: - logger().error(" search for '%s' yielded no hits" % search['title']) + logger().info(" search for '%s' yielded no hits" % search['title']) return None def _generate_thumbnail(self, book_path, book): @@ -2084,7 +2164,7 @@ class ITUNES(DriverBase): zfw.writestr(thumb_path, thumb_data) except: if self.verbose: - logger().error(" error generating thumb for '%s', caching empty marker" % book.name()) + 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 @@ -2490,6 +2570,7 @@ class ITUNES(DriverBase): ''' if self.verbose: + import platform logger().info(" %s %s" % (__appname__, __version__)) logger().info(" [OSX %s, %s %s (%s), %s driver version %d.%d.%d]" % (platform.mac_ver()[0], @@ -2524,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' @@ -2617,14 +2698,14 @@ class ITUNES(DriverBase): 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 self.verbose: @@ -2659,7 +2740,7 @@ class ITUNES(DriverBase): 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 self.verbose: logger().info(" deleting '%s' from iDevice" % cached_book['title']) @@ -2987,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: @@ -3029,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 @@ -3157,11 +3240,11 @@ class ITUNES(DriverBase): attempts -= 1 time.sleep(delay) if self.verbose: - logger().warning(" waiting %.1f seconds for iDevice metadata to become writable (attempt #%d)" % + logger().info(" waiting %.1f seconds for iDevice metadata to become writable (attempt #%d)" % (delay, (10 - attempts))) else: if self.verbose: - logger().error(" failed to write device metadata") + logger().info(" ERROR: failed to write device metadata") def _xform_metadata_via_plugboard(self, book, format): ''' Transform book metadata from plugboard templates ''' @@ -3172,7 +3255,7 @@ class ITUNES(DriverBase): 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 '')) diff --git a/src/calibre/devices/idevice/libimobiledevice.py b/src/calibre/devices/idevice/libimobiledevice.py index a760f5e345..28a12404b5 100644 --- a/src/calibre/devices/idevice/libimobiledevice.py +++ b/src/calibre/devices/idevice/libimobiledevice.py @@ -9,294 +9,15 @@ __copyright__ = '2013, Gregory Riker' http://www.libimobiledevice.org/docs/html/globals.html ''' -import binascii, os, sys, time +import os, sys from collections import OrderedDict from ctypes import * -from datetime import datetime from calibre.constants import DEBUG, islinux, isosx, iswindows from calibre.devices.usbms.driver import debug_print -#from calibre.devices.idevice.parse_xml import XmlPropertyListParser -# *** Temporarily here until added to the code tree - -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, handler, 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) - -# *** End temporary addition - class libiMobileDeviceException(Exception): def __init__(self, value): self.value = value @@ -304,6 +25,7 @@ class libiMobileDeviceException(Exception): def __str__(self): return repr(self.value) + class libiMobileDeviceIOException(Exception): def __init__(self, value): self.value = value @@ -311,6 +33,7 @@ class libiMobileDeviceIOException(Exception): def __str__(self): return repr(self.value) + class AFC_CLIENT_T(Structure): ''' http://www.libimobiledevice.org/docs/html/structafc__client__private.html @@ -342,6 +65,7 @@ class AFC_CLIENT_T(Structure): # 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 @@ -359,7 +83,8 @@ class HOUSE_ARREST_CLIENT_T(Structure): # (house_arrest.h) ('mode', c_int) - ] + ] + class IDEVICE_T(Structure): ''' @@ -370,6 +95,7 @@ class IDEVICE_T(Structure): ("conn_type", c_int), ("conn_data", c_void_p)] + class INSTPROXY_CLIENT_T(Structure): ''' http://www.libimobiledevice.org/docs/html/structinstproxy__client__private.html @@ -394,6 +120,7 @@ class INSTPROXY_CLIENT_T(Structure): ('status_updater', c_void_p) ] + class LOCKDOWND_CLIENT_T(Structure): ''' http://www.libimobiledevice.org/docs/html/structlockdownd__client__private.html @@ -416,6 +143,7 @@ class LOCKDOWND_CLIENT_T(Structure): ('udid', c_char_p), ('label', c_char_p)] + class LOCKDOWND_SERVICE_DESCRIPTOR(Structure): ''' from libimobiledevice/include/libimobiledevice/lockdown.h @@ -423,7 +151,7 @@ class LOCKDOWND_SERVICE_DESCRIPTOR(Structure): _fields_ = [ ('port', c_uint), ('ssl_enabled', c_ubyte) - ] + ] class libiMobileDevice(): @@ -468,7 +196,6 @@ class libiMobileDevice(): self.load_library() - # ~~~ Public methods ~~~ def connect_idevice(self): ''' @@ -551,14 +278,18 @@ class libiMobileDevice(): ''' Determine if path exists - Returns [True|False] or file_info + Returns file_info or {} ''' self._log_location("'%s'" % path) return self._afc_get_file_info(path) def get_device_info(self): ''' - Return device profile + Return device profile: + {'Model': 'iPad2,5', + 'FSTotalBytes': '14738952192', + 'FSFreeBytes': '11264917504', + 'FSBlockSize': '4096'} ''' self._log_location() self.device_info = self._afc_get_device_info() @@ -709,7 +440,7 @@ class libiMobileDevice(): 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']) + 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() @@ -804,8 +535,8 @@ class libiMobileDevice(): 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)) + str(from_name), + str(to_name)) if error and self.verbose: self.log(" ERROR: %s" % self.afc_error(error)) @@ -855,7 +586,8 @@ class libiMobileDevice(): self.log(" could not open file for writing") raise libiMobileDeviceIOException("could not open file for writing") - # ~~~ lib helpers ~~~ + # ~~~ AFC functions ~~~ + # http://www.libimobiledevice.org/docs/html/include_2libimobiledevice_2afc_8h.html def _afc_client_free(self): ''' Frees up an AFC client. @@ -1148,7 +880,7 @@ class libiMobileDevice(): data = content datatype = c_char * len(content) else: - data = bytearray(content,'utf-8') + data = bytearray(content, 'utf-8') datatype = c_char * len(content) error = self.lib.afc_file_write(byref(self.afc), @@ -1247,7 +979,6 @@ class libiMobileDevice(): while infolist[num_items]: item_list.append(infolist[num_items]) num_items += 1 - item_type = None 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] @@ -1305,7 +1036,8 @@ class libiMobileDevice(): 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 @@ -1411,8 +1143,8 @@ class libiMobileDevice(): self._log_location() plist = c_char_p() - error = self.lib.house_arrest_get_result(byref(self.house_arrest), - byref(plist)) & 0xFFFF + 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 @@ -1474,7 +1206,8 @@ class libiMobileDevice(): 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: @@ -1556,7 +1289,8 @@ class libiMobileDevice(): 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 @@ -1655,7 +1389,7 @@ class libiMobileDevice(): self._log_location("'%s', '%s'" % (app_type, domain)) self.lib.instproxy_client_options_add(self.client_options, - app_type, domain, None) + app_type, domain, None) def _instproxy_client_options_free(self): ''' @@ -1693,7 +1427,8 @@ class libiMobileDevice(): 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 @@ -1930,7 +1665,7 @@ class libiMobileDevice(): desc=self._lockdown_error(error)) raise libiMobileDeviceException(error_description) - + # ~~~ logging ~~~ def _log_location(self, *args): ''' ''' @@ -1945,5 +1680,4 @@ class libiMobileDevice(): 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)) - + func=sys._getframe(1).f_code.co_name, arg1=arg1, arg2=arg2)) \ No newline at end of file diff --git a/src/calibre/devices/idevice/parse_xml.py b/src/calibre/devices/idevice/parse_xml.py index 562ce22025..8da68756e6 100755 --- a/src/calibre/devices/idevice/parse_xml.py +++ b/src/calibre/devices/idevice/parse_xml.py @@ -15,6 +15,7 @@ a property list file and get back a python native data structure. .. _Property Lists: http://developer.apple.com/documentation/Cocoa/Conceptual/PropertyLists/ """ + class PropertyListParseError(Exception): """Raised when parsing a property list is failed.""" pass @@ -42,18 +43,25 @@ class XmlPropertyListParser(object): # ------------------------------------------------ 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 @@ -125,7 +133,7 @@ class XmlPropertyListParser(object): 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')) + "version 1.0 is only supported, but was '%s'." % attrs.get('version')) def _start_array(self, name, attrs): v = list() @@ -259,8 +267,7 @@ class XmlPropertyListParser(object): return self.__plist def _parse_using_sax_parser(self, xml_input): - from xml.sax import make_parser, handler, xmlreader, \ - SAXParseException + from xml.sax import make_parser, xmlreader, SAXParseException source = xmlreader.InputSource() source.setByteStream(self._to_stream(xml_input)) reader = make_parser() @@ -287,4 +294,4 @@ class XmlPropertyListParser(object): return self._parse_using_etree(xml_input) except ImportError: # No xml.etree.ccElementTree found. - return self._parse_using_sax_parser(xml_input) \ No newline at end of file + return self._parse_using_sax_parser(xml_input) From 91b576c6c3deaaffa34dd0f5beca6ca4de74e559 Mon Sep 17 00:00:00 2001 From: GRiker Date: Wed, 15 May 2013 05:19:22 -0600 Subject: [PATCH 09/10] Added import of XmlPropertyListParser --- src/calibre/devices/idevice/libimobiledevice.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/calibre/devices/idevice/libimobiledevice.py b/src/calibre/devices/idevice/libimobiledevice.py index 28a12404b5..d04d0f2b3f 100644 --- a/src/calibre/devices/idevice/libimobiledevice.py +++ b/src/calibre/devices/idevice/libimobiledevice.py @@ -15,6 +15,7 @@ from collections import OrderedDict from ctypes import * from calibre.constants import DEBUG, islinux, isosx, iswindows +from calibre.devices.idevice.parse_xml import XmlPropertyListParser from calibre.devices.usbms.driver import debug_print From 43668bbbd6346bf4c7b3ec632ec9191d6244a639 Mon Sep 17 00:00:00 2001 From: GRiker Date: Wed, 15 May 2013 06:04:11 -0600 Subject: [PATCH 10/10] Added __init__.py to calibre.devices.idevice --- src/calibre/devices/idevice/__init__.py | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 src/calibre/devices/idevice/__init__.py 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