mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Fixes to apple driver in response to lp:1179737, initial submit of libimobiledevice glue and parse_xml.py
This commit is contained in:
parent
a6b14254e2
commit
4e5b54a98a
@ -7,7 +7,7 @@ __docformat__ = 'restructuredtext en'
|
||||
|
||||
import cStringIO, ctypes, datetime, os, re, shutil, sys, tempfile, time
|
||||
|
||||
from calibre import fit_image, confirm_config_name, strftime as _strftime
|
||||
from calibre import fit_image, confirm_config_name, osx_version, strftime as _strftime
|
||||
from calibre.constants import (
|
||||
__appname__, __version__, isosx, iswindows, cache_dir as _cache_dir)
|
||||
from calibre.devices.errors import OpenFeedback, UserFeedback
|
||||
@ -191,6 +191,12 @@ class ITUNES(DriverBase):
|
||||
sync_booklists()
|
||||
card_prefix()
|
||||
free_space()
|
||||
|
||||
self.manual_sync_mode is True when we're talking directly to iBooks through iTunes.
|
||||
Determined in _discover_manual_sync_mode()
|
||||
Special handling in:
|
||||
_add_new_copy()
|
||||
|
||||
'''
|
||||
|
||||
name = 'Apple iTunes interface'
|
||||
@ -208,6 +214,7 @@ class ITUNES(DriverBase):
|
||||
USE_SERIES_AS_CATEGORY = 0
|
||||
CACHE_COVERS = 1
|
||||
USE_ITUNES_STORAGE = 2
|
||||
DEBUG_LOGGING = 3
|
||||
|
||||
OPEN_FEEDBACK_MESSAGE = _(
|
||||
'Apple iDevice detected, launching iTunes, please wait ...')
|
||||
@ -310,7 +317,7 @@ class ITUNES(DriverBase):
|
||||
verbose = False
|
||||
|
||||
def __init__(self, path):
|
||||
self.verbose = self.settings().extra_customization[3]
|
||||
self.verbose = self.settings().extra_customization[self.DEBUG_LOGGING]
|
||||
if self.verbose:
|
||||
logger().info("%s.__init__():" % self.__class__.__name__)
|
||||
|
||||
@ -341,13 +348,13 @@ class ITUNES(DriverBase):
|
||||
|
||||
# Delete any obsolete copies of the book from the booklist
|
||||
if self.update_list:
|
||||
if False:
|
||||
if False and self.verbose:
|
||||
self._dump_booklist(booklists[0], header='before', indent=2)
|
||||
self._dump_update_list(header='before', indent=2)
|
||||
self._dump_cached_books(header='before', indent=2)
|
||||
|
||||
for (j, p_book) in enumerate(self.update_list):
|
||||
if False:
|
||||
if False and self.verbose:
|
||||
if isosx:
|
||||
logger().info(" looking for '%s' by %s uuid:%s" %
|
||||
(p_book['title'], p_book['author'], p_book['uuid']))
|
||||
@ -656,7 +663,7 @@ class ITUNES(DriverBase):
|
||||
attempts -= 1
|
||||
time.sleep(1.0)
|
||||
if self.verbose:
|
||||
logger().warning(" waiting for connected iDevice, attempt #%d" % (10 - attempts))
|
||||
logger().info(" waiting for connected iDevice, attempt #%d" % (10 - attempts))
|
||||
else:
|
||||
if self.verbose:
|
||||
logger().info(' found connected iPad in iTunes')
|
||||
@ -746,9 +753,13 @@ class ITUNES(DriverBase):
|
||||
if not metadata.uuid:
|
||||
metadata.uuid = "unknown"
|
||||
|
||||
if self.verbose:
|
||||
logger().info(" Deleting '%s' from iBooks" % (path))
|
||||
|
||||
if isosx:
|
||||
self._remove_existing_copy(self.cached_books[path], metadata)
|
||||
elif iswindows:
|
||||
import pythoncom, win32com.client
|
||||
try:
|
||||
pythoncom.CoInitialize()
|
||||
self.iTunes = win32com.client.Dispatch("iTunes.Application")
|
||||
@ -940,59 +951,76 @@ class ITUNES(DriverBase):
|
||||
'''
|
||||
if self.verbose:
|
||||
logger().info("%s.remove_books_from_metadata()" % self.__class__.__name__)
|
||||
|
||||
for path in paths:
|
||||
if self.verbose:
|
||||
self._dump_cached_book(self.cached_books[path], indent=2)
|
||||
logger().info(" looking for '%s' by '%s' uuid:%s" %
|
||||
if False and self.verbose:
|
||||
logger().info(" looking for '%s' by '%s' uuid:%s" %
|
||||
(self.cached_books[path]['title'],
|
||||
self.cached_books[path]['author'],
|
||||
repr(self.cached_books[path]['uuid'])))
|
||||
|
||||
# Purge the booklist, self.cached_books, thumb cache
|
||||
for i, bl_book in enumerate(booklists[0]):
|
||||
if False:
|
||||
if False and self.verbose:
|
||||
logger().info(" evaluating '%s' by '%s' uuid:%s" %
|
||||
(bl_book.title, bl_book.author, bl_book.uuid))
|
||||
|
||||
found = False
|
||||
if bl_book.uuid and bl_book.uuid == self.cached_books[path]['uuid']:
|
||||
if True:
|
||||
if True and self.verbose:
|
||||
logger().info(" --matched uuid")
|
||||
booklists[0].pop(i)
|
||||
found = True
|
||||
elif bl_book.title == self.cached_books[path]['title'] and \
|
||||
bl_book.author == self.cached_books[path]['author']:
|
||||
if True:
|
||||
if True and self.verbose:
|
||||
logger().info(" --matched title + author")
|
||||
booklists[0].pop(i)
|
||||
found = True
|
||||
|
||||
if found:
|
||||
# Remove from booklist[0]
|
||||
popped = booklists[0].pop(i)
|
||||
if False and self.verbose:
|
||||
logger().info(" '%s' removed from booklists[0]" % popped.title)
|
||||
|
||||
# Remove from self.cached_books
|
||||
if False and self.verbose:
|
||||
logger().info("path: %s" % path)
|
||||
for cb in self.cached_books:
|
||||
if False and self.verbose:
|
||||
logger().info(" evaluating '%s' by '%s' uuid:%s" %
|
||||
(self.cached_books[cb]['title'],
|
||||
self.cached_books[cb]['author'],
|
||||
self.cached_books[cb]['uuid']))
|
||||
if (self.cached_books[cb]['uuid'] == self.cached_books[path]['uuid'] and
|
||||
self.cached_books[cb]['author'] == self.cached_books[path]['author'] and
|
||||
self.cached_books[cb]['title'] == self.cached_books[path]['title']):
|
||||
self.cached_books.pop(cb)
|
||||
popped = self.cached_books.pop(cb)
|
||||
if False and self.verbose:
|
||||
logger().info(" '%s' removed from self.cached_books" % popped['title'])
|
||||
break
|
||||
else:
|
||||
logger().error(" '%s' not found in self.cached_books" % self.cached_books[path]['title'])
|
||||
if self.verbose:
|
||||
logger().info(" '%s' not found in self.cached_books" % self.cached_books[path]['title'])
|
||||
|
||||
# Remove from thumb from thumb cache
|
||||
from calibre.utils.zipfile import ZipFile
|
||||
thumb_path = path.rpartition('.')[0] + '.jpg'
|
||||
zf = ZipFile(self.archive_path, 'a')
|
||||
|
||||
fnames = zf.namelist()
|
||||
try:
|
||||
thumb = [x for x in fnames if thumb_path in x][0]
|
||||
except:
|
||||
thumb = None
|
||||
|
||||
if thumb:
|
||||
if self.verbose:
|
||||
logger().info(" deleting '%s' from cover cache" % (thumb_path))
|
||||
zf.delete(thumb_path)
|
||||
else:
|
||||
if self.verbose:
|
||||
logger().info(" '%s' not found in cover cache" % thumb_path)
|
||||
zf.delete(thumb_path)
|
||||
elif self.verbose:
|
||||
logger().info(" '%s' not found in cover cache" % thumb_path)
|
||||
zf.close()
|
||||
|
||||
break
|
||||
@ -1003,7 +1031,7 @@ class ITUNES(DriverBase):
|
||||
self.cached_books[path]['author'],
|
||||
self.cached_books[path]['uuid']))
|
||||
|
||||
if False:
|
||||
if False and self.verbose:
|
||||
self._dump_booklist(booklists[0], indent=2)
|
||||
self._dump_cached_books(indent=2)
|
||||
|
||||
@ -1045,7 +1073,7 @@ class ITUNES(DriverBase):
|
||||
self.plugboard_func = pb_func
|
||||
|
||||
def shutdown(self):
|
||||
if False and DEBUG:
|
||||
if False and self.verbose:
|
||||
logger().info("%s.shutdown()\n" % self.__class__.__name__)
|
||||
|
||||
def sync_booklists(self, booklists, end_session=True):
|
||||
@ -1225,7 +1253,8 @@ class ITUNES(DriverBase):
|
||||
'''
|
||||
assumes pythoncom wrapper for windows
|
||||
'''
|
||||
logger().info(" %s._add_device_book()" % self.__class__.__name__)
|
||||
if self.verbose:
|
||||
logger().info(" %s._add_device_book()" % self.__class__.__name__)
|
||||
if isosx:
|
||||
import appscript
|
||||
if 'iPod' in self.sources:
|
||||
@ -1483,15 +1512,47 @@ class ITUNES(DriverBase):
|
||||
Could also be a problem with the integrity of the cover data?
|
||||
'''
|
||||
if lb_added:
|
||||
try:
|
||||
lb_added.artworks[1].data_.set(cover_data)
|
||||
except:
|
||||
delay = 2.0
|
||||
self._wait_for_writable_metadata(db_added, delay=delay)
|
||||
|
||||
# Wait for updatable artwork
|
||||
attempts = 9
|
||||
while attempts:
|
||||
try:
|
||||
lb_added.artworks[1].data_.set(cover_data)
|
||||
except:
|
||||
attempts -= 1
|
||||
time.sleep(delay)
|
||||
if self.verbose:
|
||||
# logger().warning(" iTunes automation interface reported an error"
|
||||
# " adding artwork to '%s' in the iTunes Library" % metadata.title)
|
||||
logger().info(" waiting %.1f seconds for artwork to become writable (attempt #%d)" %
|
||||
(delay, (10 - attempts)))
|
||||
else:
|
||||
if self.verbose:
|
||||
logger().warning(" iTunes automation interface reported an error"
|
||||
" adding artwork to '%s' in the iTunes Library" % metadata.title)
|
||||
pass
|
||||
logger().info(" failed to write artwork")
|
||||
|
||||
if db_added:
|
||||
delay = 2.0
|
||||
self._wait_for_writable_metadata(db_added, delay=delay)
|
||||
|
||||
# Wait for updatable artwork
|
||||
attempts = 9
|
||||
while attempts:
|
||||
try:
|
||||
db_added.artworks[1].data_.set(cover_data)
|
||||
break
|
||||
except:
|
||||
attempts -= 1
|
||||
time.sleep(delay)
|
||||
if self.verbose:
|
||||
logger().info(" waiting %.1f seconds for artwork to become writable (attempt #%d)" %
|
||||
(delay, (10 - attempts)))
|
||||
else:
|
||||
if self.verbose:
|
||||
logger().info(" failed to write artwork")
|
||||
|
||||
"""
|
||||
try:
|
||||
db_added.artworks[1].data_.set(cover_data)
|
||||
logger().info(" writing '%s' cover to iDevice" % metadata.title)
|
||||
@ -1504,6 +1565,7 @@ class ITUNES(DriverBase):
|
||||
#from calibre import ipython
|
||||
#ipython(user_ns=locals())
|
||||
pass
|
||||
"""
|
||||
|
||||
elif iswindows:
|
||||
''' Write the data to a real file for Windows iTunes '''
|
||||
@ -1524,10 +1586,28 @@ class ITUNES(DriverBase):
|
||||
pass
|
||||
|
||||
if db_added:
|
||||
if db_added.Artwork.Count:
|
||||
db_added.Artwork.Item(1).SetArtworkFromFile(tc)
|
||||
delay = 2.0
|
||||
self._wait_for_writable_metadata(db_added, delay=delay)
|
||||
|
||||
# Wait for updatable artwork
|
||||
attempts = 9
|
||||
while attempts:
|
||||
try:
|
||||
if db_added.Artwork.Count:
|
||||
db_added.Artwork.Item(1).SetArtworkFromFile(tc)
|
||||
else:
|
||||
db_added.AddArtworkFromFile(tc)
|
||||
break
|
||||
except:
|
||||
attempts -= 1
|
||||
time.sleep(delay)
|
||||
if self.verbose:
|
||||
logger().info(" waiting %.1f seconds for artwork to become writable (attempt #%d)" %
|
||||
(delay, (10 - attempts)))
|
||||
else:
|
||||
db_added.AddArtworkFromFile(tc)
|
||||
if self.verbose:
|
||||
logger().info(" failed to write artwork")
|
||||
|
||||
|
||||
elif format == 'pdf':
|
||||
if self.verbose:
|
||||
@ -1844,7 +1924,7 @@ class ITUNES(DriverBase):
|
||||
ub['title'],
|
||||
ub['author']))
|
||||
|
||||
def _find_device_book(self, search):
|
||||
def _find_device_book(self, search, attempts=9):
|
||||
'''
|
||||
Windows-only method to get a handle to device book in the current pythoncom session
|
||||
'''
|
||||
@ -1854,7 +1934,7 @@ class ITUNES(DriverBase):
|
||||
logger().info(" %s._find_device_book()" % self.__class__.__name__)
|
||||
logger().info(" searching for '%s' by '%s' (%s)" %
|
||||
(search['title'], search['author'], search['uuid']))
|
||||
attempts = 9
|
||||
|
||||
while attempts:
|
||||
# Try by uuid - only one hit
|
||||
if 'uuid' in search and search['uuid']:
|
||||
@ -1908,8 +1988,8 @@ class ITUNES(DriverBase):
|
||||
|
||||
attempts -= 1
|
||||
time.sleep(0.5)
|
||||
if self.verbose:
|
||||
logger().warning(" attempt #%d" % (10 - attempts))
|
||||
if attempts and self.verbose:
|
||||
logger().info(" attempt #%d" % (10 - attempts))
|
||||
|
||||
if self.verbose:
|
||||
logger().error(" no hits")
|
||||
@ -2010,10 +2090,10 @@ class ITUNES(DriverBase):
|
||||
attempts -= 1
|
||||
time.sleep(0.5)
|
||||
if self.verbose:
|
||||
logger().warning(" attempt #%d" % (10 - attempts))
|
||||
logger().info(" attempt #%d" % (10 - attempts))
|
||||
|
||||
if self.verbose:
|
||||
logger().error(" search for '%s' yielded no hits" % search['title'])
|
||||
logger().info(" search for '%s' yielded no hits" % search['title'])
|
||||
return None
|
||||
|
||||
def _generate_thumbnail(self, book_path, book):
|
||||
@ -2084,7 +2164,7 @@ class ITUNES(DriverBase):
|
||||
zfw.writestr(thumb_path, thumb_data)
|
||||
except:
|
||||
if self.verbose:
|
||||
logger().error(" error generating thumb for '%s', caching empty marker" % book.name())
|
||||
logger().info(" ERROR: error generating thumb for '%s', caching empty marker" % book.name())
|
||||
self._dump_hex(data[:32])
|
||||
thumb_data = None
|
||||
# Cache the empty cover
|
||||
@ -2490,6 +2570,7 @@ class ITUNES(DriverBase):
|
||||
'''
|
||||
|
||||
if self.verbose:
|
||||
import platform
|
||||
logger().info(" %s %s" % (__appname__, __version__))
|
||||
logger().info(" [OSX %s, %s %s (%s), %s driver version %d.%d.%d]" %
|
||||
(platform.mac_ver()[0],
|
||||
@ -2524,7 +2605,7 @@ class ITUNES(DriverBase):
|
||||
raise OpenFeedback('Unable to launch iTunes.\n' +
|
||||
'Try launching calibre as Administrator')
|
||||
|
||||
if not DEBUG:
|
||||
if not self.verbose:
|
||||
self.iTunes.Windows[0].Minimized = True
|
||||
self.initial_status = 'launched'
|
||||
|
||||
@ -2617,14 +2698,14 @@ class ITUNES(DriverBase):
|
||||
|
||||
if self.manual_sync_mode:
|
||||
# Delete existing from Device|Books, add to self.update_list
|
||||
# for deletion from booklist[0] during add_books_to_metadata
|
||||
# for deletion from booklist[0] during remove_books_to_metadata
|
||||
for book in self.cached_books:
|
||||
if (self.cached_books[book]['uuid'] == metadata.uuid or
|
||||
(self.cached_books[book]['title'] == metadata.title and
|
||||
self.cached_books[book]['author'] == metadata.author)):
|
||||
self.update_list.append(self.cached_books[book])
|
||||
self._remove_from_device(self.cached_books[book])
|
||||
self._remove_from_iTunes(self.cached_books[book])
|
||||
#self._remove_from_iTunes(self.cached_books[book])
|
||||
break
|
||||
else:
|
||||
if self.verbose:
|
||||
@ -2659,7 +2740,7 @@ class ITUNES(DriverBase):
|
||||
except:
|
||||
logger().error(" error deleting '%s'" % cached_book['title'])
|
||||
elif iswindows:
|
||||
hit = self._find_device_book(cached_book)
|
||||
hit = self._find_device_book(cached_book, attempts=1)
|
||||
if hit:
|
||||
if self.verbose:
|
||||
logger().info(" deleting '%s' from iDevice" % cached_book['title'])
|
||||
@ -2987,7 +3068,8 @@ class ITUNES(DriverBase):
|
||||
break
|
||||
|
||||
if db_added:
|
||||
logger().warning(" waiting for db_added to become writeable ")
|
||||
if self.verbose:
|
||||
logger().info(" waiting for db_added to become writable ")
|
||||
time.sleep(1.0)
|
||||
# If no title_sort plugboard tweak, create sort_name from series/index
|
||||
if metadata.title_sort == metadata_x.title_sort:
|
||||
@ -3029,7 +3111,8 @@ class ITUNES(DriverBase):
|
||||
lb_added.Year = metadata_x.pubdate.year
|
||||
|
||||
if db_added:
|
||||
logger().warning(" waiting for db_added to become writeable ")
|
||||
if self.verbose:
|
||||
logger().info(" waiting for db_added to become writable ")
|
||||
time.sleep(1.0)
|
||||
db_added.Name = metadata_x.title
|
||||
db_added.Album = metadata_x.title
|
||||
@ -3157,11 +3240,11 @@ class ITUNES(DriverBase):
|
||||
attempts -= 1
|
||||
time.sleep(delay)
|
||||
if self.verbose:
|
||||
logger().warning(" waiting %.1f seconds for iDevice metadata to become writable (attempt #%d)" %
|
||||
logger().info(" waiting %.1f seconds for iDevice metadata to become writable (attempt #%d)" %
|
||||
(delay, (10 - attempts)))
|
||||
else:
|
||||
if self.verbose:
|
||||
logger().error(" failed to write device metadata")
|
||||
logger().info(" ERROR: failed to write device metadata")
|
||||
|
||||
def _xform_metadata_via_plugboard(self, book, format):
|
||||
''' Transform book metadata from plugboard templates '''
|
||||
@ -3172,7 +3255,7 @@ class ITUNES(DriverBase):
|
||||
pb = self.plugboard_func(self.DEVICE_PLUGBOARD_NAME, format, self.plugboards)
|
||||
newmi = book.deepcopy_metadata()
|
||||
newmi.template_to_attribute(book, pb)
|
||||
if pb is not None and DEBUG:
|
||||
if pb is not None and self.verbose:
|
||||
#logger().info(" transforming %s using %s:" % (format, pb))
|
||||
logger().info(" title: '%s' %s" % (book.title, ">>> '%s'" %
|
||||
newmi.title if book.title != newmi.title else ''))
|
||||
|
@ -9,294 +9,15 @@ __copyright__ = '2013, Gregory Riker'
|
||||
http://www.libimobiledevice.org/docs/html/globals.html
|
||||
'''
|
||||
|
||||
import binascii, os, sys, time
|
||||
import os, sys
|
||||
|
||||
from collections import OrderedDict
|
||||
from ctypes import *
|
||||
from datetime import datetime
|
||||
|
||||
from calibre.constants import DEBUG, islinux, isosx, iswindows
|
||||
from calibre.devices.usbms.driver import debug_print
|
||||
|
||||
|
||||
#from calibre.devices.idevice.parse_xml import XmlPropertyListParser
|
||||
# *** Temporarily here until added to the code tree
|
||||
|
||||
class PropertyListParseError(Exception):
|
||||
"""Raised when parsing a property list is failed."""
|
||||
pass
|
||||
|
||||
class XmlPropertyListParser(object):
|
||||
"""
|
||||
The ``XmlPropertyListParser`` class provides methods that
|
||||
convert `Property Lists`_ objects from xml format.
|
||||
Property list objects include ``string``, ``unicode``,
|
||||
``list``, ``dict``, ``datetime``, and ``int`` or ``float``.
|
||||
|
||||
:copyright: 2008 by Takanori Ishikawa <takanori.ishikawa@gmail.com>
|
||||
: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 <plist>.")
|
||||
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, "<plist> 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 <key>%s</key> (<key> elements must be contained in <dict> element)" % content)
|
||||
#raise PropertyListParseError("<key> element '%s' must be in <dict> 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<year>\d\d\d\d)(?:-(?P<month>\d\d)(?:-(?P<day>\d\d)(?:T(?P<hour>\d\d)(?::(?P<minute>\d\d)(?::(?P<second>\d\d))?)?)?)?)?Z$")
|
||||
|
||||
def _parse_date(self, name, content):
|
||||
import datetime
|
||||
|
||||
units = ('year', 'month', 'day', 'hour', 'minute', 'second', )
|
||||
pattern = XmlPropertyListParser.DATETIME_PATTERN
|
||||
match = pattern.match(content)
|
||||
if not match:
|
||||
raise PropertyListParseError("Failed to parse datetime '%s'" % content)
|
||||
|
||||
groups, components = match.groupdict(), []
|
||||
for key in units:
|
||||
value = groups[key]
|
||||
if value is None:
|
||||
break
|
||||
components.append(int(value))
|
||||
while len(components) < 3:
|
||||
components.append(1)
|
||||
|
||||
d = datetime.datetime(*components)
|
||||
self._push_value(d)
|
||||
|
||||
def _parse_real(self, name, content):
|
||||
self._push_value(float(content))
|
||||
|
||||
def _parse_integer(self, name, content):
|
||||
self._push_value(int(content))
|
||||
|
||||
START_CALLBACKS = {
|
||||
'plist': _start_plist,
|
||||
'array': _start_array,
|
||||
'dict': _start_dict,
|
||||
'true': _start_true,
|
||||
'false': _start_false,
|
||||
}
|
||||
|
||||
END_CALLBACKS = {
|
||||
'array': _end_array,
|
||||
'dict': _end_dict,
|
||||
}
|
||||
|
||||
PARSE_CALLBACKS = {
|
||||
'key': _parse_key,
|
||||
'string': _parse_string,
|
||||
'data': _parse_data,
|
||||
'date': _parse_date,
|
||||
'real': _parse_real,
|
||||
'integer': _parse_integer,
|
||||
}
|
||||
|
||||
# ------------------------------------------------
|
||||
# XmlPropertyListParser
|
||||
# ------------------------------------------------
|
||||
def _to_stream(self, io_or_string):
|
||||
if isinstance(io_or_string, basestring):
|
||||
# Creates a string stream for in-memory contents.
|
||||
from cStringIO import StringIO
|
||||
return StringIO(io_or_string)
|
||||
elif hasattr(io_or_string, 'read') and callable(getattr(io_or_string, 'read')):
|
||||
return io_or_string
|
||||
else:
|
||||
raise TypeError('Can\'t convert %s to file-like-object' % type(io_or_string))
|
||||
|
||||
def _parse_using_etree(self, xml_input):
|
||||
from xml.etree.cElementTree import iterparse
|
||||
|
||||
parser = iterparse(self._to_stream(xml_input), events=('start', 'end'))
|
||||
self.startDocument()
|
||||
try:
|
||||
for action, element in parser:
|
||||
name = element.tag
|
||||
if action == 'start':
|
||||
if name in XmlPropertyListParser.START_CALLBACKS:
|
||||
XmlPropertyListParser.START_CALLBACKS[name](self, element.tag, element.attrib)
|
||||
elif action == 'end':
|
||||
if name in XmlPropertyListParser.END_CALLBACKS:
|
||||
XmlPropertyListParser.END_CALLBACKS[name](self, name)
|
||||
if name in XmlPropertyListParser.PARSE_CALLBACKS:
|
||||
XmlPropertyListParser.PARSE_CALLBACKS[name](self, name, element.text or "")
|
||||
element.clear()
|
||||
except SyntaxError, e:
|
||||
raise PropertyListParseError(e)
|
||||
|
||||
self.endDocument()
|
||||
return self.__plist
|
||||
|
||||
def _parse_using_sax_parser(self, xml_input):
|
||||
from xml.sax import make_parser, handler, xmlreader, \
|
||||
SAXParseException
|
||||
source = xmlreader.InputSource()
|
||||
source.setByteStream(self._to_stream(xml_input))
|
||||
reader = make_parser()
|
||||
reader.setContentHandler(self)
|
||||
try:
|
||||
reader.parse(source)
|
||||
except SAXParseException, e:
|
||||
raise PropertyListParseError(e)
|
||||
|
||||
return self.__plist
|
||||
|
||||
def parse(self, xml_input):
|
||||
"""
|
||||
Parse the property list (`.plist`, `.xml, for example) ``xml_input``,
|
||||
which can be either a string or a file-like object.
|
||||
|
||||
>>> parser = XmlPropertyListParser()
|
||||
>>> parser.parse(r'<plist version="1.0">'
|
||||
... r'<dict><key>Python</key><string>.py</string></dict>'
|
||||
... r'</plist>')
|
||||
{'Python': '.py'}
|
||||
"""
|
||||
try:
|
||||
return self._parse_using_etree(xml_input)
|
||||
except ImportError:
|
||||
# No xml.etree.ccElementTree found.
|
||||
return self._parse_using_sax_parser(xml_input)
|
||||
|
||||
# *** End temporary addition
|
||||
|
||||
class libiMobileDeviceException(Exception):
|
||||
def __init__(self, value):
|
||||
self.value = value
|
||||
@ -304,6 +25,7 @@ class libiMobileDeviceException(Exception):
|
||||
def __str__(self):
|
||||
return repr(self.value)
|
||||
|
||||
|
||||
class libiMobileDeviceIOException(Exception):
|
||||
def __init__(self, value):
|
||||
self.value = value
|
||||
@ -311,6 +33,7 @@ class libiMobileDeviceIOException(Exception):
|
||||
def __str__(self):
|
||||
return repr(self.value)
|
||||
|
||||
|
||||
class AFC_CLIENT_T(Structure):
|
||||
'''
|
||||
http://www.libimobiledevice.org/docs/html/structafc__client__private.html
|
||||
@ -342,6 +65,7 @@ class AFC_CLIENT_T(Structure):
|
||||
# afc_client_private (afc.h)
|
||||
('free_parent', c_int)]
|
||||
|
||||
|
||||
class HOUSE_ARREST_CLIENT_T(Structure):
|
||||
'''
|
||||
http://www.libimobiledevice.org/docs/html/structhouse__arrest__client__private.html
|
||||
@ -359,7 +83,8 @@ class HOUSE_ARREST_CLIENT_T(Structure):
|
||||
|
||||
# (house_arrest.h)
|
||||
('mode', c_int)
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
class IDEVICE_T(Structure):
|
||||
'''
|
||||
@ -370,6 +95,7 @@ class IDEVICE_T(Structure):
|
||||
("conn_type", c_int),
|
||||
("conn_data", c_void_p)]
|
||||
|
||||
|
||||
class INSTPROXY_CLIENT_T(Structure):
|
||||
'''
|
||||
http://www.libimobiledevice.org/docs/html/structinstproxy__client__private.html
|
||||
@ -394,6 +120,7 @@ class INSTPROXY_CLIENT_T(Structure):
|
||||
('status_updater', c_void_p)
|
||||
]
|
||||
|
||||
|
||||
class LOCKDOWND_CLIENT_T(Structure):
|
||||
'''
|
||||
http://www.libimobiledevice.org/docs/html/structlockdownd__client__private.html
|
||||
@ -416,6 +143,7 @@ class LOCKDOWND_CLIENT_T(Structure):
|
||||
('udid', c_char_p),
|
||||
('label', c_char_p)]
|
||||
|
||||
|
||||
class LOCKDOWND_SERVICE_DESCRIPTOR(Structure):
|
||||
'''
|
||||
from libimobiledevice/include/libimobiledevice/lockdown.h
|
||||
@ -423,7 +151,7 @@ class LOCKDOWND_SERVICE_DESCRIPTOR(Structure):
|
||||
_fields_ = [
|
||||
('port', c_uint),
|
||||
('ssl_enabled', c_ubyte)
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
class libiMobileDevice():
|
||||
@ -468,7 +196,6 @@ class libiMobileDevice():
|
||||
|
||||
self.load_library()
|
||||
|
||||
|
||||
# ~~~ Public methods ~~~
|
||||
def connect_idevice(self):
|
||||
'''
|
||||
@ -551,14 +278,18 @@ class libiMobileDevice():
|
||||
'''
|
||||
Determine if path exists
|
||||
|
||||
Returns [True|False] or file_info
|
||||
Returns file_info or {}
|
||||
'''
|
||||
self._log_location("'%s'" % path)
|
||||
return self._afc_get_file_info(path)
|
||||
|
||||
def get_device_info(self):
|
||||
'''
|
||||
Return device profile
|
||||
Return device profile:
|
||||
{'Model': 'iPad2,5',
|
||||
'FSTotalBytes': '14738952192',
|
||||
'FSFreeBytes': '11264917504',
|
||||
'FSBlockSize': '4096'}
|
||||
'''
|
||||
self._log_location()
|
||||
self.device_info = self._afc_get_device_info()
|
||||
@ -709,7 +440,7 @@ class libiMobileDevice():
|
||||
self._lockdown_start_service("com.apple.mobile.house_arrest")
|
||||
self.house_arrest = self._house_arrest_client_new()
|
||||
self._house_arrest_send_command(command='VendContainer',
|
||||
appid=self.installed_apps[app_name]['app_id'])
|
||||
appid=self.installed_apps[app_name]['app_id'])
|
||||
self._house_arrest_get_result()
|
||||
self.afc = self._afc_client_new_from_house_arrest_client()
|
||||
self._lockdown_client_free()
|
||||
@ -804,8 +535,8 @@ class libiMobileDevice():
|
||||
self._log_location("from: '%s' to: '%s'" % (from_name, to_name))
|
||||
|
||||
error = self.lib.afc_rename_path(byref(self.afc),
|
||||
str(from_name),
|
||||
str(to_name))
|
||||
str(from_name),
|
||||
str(to_name))
|
||||
if error and self.verbose:
|
||||
self.log(" ERROR: %s" % self.afc_error(error))
|
||||
|
||||
@ -855,7 +586,8 @@ class libiMobileDevice():
|
||||
self.log(" could not open file for writing")
|
||||
raise libiMobileDeviceIOException("could not open file for writing")
|
||||
|
||||
# ~~~ lib helpers ~~~
|
||||
# ~~~ AFC functions ~~~
|
||||
# http://www.libimobiledevice.org/docs/html/include_2libimobiledevice_2afc_8h.html
|
||||
def _afc_client_free(self):
|
||||
'''
|
||||
Frees up an AFC client.
|
||||
@ -1148,7 +880,7 @@ class libiMobileDevice():
|
||||
data = content
|
||||
datatype = c_char * len(content)
|
||||
else:
|
||||
data = bytearray(content,'utf-8')
|
||||
data = bytearray(content, 'utf-8')
|
||||
datatype = c_char * len(content)
|
||||
|
||||
error = self.lib.afc_file_write(byref(self.afc),
|
||||
@ -1247,7 +979,6 @@ class libiMobileDevice():
|
||||
while infolist[num_items]:
|
||||
item_list.append(infolist[num_items])
|
||||
num_items += 1
|
||||
item_type = None
|
||||
for i in range(0, len(item_list), 2):
|
||||
if item_list[i].contents.value in ['st_mtime', 'st_birthtime']:
|
||||
integer = item_list[i+1].contents.value[:10]
|
||||
@ -1305,7 +1036,8 @@ class libiMobileDevice():
|
||||
self.current_dir = directory
|
||||
return file_stats
|
||||
|
||||
|
||||
# ~~~ house_arrest functions ~~~
|
||||
# http://www.libimobiledevice.org/docs/html/include_2libimobiledevice_2house__arrest_8h.html
|
||||
def _house_arrest_client_free(self):
|
||||
'''
|
||||
Disconnects a house_arrest client from the device, frees up the
|
||||
@ -1411,8 +1143,8 @@ class libiMobileDevice():
|
||||
self._log_location()
|
||||
|
||||
plist = c_char_p()
|
||||
error = self.lib.house_arrest_get_result(byref(self.house_arrest),
|
||||
byref(plist)) & 0xFFFF
|
||||
self.lib.house_arrest_get_result(byref(self.house_arrest),
|
||||
byref(plist)) & 0xFFFF
|
||||
plist = c_void_p.from_buffer(plist)
|
||||
|
||||
# Convert the plist to xml
|
||||
@ -1474,7 +1206,8 @@ class libiMobileDevice():
|
||||
desc=self._house_arrest_error(error))
|
||||
raise libiMobileDeviceException(error_description)
|
||||
|
||||
|
||||
# ~~~ idevice functions ~~~
|
||||
# http://www.libimobiledevice.org/docs/html/libimobiledevice_8h.html
|
||||
def _idevice_error(self, error):
|
||||
e = "UNKNOWN ERROR"
|
||||
if not error:
|
||||
@ -1556,7 +1289,8 @@ class libiMobileDevice():
|
||||
self._log_location(debug)
|
||||
self.lib.idevice_set_debug_level(debug)
|
||||
|
||||
|
||||
# ~~~ instproxy functions ~~~
|
||||
# http://www.libimobiledevice.org/docs/html/include_2libimobiledevice_2installation__proxy_8h.html
|
||||
def _instproxy_browse(self, applist=[]):
|
||||
'''
|
||||
Fetch the app list
|
||||
@ -1655,7 +1389,7 @@ class libiMobileDevice():
|
||||
self._log_location("'%s', '%s'" % (app_type, domain))
|
||||
|
||||
self.lib.instproxy_client_options_add(self.client_options,
|
||||
app_type, domain, None)
|
||||
app_type, domain, None)
|
||||
|
||||
def _instproxy_client_options_free(self):
|
||||
'''
|
||||
@ -1693,7 +1427,8 @@ class libiMobileDevice():
|
||||
e = "Operation failed (-5)"
|
||||
return e
|
||||
|
||||
|
||||
# ~~~ lockdown functions ~~~
|
||||
# http://www.libimobiledevice.org/docs/html/include_2libimobiledevice_2lockdown_8h.html
|
||||
def _lockdown_client_free(self):
|
||||
'''
|
||||
Close the lockdownd client session if one is running, free up the lockdown_client struct
|
||||
@ -1930,7 +1665,7 @@ class libiMobileDevice():
|
||||
desc=self._lockdown_error(error))
|
||||
raise libiMobileDeviceException(error_description)
|
||||
|
||||
|
||||
# ~~~ logging ~~~
|
||||
def _log_location(self, *args):
|
||||
'''
|
||||
'''
|
||||
@ -1945,5 +1680,4 @@ class libiMobileDevice():
|
||||
arg2 = args[1]
|
||||
|
||||
self.log(self.LOCATION_TEMPLATE.format(cls=self.__class__.__name__,
|
||||
func=sys._getframe(1).f_code.co_name, arg1=arg1, arg2=arg2))
|
||||
|
||||
func=sys._getframe(1).f_code.co_name, arg1=arg1, arg2=arg2))
|
@ -15,6 +15,7 @@ a property list file and get back a python native data structure.
|
||||
.. _Property Lists: http://developer.apple.com/documentation/Cocoa/Conceptual/PropertyLists/
|
||||
"""
|
||||
|
||||
|
||||
class PropertyListParseError(Exception):
|
||||
"""Raised when parsing a property list is failed."""
|
||||
pass
|
||||
@ -42,18 +43,25 @@ class XmlPropertyListParser(object):
|
||||
# ------------------------------------------------
|
||||
def setDocumentLocator(self, locator):
|
||||
pass
|
||||
|
||||
def startPrefixMapping(self, prefix, uri):
|
||||
pass
|
||||
|
||||
def endPrefixMapping(self, prefix):
|
||||
pass
|
||||
|
||||
def startElementNS(self, name, qname, attrs):
|
||||
pass
|
||||
|
||||
def endElementNS(self, name, qname):
|
||||
pass
|
||||
|
||||
def ignorableWhitespace(self, whitespace):
|
||||
pass
|
||||
|
||||
def processingInstruction(self, target, data):
|
||||
pass
|
||||
|
||||
def skippedEntity(self, name):
|
||||
pass
|
||||
|
||||
@ -125,7 +133,7 @@ class XmlPropertyListParser(object):
|
||||
def _start_plist(self, name, attrs):
|
||||
self._assert(not self.__stack and self.__plist is None, "<plist> more than once.")
|
||||
self._assert(attrs.get('version', '1.0') == '1.0',
|
||||
"version 1.0 is only supported, but was '%s'." % attrs.get('version'))
|
||||
"version 1.0 is only supported, but was '%s'." % attrs.get('version'))
|
||||
|
||||
def _start_array(self, name, attrs):
|
||||
v = list()
|
||||
@ -259,8 +267,7 @@ class XmlPropertyListParser(object):
|
||||
return self.__plist
|
||||
|
||||
def _parse_using_sax_parser(self, xml_input):
|
||||
from xml.sax import make_parser, handler, xmlreader, \
|
||||
SAXParseException
|
||||
from xml.sax import make_parser, xmlreader, SAXParseException
|
||||
source = xmlreader.InputSource()
|
||||
source.setByteStream(self._to_stream(xml_input))
|
||||
reader = make_parser()
|
||||
@ -287,4 +294,4 @@ class XmlPropertyListParser(object):
|
||||
return self._parse_using_etree(xml_input)
|
||||
except ImportError:
|
||||
# No xml.etree.ccElementTree found.
|
||||
return self._parse_using_sax_parser(xml_input)
|
||||
return self._parse_using_sax_parser(xml_input)
|
||||
|
Loading…
x
Reference in New Issue
Block a user