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