From 8daa9ab683f5cb341994240c61fb56135d3ec878 Mon Sep 17 00:00:00 2001 From: GRiker Date: Sat, 22 May 2010 06:22:01 -0600 Subject: [PATCH 01/11] GwR initial iTunes driver --- src/calibre/devices/apple/__init__.py | 2 + src/calibre/devices/apple/driver.py | 287 ++++++++++++++++++++++++++ 2 files changed, 289 insertions(+) create mode 100644 src/calibre/devices/apple/__init__.py create mode 100644 src/calibre/devices/apple/driver.py diff --git a/src/calibre/devices/apple/__init__.py b/src/calibre/devices/apple/__init__.py new file mode 100644 index 0000000000..c705e32a66 --- /dev/null +++ b/src/calibre/devices/apple/__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/apple/driver.py b/src/calibre/devices/apple/driver.py new file mode 100644 index 0000000000..ef5cf344a1 --- /dev/null +++ b/src/calibre/devices/apple/driver.py @@ -0,0 +1,287 @@ +''' + Device driver for iTunes + + GRiker + + 22 May 2010 +''' + +from calibre.devices.interface import DevicePlugin + +class iDevice(DevicePlugin): + name = 'Apple device interface' + gui_name = 'Apple device' + supported_platforms = ['windows','osx'] + author = 'GRiker' + + FORMATS = ['epub'] + + VENDOR_ID = [0x0830] + PRODUCT_ID = [0x8004, 0x8002, 0x0101] + BCD = [0x0316] + + def is_usb_connected(self, device_on_system): + return True + + def can_handle(self, device_info): + # Return True if iTunes installed + + def can_handle_windows(self, device_id, debug=False): + ''' + Optional method to perform further checks on a device to see if this driver + is capable of handling it. If it is not it should return False. This method + is only called after the vendor, product ids and the bcd have matched, so + it can do some relatively time intensive checks. The default implementation + returns True. This method is called only on windows. See also + :method:`can_handle`. + + :param device_info: On windows a device ID string. On Unix a tuple of + ``(vendor_id, product_id, bcd)``. + ''' + return True + + def can_handle(self, device_info, debug=False): + ''' + Unix version of :method:`can_handle_windows` + + :param device_info: Is a tupe of (vid, pid, bcd, manufacturer, product, + serial number) + ''' + + return True + + def open(self): + ''' + Perform any device specific initialization. Called after the device is + detected but before any other functions that communicate with the device. + For example: For devices that present themselves as USB Mass storage + devices, this method would be responsible for mounting the device or + if the device has been automounted, for finding out where it has been + mounted. The base class within USBMS device.py has a implementation of + this function that should serve as a good example for USB Mass storage + devices. + ''' + print "iDevice(): I am here!" + + def eject(self): + ''' + 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. + ''' + raise NotImplementedError() + + def post_yank_cleanup(self): + ''' + Called if the user yanks the device without ejecting it first. + ''' + raise NotImplementedError() + + def set_progress_reporter(self, report_progress): + ''' + @param report_progress: Function that is called with a % progress + (number between 0 and 100) for various tasks + If it is called with -1 that means that the + task does not have any progress information + ''' + raise NotImplementedError() + + def get_device_information(self, end_session=True): + """ + Ask device for device information. See L{DeviceInfoQuery}. + @return: (device name, device version, software version on device, mime type) + """ + raise NotImplementedError() + + def card_prefix(self, end_session=True): + ''' + Return a 2 element list of the prefix to paths on the cards. + If no card is present None is set for the card's prefix. + E.G. + ('/place', '/place2') + (None, 'place2') + ('place', None) + (None, None) + ''' + raise NotImplementedError() + + def total_space(self, end_session=True): + """ + Get total space available on the mountpoints: + 1. Main memory + 2. Memory Card A + 3. Memory Card B + + @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. + """ + raise NotImplementedError() + + def free_space(self, end_session=True): + """ + Get free space available on the mountpoints: + 1. Main memory + 2. Card A + 3. Card B + + @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. + """ + raise NotImplementedError() + + def books(self, oncard=None, end_session=True): + """ + Return a list of ebooks on the device. + @param oncard: If 'carda' or 'cardb' return a list of ebooks on the + specific storage card, otherwise return list of ebooks + in main memory of device. If a card is specified and no + books are on the card return empty list. + @return: A BookList. + """ + raise NotImplementedError() + + def upload_books(self, files, names, on_card=None, end_session=True, + metadata=None): + ''' + Upload a list of books to the device. If a file already + exists on the device, it should be replaced. + This method should raise a L{FreeSpaceError} if there is not enough + free space on the device. The text of the FreeSpaceError must contain the + word "card" if C{on_card} is not None otherwise it must contain the word "memory". + :files: A list of paths and/or file-like objects. + :names: A list of file names that the books should have + once uploaded to the device. len(names) == len(files) + :return: A list of 3-element tuples. The list is meant to be passed + to L{add_books_to_metadata}. + :metadata: If not None, it is a list of :class:`MetaInformation` objects. + The idea is to use the metadata to determine where on the device to + put the book. len(metadata) == len(files). Apart from the regular + cover (path to cover), there may also be a thumbnail attribute, which should + be used in preference. The thumbnail attribute is of the form + (width, height, cover_data as jpeg). + ''' + raise NotImplementedError() + + @classmethod + def add_books_to_metadata(cls, locations, metadata, booklists): + ''' + Add locations to the booklists. This function must not communicate with + the device. + @param locations: Result of a call to L{upload_books} + @param metadata: List of MetaInformation objects, same as for + :method:`upload_books`. + @param booklists: A tuple containing the result of calls to + (L{books}(oncard=None), L{books}(oncard='carda'), + L{books}(oncard='cardb')). + ''' + raise NotImplementedError + + def delete_books(self, paths, end_session=True): + ''' + Delete books at paths on device. + ''' + raise NotImplementedError() + + @classmethod + def remove_books_from_metadata(cls, paths, booklists): + ''' + Remove books from the metadata list. This function must not communicate + with the device. + @param paths: paths to books on the device. + @param booklists: A tuple containing the result of calls to + (L{books}(oncard=None), L{books}(oncard='carda'), + L{books}(oncard='cardb')). + ''' + raise NotImplementedError() + + def sync_booklists(self, booklists, end_session=True): + ''' + Update metadata on device. + @param booklists: A tuple containing the result of calls to + (L{books}(oncard=None), L{books}(oncard='carda'), + L{books}(oncard='cardb')). + ''' + raise NotImplementedError() + + def get_file(self, path, outfile, end_session=True): + ''' + 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 + ''' + raise NotImplementedError() + + @classmethod + def config_widget(cls): + ''' + Should return a QWidget. The QWidget contains the settings for the device interface + ''' + raise NotImplementedError() + + @classmethod + def save_settings(cls, settings_widget): + ''' + Should save settings to disk. Takes the widget created in config_widget + and saves all settings to disk. + ''' + raise NotImplementedError() + + @classmethod + def settings(cls): + ''' + Should return an opts object. The opts object should have one attribute + `format_map` which is an ordered list of formats for the device. + ''' + raise NotImplementedError() + + + + +class BookList(list): + ''' + A list of books. Each Book object must have the fields: + 1. title + 2. authors + 3. size (file size of the book) + 4. datetime (a UTC time tuple) + 5. path (path on the device to the book) + 6. thumbnail (can be None) thumbnail is either a str/bytes object with the + image data or it should have an attribute image_path that stores an + absolute (platform native) path to the image + 7. tags (a list of strings, can be empty). + ''' + + __getslice__ = None + __setslice__ = None + + def __init__(self, oncard, prefix, settings): + pass + + def supports_collections(self): + ''' Return True if the the device supports collections for this book list. ''' + raise NotImplementedError() + + def add_book(self, book, replace_metadata): + ''' + Add the book to the booklist. Intent is to maintain any device-internal + metadata. Return True if booklists must be sync'ed + ''' + raise NotImplementedError() + + def remove_book(self, book): + ''' + Remove a book from the booklist. Correct any device metadata at the + same time + ''' + raise NotImplementedError() + + def get_collections(self, collection_attributes): + ''' + Return a dictionary of collections created from collection_attributes. + Each entry in the dictionary is of the form collection name:[list of + books] + + The list of books is sorted by book title, except for collections + created from series, in which case series_index is used. + + :param collection_attributes: A list of attributes of the Book object + ''' + raise NotImplementedError() \ No newline at end of file From ddda93ea7a297a7711c176d4e842460d2a7f360a Mon Sep 17 00:00:00 2001 From: GRiker Date: Sat, 22 May 2010 06:31:22 -0600 Subject: [PATCH 02/11] GwR initial iTunes driver --- src/calibre/devices/apple/driver.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/calibre/devices/apple/driver.py b/src/calibre/devices/apple/driver.py index ef5cf344a1..cebc20f732 100644 --- a/src/calibre/devices/apple/driver.py +++ b/src/calibre/devices/apple/driver.py @@ -23,9 +23,6 @@ class iDevice(DevicePlugin): def is_usb_connected(self, device_on_system): return True - def can_handle(self, device_info): - # Return True if iTunes installed - def can_handle_windows(self, device_id, debug=False): ''' Optional method to perform further checks on a device to see if this driver From 007cf9d6c1121a99c02a4880fdc39557425b61a2 Mon Sep 17 00:00:00 2001 From: GRiker Date: Sun, 23 May 2010 10:15:07 -0600 Subject: [PATCH 03/11] GwR early apple driver --- src/calibre/customize/builtins.py | 3 +- src/calibre/devices/apple/driver.py | 346 +++++++++++++++++----------- 2 files changed, 215 insertions(+), 134 deletions(-) diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index d1f5ea050c..e2e4b549c8 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -430,7 +430,7 @@ from calibre.ebooks.txt.output import TXTOutput from calibre.customize.profiles import input_profiles, output_profiles - +from calibre.devices.apple.driver import ITUNES from calibre.devices.hanlin.driver import HANLINV3, HANLINV5, BOOX from calibre.devices.blackberry.driver import BLACKBERRY from calibre.devices.cybook.driver import CYBOOK @@ -495,6 +495,7 @@ plugins += [ ] # Order here matters. The first matched device is the one used. plugins += [ + ITUNES, HANLINV3, HANLINV5, BLACKBERRY, diff --git a/src/calibre/devices/apple/driver.py b/src/calibre/devices/apple/driver.py index cebc20f732..154412d220 100644 --- a/src/calibre/devices/apple/driver.py +++ b/src/calibre/devices/apple/driver.py @@ -5,22 +5,83 @@ 22 May 2010 ''' +import datetime +from calibre.constants import isosx, iswindows from calibre.devices.interface import DevicePlugin +#from calibre.ebooks.metadata import MetaInformation +from calibre.utils.config import Config -class iDevice(DevicePlugin): +if isosx: + print "running in OSX" + import appscript + +if iswindows: + print "running in Windows" + import win32com.client + +class ITUNES(DevicePlugin): name = 'Apple device interface' gui_name = 'Apple device' + icon = I('devices/iPad.png') + description = _('Communicate with iBooks through iTunes.') supported_platforms = ['windows','osx'] author = 'GRiker' FORMATS = ['epub'] - VENDOR_ID = [0x0830] - PRODUCT_ID = [0x8004, 0x8002, 0x0101] - BCD = [0x0316] + VENDOR_ID = [0x05ac] + # 0x129a:iPad 0x1292:iPhone 3G + PRODUCT_ID = [0x129a,0x1292] + BCD = [0x01] - def is_usb_connected(self, device_on_system): + app = None + is_connected = False + + + # Public methods + + def add_books_to_metadata(cls, locations, metadata, booklists): + ''' + Add locations to the booklists. This function must not communicate with + the device. + @param locations: Result of a call to L{upload_books} + @param metadata: List of MetaInformation objects, same as for + :method:`upload_books`. + @param booklists: A tuple containing the result of calls to + (L{books}(oncard=None), L{books}(oncard='carda'), + L{books}(oncard='cardb')). + ''' + raise NotImplementedError + + def books(self, oncard=None, end_session=True): + """ + Return a list of ebooks on the device. + @param oncard: If 'carda' or 'cardb' return a list of ebooks on the + specific storage card, otherwise return list of ebooks + in main memory of device. If a card is specified and no + books are on the card return empty list. + @return: A BookList. + """ + print "ITUNES:books(oncard=%s)" % oncard + if not oncard: + myBooks = BookList() + book = Book() + + myBooks.add_book(book, False) + print "len(myBooks): %d" % len(myBooks) + return myBooks + else: + return [] + + def can_handle(self, device_info, debug=False): + ''' + Unix version of :method:`can_handle_windows` + + :param device_info: Is a tupe of (vid, pid, bcd, manufacturer, product, + serial number) + ''' + print "ITUNES:can_handle()" return True def can_handle_windows(self, device_id, debug=False): @@ -35,17 +96,68 @@ class iDevice(DevicePlugin): :param device_info: On windows a device ID string. On Unix a tuple of ``(vendor_id, product_id, bcd)``. ''' + print "ITUNES:can_handle_windows()" return True - def can_handle(self, device_info, debug=False): + def card_prefix(self, end_session=True): ''' - Unix version of :method:`can_handle_windows` - - :param device_info: Is a tupe of (vid, pid, bcd, manufacturer, product, - serial number) + Return a 2 element list of the prefix to paths on the cards. + If no card is present None is set for the card's prefix. + E.G. + ('/place', '/place2') + (None, 'place2') + ('place', None) + (None, None) ''' + print "ITUNES:card_prefix()" + return (None,None) - return True + def config_widget(cls): + ''' + Should return a QWidget. The QWidget contains the settings for the device interface + ''' + raise NotImplementedError() + + def delete_books(self, paths, end_session=True): + ''' + Delete books at paths on device. + ''' + raise NotImplementedError() + + def eject(self): + ''' + 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. + ''' + print "ITUNES:eject()" + + def free_space(self, end_session=True): + """ + Get free space available on the mountpoints: + 1. Main memory + 2. Card A + 3. Card B + + @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. + """ + print "ITUNES:free_space()" + return (0,-1,-1) + + def get_device_information(self, end_session=True): + """ + Ask device for device information. See L{DeviceInfoQuery}. + @return: (device name, device version, software version on device, mime type) + """ + print "ITUNES:get_device_information()" + return ('iPad','hw v1.0','sw v1.0', 'mime type') + + def get_file(self, path, outfile, end_session=True): + ''' + 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 + ''' + raise NotImplementedError() def open(self): ''' @@ -58,14 +170,16 @@ class iDevice(DevicePlugin): this function that should serve as a good example for USB Mass storage devices. ''' - print "iDevice(): I am here!" - - def eject(self): - ''' - 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. - ''' - raise NotImplementedError() + print "ITUNES.open()" + if isosx: + # Launch iTunes if not already running + running_apps = appscript.app('System Events') + if not 'iTunes' in running_apps.processes.name(): + print " launching iTunes" + app = appscript.app('iTunes', hide=True) + app.run() + self.app = app + # May need to set focus back to calibre here? def post_yank_cleanup(self): ''' @@ -73,6 +187,37 @@ class iDevice(DevicePlugin): ''' raise NotImplementedError() + def remove_books_from_metadata(cls, paths, booklists): + ''' + Remove books from the metadata list. This function must not communicate + with the device. + @param paths: paths to books on the device. + @param booklists: A tuple containing the result of calls to + (L{books}(oncard=None), L{books}(oncard='carda'), + L{books}(oncard='cardb')). + ''' + raise NotImplementedError() + + def reset(self, key='-1', log_packets=False, report_progress=None, + detected_device=None) : + """ + :key: The key to unlock the device + :log_packets: If true the packet stream to/from the device is logged + :report_progress: Function that is called with a % progress + (number between 0 and 100) for various tasks + If it is called with -1 that means that the + task does not have any progress information + :detected_device: Device information from the device scanner + """ + print "ITUNE.reset()" + + def save_settings(cls, settings_widget): + ''' + Should save settings to disk. Takes the widget created in config_widget + and saves all settings to disk. + ''' + raise NotImplementedError() + def set_progress_reporter(self, report_progress): ''' @param report_progress: Function that is called with a % progress @@ -80,26 +225,28 @@ class iDevice(DevicePlugin): If it is called with -1 that means that the task does not have any progress information ''' - raise NotImplementedError() + print "ITUNES:set_progress_reporter()" - def get_device_information(self, end_session=True): - """ - Ask device for device information. See L{DeviceInfoQuery}. - @return: (device name, device version, software version on device, mime type) - """ - raise NotImplementedError() + def settings(cls): + ''' + Should return an opts object. The opts object should have one attribute + `format_map` which is an ordered list of formats for the device. + ''' + print "ITUNES.settings()" + klass = cls if isinstance(cls, type) else cls.__class__ + c = Config('device_drivers_%s' % klass.__name__, _('settings for device drivers')) + c.add_opt('format_map', default=cls.FORMATS, + help=_('Ordered list of formats the device will accept')) + return c.parse() - def card_prefix(self, end_session=True): + def sync_booklists(self, booklists, end_session=True): ''' - Return a 2 element list of the prefix to paths on the cards. - If no card is present None is set for the card's prefix. - E.G. - ('/place', '/place2') - (None, 'place2') - ('place', None) - (None, None) + Update metadata on device. + @param booklists: A tuple containing the result of calls to + (L{books}(oncard=None), L{books}(oncard='carda'), + L{books}(oncard='cardb')). ''' - raise NotImplementedError() + print "ITUNES:sync_booklists():" def total_space(self, end_session=True): """ @@ -111,30 +258,7 @@ class iDevice(DevicePlugin): @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. """ - raise NotImplementedError() - - def free_space(self, end_session=True): - """ - Get free space available on the mountpoints: - 1. Main memory - 2. Card A - 3. Card B - - @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. - """ - raise NotImplementedError() - - def books(self, oncard=None, end_session=True): - """ - Return a list of ebooks on the device. - @param oncard: If 'carda' or 'cardb' return a list of ebooks on the - specific storage card, otherwise return list of ebooks - in main memory of device. If a card is specified and no - books are on the card return empty list. - @return: A BookList. - """ - raise NotImplementedError() + print "ITUNES:total_space()" def upload_books(self, files, names, on_card=None, end_session=True, metadata=None): @@ -158,79 +282,16 @@ class iDevice(DevicePlugin): ''' raise NotImplementedError() - @classmethod - def add_books_to_metadata(cls, locations, metadata, booklists): - ''' - Add locations to the booklists. This function must not communicate with - the device. - @param locations: Result of a call to L{upload_books} - @param metadata: List of MetaInformation objects, same as for - :method:`upload_books`. - @param booklists: A tuple containing the result of calls to - (L{books}(oncard=None), L{books}(oncard='carda'), - L{books}(oncard='cardb')). - ''' - raise NotImplementedError + # Private methods - def delete_books(self, paths, end_session=True): + def _get_source(self): ''' - Delete books at paths on device. + Get iTunes sources (Library, iPod, Radio ...) ''' - raise NotImplementedError() - - @classmethod - def remove_books_from_metadata(cls, paths, booklists): - ''' - Remove books from the metadata list. This function must not communicate - with the device. - @param paths: paths to books on the device. - @param booklists: A tuple containing the result of calls to - (L{books}(oncard=None), L{books}(oncard='carda'), - L{books}(oncard='cardb')). - ''' - raise NotImplementedError() - - def sync_booklists(self, booklists, end_session=True): - ''' - Update metadata on device. - @param booklists: A tuple containing the result of calls to - (L{books}(oncard=None), L{books}(oncard='carda'), - L{books}(oncard='cardb')). - ''' - raise NotImplementedError() - - def get_file(self, path, outfile, end_session=True): - ''' - 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 - ''' - raise NotImplementedError() - - @classmethod - def config_widget(cls): - ''' - Should return a QWidget. The QWidget contains the settings for the device interface - ''' - raise NotImplementedError() - - @classmethod - def save_settings(cls, settings_widget): - ''' - Should save settings to disk. Takes the widget created in config_widget - and saves all settings to disk. - ''' - raise NotImplementedError() - - @classmethod - def settings(cls): - ''' - Should return an opts object. The opts object should have one attribute - `format_map` which is an ordered list of formats for the device. - ''' - raise NotImplementedError() - - - + sources = self._app.sources() + names = [s.name() for s in sources] + kinds = [s.kind() for s in sources] + return dict(zip(kinds,names)) class BookList(list): ''' @@ -249,19 +310,20 @@ class BookList(list): __getslice__ = None __setslice__ = None - def __init__(self, oncard, prefix, settings): + def __init__(self): pass def supports_collections(self): ''' Return True if the the device supports collections for this book list. ''' - raise NotImplementedError() + return False def add_book(self, book, replace_metadata): ''' Add the book to the booklist. Intent is to maintain any device-internal metadata. Return True if booklists must be sync'ed ''' - raise NotImplementedError() + print "adding %s" % book + self.append(book) def remove_book(self, book): ''' @@ -281,4 +343,22 @@ class BookList(list): :param collection_attributes: A list of attributes of the Book object ''' - raise NotImplementedError() \ No newline at end of file + return {} + +class Book(object): + ''' + A simple class describing a book in the iTunes Books Library. + These seem to be the minimum Book attributes needed. + ''' + def __init__(self): + setattr(self,'title','A Book Title') + setattr(self,'authors',['John Doe']) + setattr(self,'path','some/path.epub') + setattr(self,'size',1234567) + setattr(self,'datetime',datetime.datetime.now().timetuple()) + setattr(self,'thumbnail',None) + setattr(self,'db_id',0) + setattr(self,'device_collections',[]) + setattr(self,'tags',['Genre']) + + From e2ef2579536d39c720d083007652fbaea67a7090 Mon Sep 17 00:00:00 2001 From: GRiker Date: Sun, 23 May 2010 15:52:17 -0600 Subject: [PATCH 04/11] GwR early iPad device driver --- src/calibre/devices/apple/driver.py | 79 +++++++++++++++++------------ 1 file changed, 47 insertions(+), 32 deletions(-) diff --git a/src/calibre/devices/apple/driver.py b/src/calibre/devices/apple/driver.py index 154412d220..8ec93b9499 100644 --- a/src/calibre/devices/apple/driver.py +++ b/src/calibre/devices/apple/driver.py @@ -5,12 +5,13 @@ 22 May 2010 ''' -import datetime +import datetime, re from calibre.constants import isosx, iswindows from calibre.devices.interface import DevicePlugin -#from calibre.ebooks.metadata import MetaInformation +from calibre.ebooks.metadata import MetaInformation from calibre.utils.config import Config +from calibre.utils.date import parse_date if isosx: print "running in OSX" @@ -35,7 +36,7 @@ class ITUNES(DevicePlugin): PRODUCT_ID = [0x129a,0x1292] BCD = [0x01] - app = None + it = None is_connected = False @@ -65,12 +66,32 @@ class ITUNES(DevicePlugin): """ print "ITUNES:books(oncard=%s)" % oncard if not oncard: - myBooks = BookList() - book = Book() + # Fetch a list of books from iTunes + if isosx: + names = [s.name() for s in self.it.sources()] + kinds = [s.kind() for s in self.it.sources()] + sources = dict(zip(kinds,names)) + + lib = self.it.sources['Library'] + + if 'Books' in lib.playlists.name(): + booklist = BookList() + it_books = lib.playlists['Books'].file_tracks() + for it_book in it_books: + this_book = Book(it_book.name(), it_book.artist()) + this_book.datetime = parse_date(str(it_book.date_added())).timetuple() + this_book.db_id = None + this_book.device_collections = [] + this_book.path = 'iTunes/Books/%s.epub' % it_book.name() + this_book.size = it_book.size() + this_book.thumbnail = None + booklist.add_book(this_book, False) + return booklist + + else: + return [] + - myBooks.add_book(book, False) - print "len(myBooks): %d" % len(myBooks) - return myBooks else: return [] @@ -80,8 +101,9 @@ class ITUNES(DevicePlugin): :param device_info: Is a tupe of (vid, pid, bcd, manufacturer, product, serial number) + This gets called ~1x/second while device is sensed ''' - print "ITUNES:can_handle()" + # print "ITUNES:can_handle()" return True def can_handle_windows(self, device_id, debug=False): @@ -176,10 +198,11 @@ class ITUNES(DevicePlugin): running_apps = appscript.app('System Events') if not 'iTunes' in running_apps.processes.name(): print " launching iTunes" - app = appscript.app('iTunes', hide=True) + it = appscript.app('iTunes', hide=True) app.run() - self.app = app - # May need to set focus back to calibre here? + self.it = it + else: + self.it = appscript.app('iTunes') def post_yank_cleanup(self): ''' @@ -284,14 +307,6 @@ class ITUNES(DevicePlugin): # Private methods - def _get_source(self): - ''' - Get iTunes sources (Library, iPod, Radio ...) - ''' - sources = self._app.sources() - names = [s.name() for s in sources] - kinds = [s.kind() for s in sources] - return dict(zip(kinds,names)) class BookList(list): ''' @@ -345,20 +360,20 @@ class BookList(list): ''' return {} -class Book(object): +class Book(MetaInformation): ''' A simple class describing a book in the iTunes Books Library. - These seem to be the minimum Book attributes needed. + Q's: + - Should thumbnail come from calibre if available? + - See ebooks.metadata.__init__ for all fields ''' - def __init__(self): - setattr(self,'title','A Book Title') - setattr(self,'authors',['John Doe']) - setattr(self,'path','some/path.epub') - setattr(self,'size',1234567) - setattr(self,'datetime',datetime.datetime.now().timetuple()) - setattr(self,'thumbnail',None) - setattr(self,'db_id',0) - setattr(self,'device_collections',[]) - setattr(self,'tags',['Genre']) + def __init__(self,title,author): + MetaInformation.__init__(self, title, authors=[author]) + @dynamic_property + def title_sorter(self): + doc = '''String to sort the title. If absent, title is returned''' + def fget(self): + return re.sub('^\s*A\s+|^\s*The\s+|^\s*An\s+', '', self.title).rstrip() + return property(doc=doc, fget=fget) From ac8c95135bcbf4d5b152eaeff2cdd18718da1e68 Mon Sep 17 00:00:00 2001 From: GRiker Date: Mon, 24 May 2010 10:30:15 -0600 Subject: [PATCH 05/11] GwR apple driver in progress --- src/calibre/devices/apple/driver.py | 38 ++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/src/calibre/devices/apple/driver.py b/src/calibre/devices/apple/driver.py index 102cc1ebab..180fcf5a89 100644 --- a/src/calibre/devices/apple/driver.py +++ b/src/calibre/devices/apple/driver.py @@ -39,6 +39,7 @@ class ITUNES(DevicePlugin): BCD = [0x01] # Properties + cached_paths = {} iTunes= None sources = None verbose = True @@ -98,16 +99,22 @@ class ITUNES(DevicePlugin): device = self.sources['iPod'] if 'Books' in self.iTunes.sources[device].playlists.name(): booklist = BookList() + cached_paths = {} books = self.iTunes.sources[device].playlists['Books'].file_tracks() for book in books: this_book = Book(book.name(), book.artist()) this_book.datetime = parse_date(str(book.date_added())).timetuple() this_book.db_id = None this_book.device_collections = [] - this_book.path = '%s.epub' % book.name() + this_book.path = 'iTunes/%s - %s.epub' % (book.name(), book.artist()) this_book.size = book.size() this_book.thumbnail = None booklist.add_book(this_book, False) + cached_paths[this_book.path] = { 'title':book.name(), + 'author':book.artist(), + 'book':book} + self.cached_paths = cached_paths + print self.cached_paths return booklist else: # No books installed on this device @@ -202,8 +209,33 @@ class ITUNES(DevicePlugin): def delete_books(self, paths, end_session=True): ''' Delete books at paths on device. + Since we're deleting through iTunes, we'll use the cached handle to the book ''' - raise NotImplementedError() + for path in paths: + title = self.cached_paths[path]['title'] + author = self.cached_paths[path]['author'] + book = self.cached_paths[path]['book'] + print "ITUNES.delete_books(): Searching for '%s - %s'" % (title,author) + if True: + results = self.iTunes.playlists['library'].file_tracks[ + (appscript.its.name == title).AND + (appscript.its.artist == author).AND + (appscript.its.kind == 'Book')].get() + if len(results) == 1: + book_to_delete = results[0] + print "book_to_delete: %s" % book_to_delete + if self.verbose: + print "ITUNES:delete_books(): Deleting '%s - %s'" % (title, author) + self.iTunes.delete(results[0]) + elif len(results) > 1: + print "ITUNES.delete_books(): More than one book matches '%s - %s'" % (title, author) + else: + print "ITUNES.delete_books(): No book '%s - %s' found in iTunes" % (title, author) + else: + if self.verbose: + print "ITUNES:delete_books(): Deleting '%s - %s'" % (title, author) + self.iTunes.delete(book) + def eject(self): ''' @@ -279,7 +311,7 @@ class ITUNES(DevicePlugin): (L{books}(oncard=None), L{books}(oncard='carda'), L{books}(oncard='cardb')). ''' - raise NotImplementedError() + print "ITUNES.remove_books_from_metadata(): need to implement" def reset(self, key='-1', log_packets=False, report_progress=None, detected_device=None) : From afe594d5aa8968eb551bc6f9819cc114c631491d Mon Sep 17 00:00:00 2001 From: GRiker Date: Mon, 24 May 2010 14:53:21 -0600 Subject: [PATCH 06/11] GwR apple driver wip --- src/calibre/devices/apple/driver.py | 75 ++++++++++++++++------------- 1 file changed, 41 insertions(+), 34 deletions(-) diff --git a/src/calibre/devices/apple/driver.py b/src/calibre/devices/apple/driver.py index 180fcf5a89..1c596f9da8 100644 --- a/src/calibre/devices/apple/driver.py +++ b/src/calibre/devices/apple/driver.py @@ -139,38 +139,25 @@ class ITUNES(DevicePlugin): ''' # print "ITUNES:can_handle()" if isosx: - # Launch iTunes if not already running - if not self.iTunes: - if self.verbose: - print "ITUNES:can_handle(): Instantiating iTunes" - running_apps = appscript.app('System Events') - if not 'iTunes' in running_apps.processes.name(): + if self.iTunes: + # Check for connected book-capable device + names = [s.name() for s in self.iTunes.sources()] + kinds = [str(s.kind()).rpartition('.')[2] for s in self.iTunes.sources()] + self.sources = sources = dict(zip(kinds,names)) + if 'iPod' in sources: if self.verbose: - print "ITUNES:can_handle(): Launching iTunes" - self.iTunes = iTunes= appscript.app('iTunes', hide=True) - iTunes.run() - if self.verbose: - print "%s - %s (launched)" % (self.iTunes.name(), self.iTunes.version()) + sys.stdout.write('.') + sys.stdout.flush() + return True else: - self.iTunes = appscript.app('iTunes') if self.verbose: - print " %s - %s (already running)" % (self.iTunes.name(), self.iTunes.version()) - - # Check for connected book-capable device - names = [s.name() for s in self.iTunes.sources()] - kinds = [str(s.kind()).rpartition('.')[2] for s in self.iTunes.sources()] - self.sources = sources = dict(zip(kinds,names)) - if 'iPod' in sources: - if self.verbose: - sys.stdout.write('.') - sys.stdout.flush() - return True + print "ITUNES.can_handle(): device not connected" + return False else: - if self.verbose: - print "ITUNES.can_handle(): device not connected" - self.iTunes = None - self.sources = None - return False + # can_handle() is called once before open(), so need to return True + # to keep things going + print "ITUNES:can_handle(): iTunes not yet instantiated" + return True def can_handle_windows(self, device_id, debug=False): ''' @@ -236,7 +223,6 @@ class ITUNES(DevicePlugin): print "ITUNES:delete_books(): Deleting '%s - %s'" % (title, author) self.iTunes.delete(book) - def eject(self): ''' Un-mount / eject the device from the OS. This does not check if there @@ -294,7 +280,22 @@ class ITUNES(DevicePlugin): this function that should serve as a good example for USB Mass storage devices. ''' - print "ITUNES.open()" + if isosx: + # Launch iTunes if not already running + if self.verbose: + print "ITUNES:open(): Instantiating iTunes" + running_apps = appscript.app('System Events') + if not 'iTunes' in running_apps.processes.name(): + if self.verbose: + print "ITUNES:open(): Launching iTunes" + self.iTunes = iTunes= appscript.app('iTunes', hide=True) + iTunes.run() + if self.verbose: + print "%s - %s (launched)" % (self.iTunes.name(), self.iTunes.version()) + else: + self.iTunes = appscript.app('iTunes') + if self.verbose: + print " %s - %s (already running)" % (self.iTunes.name(), self.iTunes.version()) def post_yank_cleanup(self): ''' @@ -373,7 +374,16 @@ class ITUNES(DevicePlugin): @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. """ - print "ITUNES:total_space()" + if self.verbose: + print "ITUNES:total_space()" + capacity = 0 + if isosx: + if 'iPod' in self.sources: + connected_device = self.sources['iPod'] + capacity = self.iTunes.sources[connected_device].capacity() + + return (capacity,-1,-1) + def upload_books(self, files, names, on_card=None, end_session=True, metadata=None): @@ -397,9 +407,6 @@ class ITUNES(DevicePlugin): ''' raise NotImplementedError() - # Private methods - - class BookList(list): ''' A list of books. Each Book object must have the fields: From 546ba96b59d50b803ef45b0964c37d65fddfc6e7 Mon Sep 17 00:00:00 2001 From: GRiker Date: Fri, 28 May 2010 05:11:35 -0600 Subject: [PATCH 07/11] GwR apple driver wip --- src/calibre/devices/apple/driver.py | 384 +++++++++++++++++----------- 1 file changed, 234 insertions(+), 150 deletions(-) diff --git a/src/calibre/devices/apple/driver.py b/src/calibre/devices/apple/driver.py index 69762eaa91..72d1832540 100644 --- a/src/calibre/devices/apple/driver.py +++ b/src/calibre/devices/apple/driver.py @@ -5,29 +5,26 @@ 22 May 2010 ''' -import cStringIO, datetime, os, re, shutil, sys, time +import cStringIO, datetime, os, re, shutil, sys, time, zipfile from calibre import fit_image from calibre.constants import isosx, iswindows from calibre.devices.interface import DevicePlugin from calibre.ebooks.metadata import MetaInformation from calibre.library.server.utils import strftime -from calibre.utils.config import Config +from calibre.utils.config import Config, config_dir from calibre.utils.date import parse_date +from calibre.utils.logging import Log -from PIL import Image as PILImage - +from PIL import Image as PILImage, TarIO if isosx: - print "running in OSX" import appscript, osax if iswindows: - print "running in Windows" import win32com.client class UserInteractionRequired(Exception): - print "UserInteractionRequired() exception" pass class UserFeedback(Exception): @@ -61,20 +58,21 @@ class ITUNES(DevicePlugin): BCD = [0x01] # Properties + add_list = None cached_books = {} + cache_dir = os.path.join(config_dir, 'caches', 'itunes') iTunes= None + log = Log() path_template = 'iTunes/%s - %s.epub' presync = True - purge_list = None + update_list = None sources = None update_msg = None update_needed = False use_thumbnail_as_cover = False - verbose = True - + verbose = False # Public methods - def add_books_to_metadata(self, locations, metadata, booklists): ''' Add locations to the booklists. This function must not communicate with @@ -86,23 +84,26 @@ class ITUNES(DevicePlugin): (L{books}(oncard=None), L{books}(oncard='carda'), L{books}(oncard='cardb')). ''' - print "ITUNES.add_books_to_metadata()" + if self.verbose: + self.log.info( "ITUNES.add_books_to_metadata()") - self._dump_booklist(booklists[0]) # Delete any obsolete copies of the book from the booklist - if self.purge_list: - if self.verbose: - print " purging updated books" - for library_id in self.purge_list: - for i,book in enumerate(booklists[0]): - if book.library_id == library_id: + if self.update_list: + for p_book in self.update_list: + #self.log.info("ITUNES.add_books_to_metadata(): looking for %s" % p_book['lib_book']) + for i,bl_book in enumerate(booklists[0]): + #self.log.info("ITUNES.add_books_to_metadata(): evaluating %s" % bl_book.library_id) + if bl_book.library_id == p_book['lib_book']: booklists[0].pop(i) - self.purge_list = [] + #self.log.info("ITUNES.add_books_to_metadata(): removing %s" % p_book['title']) + break + else: + self.log.error("ITUNES.add_books_to_metadata(): update_list item '%s' not found in booklists[0]" % p_book['title']) + # Add new books to booklists[0] for new_book in locations[0]: booklists[0].append(new_book) - self._dump_booklist(booklists[0]) def books(self, oncard=None, end_session=True): """ @@ -119,7 +120,8 @@ class ITUNES(DevicePlugin): list of device books. """ - print "ITUNES:books(oncard=%s)" % oncard + if self.verbose: + self.log.info("ITUNES:books(oncard=%s)" % oncard) if not oncard: # Fetch a list of books from iPod device connected to iTunes @@ -131,7 +133,7 @@ class ITUNES(DevicePlugin): if 'iPod' in self.sources: device = self.sources['iPod'] if 'Books' in self.iTunes.sources[device].playlists.name(): - booklist = BookList() + booklist = BookList(self.log,self.verbose) cached_books = {} device_books = self._get_device_books() for book in device_books: @@ -142,7 +144,7 @@ class ITUNES(DevicePlugin): this_book.device_collections = [] this_book.library_id = library_books[this_book.path] if this_book.path in library_books else None this_book.size = book.size() - this_book.thumbnail = self._generate_thumbnail(book) + this_book.thumbnail = self._generate_thumbnail(this_book.path, book) booklist.add_book(this_book, False) @@ -152,16 +154,9 @@ class ITUNES(DevicePlugin): 'lib_book':library_books[this_book.path] if this_book.path in library_books else None } - - if self.verbose: - print - print "%-40.40s %-12.12s" % ('Device Books','In Library') - print "%-40.40s %-12.12s" % ('------------','----------') - - for cp in cached_books.keys(): - print "%-40.40s %6.6s" % (cached_books[cp]['title'], 'yes' if cached_books[cp]['lib_book'] else ' no') - print self.cached_books = cached_books + if self.verbose: + self._dump_cached_books() return booklist else: # No books installed on this device @@ -197,12 +192,13 @@ class ITUNES(DevicePlugin): return True else: if self.verbose: - print "ITUNES.can_handle(): device ejected" + self.log.info("ITUNES.can_handle(): device ejected") return False else: # can_handle() is called once before open(), so need to return True # to keep things going - print "ITUNES:can_handle(): iTunes not yet instantiated" + if self.verbose: + self.log.info("ITUNES:can_handle(): iTunes not yet instantiated") return True def can_handle_windows(self, device_id, debug=False): @@ -217,7 +213,8 @@ class ITUNES(DevicePlugin): :param device_info: On windows a device ID string. On Unix a tuple of ``(vendor_id, product_id, bcd)``. ''' - print "ITUNES:can_handle_windows()" + if self.verbose: + self.log.info("ITUNES:can_handle_windows()") return True def card_prefix(self, end_session=True): @@ -230,7 +227,8 @@ class ITUNES(DevicePlugin): ('place', None) (None, None) ''' - print "ITUNES:card_prefix()" + if self.verbose: + self.log.info("ITUNES:card_prefix()") return (None,None) def config_widget(cls): @@ -251,7 +249,7 @@ class ITUNES(DevicePlugin): for path in paths: if self.cached_books[path]['lib_book']: if self.verbose: - print "ITUNES:delete_books(): Deleting '%s' from iTunes library" % (path) + self.log.info("ITUNES:delete_books(): Deleting '%s' from iTunes library" % (path)) self._remove_iTunes_dir(self.cached_books[path]) self.iTunes.delete(self.cached_books[path]['lib_book']) self.update_needed = True @@ -269,7 +267,7 @@ class ITUNES(DevicePlugin): are pending GUI jobs that need to communicate with the device. ''' if self.verbose: - print "ITUNES:eject(): ejecting '%s'" % self.sources['iPod'] + self.log.info("ITUNES:eject(): ejecting '%s'" % self.sources['iPod']) self.iTunes.eject(self.sources['iPod']) self.iTunes = None self.sources = None @@ -284,7 +282,8 @@ class ITUNES(DevicePlugin): @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. """ - print "ITUNES:free_space()" + if self.verbose: + self.log.info("ITUNES:free_space()") free_space = 0 if isosx: @@ -299,7 +298,8 @@ class ITUNES(DevicePlugin): Ask device for device information. See L{DeviceInfoQuery}. @return: (device name, device version, software version on device, mime type) """ - print "ITUNES:get_device_information()" + if self.verbose: + self.log.info("ITUNES:get_device_information()") return ('iPad','hw v1.0','sw v1.0', 'mime type') @@ -325,21 +325,21 @@ class ITUNES(DevicePlugin): if isosx: # Launch iTunes if not already running if self.verbose: - print "ITUNES:open(): Instantiating iTunes" + self.log.info("ITUNES:open(): Instantiating iTunes") # Instantiate iTunes running_apps = appscript.app('System Events') if not 'iTunes' in running_apps.processes.name(): if self.verbose: - print "ITUNES:open(): Launching iTunes" + self.log.info( "ITUNES:open(): Launching iTunes" ) self.iTunes = iTunes= appscript.app('iTunes', hide=True) iTunes.run() if self.verbose: - print "%s - %s (launched)" % (self.iTunes.name(), self.iTunes.version()) + self.log.info( "%s - %s (launched)" % (self.iTunes.name(), self.iTunes.version())) else: self.iTunes = appscript.app('iTunes') if self.verbose: - print " %s - %s (already running)" % (self.iTunes.name(), self.iTunes.version()) + self.log.info( " %s - %s (already running)" % (self.iTunes.name(), self.iTunes.version())) # Init the iTunes source list names = [s.name() for s in self.iTunes.sources()] @@ -353,14 +353,31 @@ class ITUNES(DevicePlugin): pb_count = len(self._get_purchased_book_ids()) if db_count != lb_count + pb_count: if self.verbose: - print "ITUNES.open(): pre-syncing iTunes with device" - print " Library|Books : %d" % len(self._get_library_books()) - print " Devices|iPad|Books : %d" % len(self._get_device_books()) - print " Devices|iPad|Purchased: %d" % len(self._get_purchased_book_ids()) + self.log.info( "ITUNES.open(): pre-syncing iTunes with device") + self.log.info( " Library|Books : %d" % lb_count) + self.log.info( " Devices|iPad|Books : %d" % db_count) + self.log.info( " Devices|iPad|Purchased: %d" % pb_count) self._update_device(msg="Presyncing iTunes with device, mismatched book count") else: if self.verbose: - print "Skipping pre-sync check" + self.log.info( "Skipping pre-sync check") + + # Create thumbs archive + archive_path = os.path.join(self.cache_dir, "thumbs.zip") + + if not os.path.exists(self.cache_dir): + if self.verbose: + self.log.info(" creating thumb cache '%s'" % self.cache_dir) + os.makedirs(self.cache_dir) + + if not os.path.exists(archive_path): + self.log.info(" creating zip archive") + zfw = zipfile.ZipFile(archive_path, mode='w') + zfw.writestr("iTunes Thumbs Archive",'') + zfw.close() + else: + if self.verbose: + self.log.info(" existing thumb cache at '%s'" % archive_path) def post_yank_cleanup(self): ''' @@ -377,22 +394,26 @@ class ITUNES(DevicePlugin): (L{books}(oncard=None), L{books}(oncard='carda'), L{books}(oncard='cardb')). ''' - print "ITUNES.remove_books_from_metadata():" + if self.verbose: + self.log.info("ITUNES.remove_books_from_metadata():") for path in paths: if self.cached_books[path]['lib_book']: # Remove from the booklist for i,book in enumerate(booklists[0]): if book.path == path: - print " removing '%s' from calibre booklist, index: %d" % (path, i) + self.log.info(" removing '%s' from calibre booklist, index: %d" % (path, i)) booklists[0].pop(i) break + else: + self.log.error("ITUNES.remove_books_from_metadata(): '%s' not found in self.cached_book" % path) # Remove from cached_books - print " Removing '%s' from self.cached_books" % path + if self.verbose: + self.log.info("ITUNES.remove_books_from_metadata(): Removing '%s' from self.cached_books" % path) self.cached_books.pop(path) else: - print " skipping purchased book, can't remove via automation interface" + self.log.warning("ITUNES.remove_books_from_metadata(): skipping purchased book, can't remove via automation interface") def reset(self, key='-1', log_packets=False, report_progress=None, detected_device=None) : @@ -405,7 +426,8 @@ class ITUNES(DevicePlugin): task does not have any progress information :detected_device: Device information from the device scanner """ - print "ITUNE.reset()" + if self.verbose: + self.log.info("ITUNE.reset()") def save_settings(cls, settings_widget): ''' @@ -421,17 +443,19 @@ class ITUNES(DevicePlugin): If it is called with -1 that means that the task does not have any progress information ''' - print "ITUNES:set_progress_reporter()" + if self.verbose: + self.log.info("ITUNES:set_progress_reporter()") - def settings(cls): + def settings(self): ''' Should return an opts object. The opts object should have one attribute `format_map` which is an ordered list of formats for the device. ''' - print "ITUNES.settings()" - klass = cls if isinstance(cls, type) else cls.__class__ + if self.verbose: + self.log.info("ITUNES.settings()") + klass = self if isinstance(self, type) else self.__class__ c = Config('device_drivers_%s' % klass.__name__, _('settings for device drivers')) - c.add_opt('format_map', default=cls.FORMATS, + c.add_opt('format_map', default=self.FORMATS, help=_('Ordered list of formats the device will accept')) return c.parse() @@ -442,11 +466,34 @@ class ITUNES(DevicePlugin): (L{books}(oncard=None), L{books}(oncard='carda'), L{books}(oncard='cardb')). ''' - print "ITUNES:sync_booklists():" + if self.verbose: + self.log.info("ITUNES:sync_booklists():") if self.update_needed: self._update_device(msg=self.update_msg) self.update_needed = False + # Get actual size of updated books on device + if self.update_list: + if self.verbose: + self.log.info("ITUNES:sync_booklists(): update_list:") + for ub in self.update_list: + self.log.info(" '%s'" % ub['title']) + + for updated_book in self.update_list: + size_on_device = self._get_device_book_size(updated_book['title'], updated_book['author']) + if size_on_device: + for book in booklists[0]: + if book.title == updated_book['title'] and \ + book.author[0] == updated_book['author']: + book.size = size_on_device + break + else: + self.log.error("ITUNES:sync_booklists(): could not update book size for '%s'" % updated_book['title']) + + else: + self.log.error("ITUNES:sync_booklists(): could not find '%s' on device" % updated_book['title']) + self.update_list = None + def total_space(self, end_session=True): """ Get total space available on the mountpoints: @@ -458,7 +505,7 @@ class ITUNES(DevicePlugin): particular device doesn't have any of these locations it should return 0. """ if self.verbose: - print "ITUNES:total_space()" + self.log.info("ITUNES:total_space()") capacity = 0 if isosx: if 'iPod' in self.sources: @@ -467,7 +514,6 @@ class ITUNES(DevicePlugin): return (capacity,-1,-1) - def upload_books(self, files, names, on_card=None, end_session=True, metadata=None): ''' @@ -488,77 +534,63 @@ class ITUNES(DevicePlugin): be used in preference. The thumbnail attribute is of the form (width, height, cover_data as jpeg). ''' - if False: - print - print "ITUNES.upload_books():" - for file in files: - print " file: %s" % file - print - - print "names:" - for name in names: - print " name: %s" % name - print - - print "metadata:" - print dir(metadata[0]) - for md in metadata: - print " title: %s" % md.title - print " title_sort: %s" % md.title_sort - print " author: %s" % md.author[0] - print " author_sort: %s" % md.author_sort - print " tags: %s" % md.tags - print " rating: %s" % md.rating - print " cover: %s" % md.cover - #print " cover_data: %s" % repr(md.cover_data) - #print "thumbnail: %s" % repr(md.thumbnail) - - print - print - - #print "thumbnail: width: %d height: %d" % (metadata[0].thumbnail[0], metadata[0].thumbnail[1]) - #self._hexdump(metadata[0].thumbnail[2]) new_booklist = [] - self.purge_list = [] + self.update_list = [] + self.add_list = [] if isosx: for (i,file) in enumerate(files): path = self.path_template % (metadata[i].title, metadata[i].author[0]) - # Delete existing from Library|Books, add to self.purge_list + # Delete existing from Library|Books, add to self.update_list # for deletion from booklist[0] during add_books_to_metadata if path in self.cached_books: - self.purge_list.append(self.cached_books[path]) + self.update_list.append(self.cached_books[path]) + self.add_list.append({'title':metadata[i].title,'author':metadata[i].author[0]}) if self.verbose: - print " deleting existing '%s' at\n %s" % (path,self.cached_books[path]['lib_book']) + self.log.info("ITUNES.upload_books():") + self.log.info( " deleting existing '%s'" % (path)) self._remove_iTunes_dir(self.cached_books[path]) self.iTunes.delete(self.cached_books[path]['lib_book']) + else: + self.add_list.append({'title':metadata[i].title,'author':metadata[i].author[0]}) # Add to iTunes Library|Books added = self.iTunes.add(appscript.mactypes.File(files[i])) thumb = None - if self.use_thumbnail_as_cover: - # Use thumbnail data as artwork - added.artworks[1].data_.set(metadata[i].thumbnail[2]) - thumb = metadata[i].thumbnail[2] - else: - # Use cover data as artwork - cover_data = open(metadata[i].cover,'rb') - added.artworks[1].data_.set(cover_data.read()) + try: + if self.use_thumbnail_as_cover: + # Use thumbnail data as artwork + added.artworks[1].data_.set(metadata[i].thumbnail[2]) + thumb = metadata[i].thumbnail[2] + else: + # Use cover data as artwork + cover_data = open(metadata[i].cover,'rb') + added.artworks[1].data_.set(cover_data.read()) - # Resize for thumb - width = metadata[i].thumbnail[0] - height = metadata[i].thumbnail[1] - im = PILImage.open(metadata[i].cover) - im = im.resize((width, height), PILImage.ANTIALIAS) - of = cStringIO.StringIO() - im.convert('RGB').save(of, 'JPEG') - thumb = of.getvalue() + # Resize for thumb + width = metadata[i].thumbnail[0] + height = metadata[i].thumbnail[1] + im = PILImage.open(metadata[i].cover) + im = im.resize((width, height), PILImage.ANTIALIAS) + of = cStringIO.StringIO() + im.convert('RGB').save(of, 'JPEG') + thumb = of.getvalue() + # Cache the thumbnail always, could be updated + if self.verbose: + self.log.info( " refreshing cached thumb for '%s'" % metadata[i].title) + archive_path = os.path.join(self.cache_dir, "thumbs.zip") + zfw = zipfile.ZipFile(archive_path, mode='a') + thumb_path = path.rpartition('.')[0] + '.jpg' + zfw.writestr(thumb_path, thumb) + zfw.close() + except: + self.log.error("ITUNES.upload_books(): error converting '%s' to thumb for '%s'" % (metadata[i].cover,metadata[i].title)) # Create a new Book this_book = Book(metadata[i].title, metadata[i].author[0]) @@ -567,7 +599,7 @@ class ITUNES(DevicePlugin): this_book.device_collections = [] this_book.library_id = added this_book.path = path - this_book.size = added.size() # GwR this is wrong, needs to come from device or fake it + this_book.size = added.size() # Updated later from actual storage size this_book.thumbnail = thumb this_book.iTunes_id = added @@ -578,16 +610,20 @@ class ITUNES(DevicePlugin): added.rating.set(metadata[i].rating*10) added.sort_artist.set(metadata[i].author_sort) added.sort_name.set(this_book.title_sorter) + # Set genre from metadata # iTunes grabs the first dc:subject from the opf metadata, - # But we can manually override - # added.genre.set(metadata[i].tags[0]) + # But we can manually override with first tag starting with alpha + for tag in metadata[i].tags: + if self._is_alpha(tag[0]): + added.genre.set(tag) + break # Add new_book to self.cached_paths self.cached_books[this_book.path] = { 'title': this_book.title, 'author': this_book.author, - 'lib_book': this_book.library_id + 'lib_book': added } @@ -599,15 +635,27 @@ class ITUNES(DevicePlugin): # Private methods def _dump_booklist(self,booklist, header="booklists[0]"): - print - print header - print "%s" % ('-' * len(header)) + ''' + ''' + self.log.info() + self.log.info(header) + self.log.info( "%s" % ('-' * len(header))) for i,book in enumerate(booklist): - print "%2d %-25.25s %s" % (i,book.title, book.library_id) - print + self.log.info( "%2d %-25.25s %s" % (i,book.title, book.library_id)) + self.log.info() + + def _dump_cached_books(self): + ''' + ''' + self.log.info("\n%-40.40s %-12.12s" % ('Device Books','In Library')) + self.log.info("%-40.40s %-12.12s" % ('------------','----------')) + for cb in self.cached_books.keys(): + self.log.info("%-40.40s %6.6s" % (self.cached_books[cb]['title'], 'yes' if self.cached_books[cb]['lib_book'] else ' no')) + self.log.info("\n") def _hexdump(self, src, length=16): - # Diagnostic + ''' + ''' FILTER=''.join([(len(repr(chr(x)))==3) and chr(x) or '.' for x in range(256)]) N=0; result='' while src: @@ -619,6 +667,8 @@ class ITUNES(DevicePlugin): print result def _get_library_books(self): + ''' + ''' lib = self.iTunes.sources['library'] library_books = {} if 'Books' in lib.playlists.name(): @@ -628,30 +678,48 @@ class ITUNES(DevicePlugin): library_books[path] = book return library_books + def _get_device_book_size(self, title, author): + ''' + Fetch the size of a book stored on the device + ''' + device_books = self._get_device_books() + for d_book in device_books: + if d_book.name() == title and d_book.artist() == author: + return d_book.size() + else: + self.log.error("ITUNES._get_device_book_size(): could not find '%s' by '%s' in device_books" % (title,author)) + return None + def _get_device_books(self): + ''' + ''' if 'iPod' in self.sources: device = self.sources['iPod'] device_books = [] if 'Books' in self.iTunes.sources[device].playlists.name(): return self.iTunes.sources[device].playlists['Books'].file_tracks() - def _generate_thumbnail(self, book): + def _generate_thumbnail(self, book_path, book): ''' Convert iTunes artwork to thumbnail Cache generated thumbnails + cache_dir = os.path.join(config_dir, 'caches', 'itunes') ''' - print "ITUNES._generate_thumbnail()" + archive_path = os.path.join(self.cache_dir, "thumbs.zip") + thumb_path = book_path.rpartition('.')[0] + '.jpg' try: - n = len(book.artworks()) - print "Library '%s' has %d artwork items" % (book.name(),n) -# for art in book.artworks(): -# print "description: %s" % art.description() -# if str(art.description()) == 'calibre_thumb': -# print "using cached thumb" -# return art.raw_data().data - + zfr = zipfile.ZipFile(archive_path) + thumb_data = zfr.read(thumb_path) + zfr.close() + except: + zfw = zipfile.ZipFile(archive_path, mode='a') + else: + if self.verbose: + self.log.info("ITUNES._generate_thumbnail(): cached thumb found for '%s'" % book.name()) + return thumb_data + try: # Resize the cover data = book.artworks[1].raw_data().data #self._hexdump(data[:256]) @@ -662,20 +730,32 @@ class ITUNES(DevicePlugin): im.convert('RGB').save(thumb,'JPEG') # Cache the tagged thumb -# print "caching thumb" -# book.artworks[n+1].data_.set(thumb.getvalue()) -# book.artworks[n+1].description.set(u'calibre_thumb') + if self.verbose: + self.log.info("ITUNES._generate_thumbnail(): generated thumb for '%s', caching" % book.name()) + zfw.writestr(thumb_path, thumb.getvalue()) + zfw.close() return thumb.getvalue() except: - print "Can't generate thumb for '%s'" % book.name() + self.log.error("ITUNES._generate_thumbnail(): error generating thumb for '%s'" % book.name()) return None def _get_purchased_book_ids(self): + ''' + ''' if 'iPod' in self.sources: device = self.sources['iPod'] - purchased_book_ids = [] if 'Purchased' in self.iTunes.sources[device].playlists.name(): return [pb.database_ID() for pb in self.iTunes.sources[device].playlists['Purchased'].file_tracks()] + else: + return [] + + def _is_alpha(self,char): + ''' + ''' + if not re.search('[a-zA-Z]',char): + return False + else: + return True def _remove_iTunes_dir(self, cached_book): ''' @@ -683,29 +763,29 @@ class ITUNES(DevicePlugin): ''' storage_path = os.path.split(cached_book['lib_book'].location().path) if self.verbose: - print "ITUNES._remove_iTunes_dir():" - print " removing storage_path: %s" % storage_path[0] + self.log.info( "ITUNES._remove_iTunes_dir():") + self.log.info( " removing storage_path: %s" % storage_path[0]) shutil.rmtree(storage_path[0]) - def _update_device(self, msg='', wait=True): ''' - This probably needs a job spinner + ''' if self.verbose: - print "ITUNES:_update_device(): %s" % msg + self.log.info("ITUNES:_update_device(): %s" % msg) self.iTunes.update() if wait: # This works if iTunes has books not yet synced to iPad. - print "Waiting for iPad sync to complete ...", + if self.verbose: + self.log.info("Waiting for iPad sync to complete ...",) while len(self._get_device_books()) != (len(self._get_library_books()) + len(self._get_purchased_book_ids())): - sys.stdout.write('.') - sys.stdout.flush() + if self.verbose: + sys.stdout.write('.') + sys.stdout.flush() time.sleep(2) print - class BookList(list): ''' A list of books. Each Book object must have the fields: @@ -722,9 +802,12 @@ class BookList(list): __getslice__ = None __setslice__ = None + log = None + verbose = False - def __init__(self): - pass + def __init__(self, log, verbose=False): + self.log = log + self.verbose = verbose def supports_collections(self): ''' Return True if the the device supports collections for this book list. ''' @@ -735,7 +818,8 @@ class BookList(list): Add the book to the booklist. Intent is to maintain any device-internal metadata. Return True if booklists must be sync'ed ''' - print "adding %s" % book + if self.verbose: + self.log.info("BookList.add_book(): adding %s" % book) self.append(book) def remove_book(self, book): From 8a6faa8b753c4eb8cc25d44d08e995e848d792b5 Mon Sep 17 00:00:00 2001 From: GRiker Date: Fri, 28 May 2010 07:20:28 -0600 Subject: [PATCH 08/11] GwR apple driver wip --- src/calibre/devices/apple/driver.py | 59 ++++++++++++++++++----------- 1 file changed, 36 insertions(+), 23 deletions(-) diff --git a/src/calibre/devices/apple/driver.py b/src/calibre/devices/apple/driver.py index 44f5e5a3e5..46491695bb 100644 --- a/src/calibre/devices/apple/driver.py +++ b/src/calibre/devices/apple/driver.py @@ -22,7 +22,6 @@ if isosx: import appscript, osax if iswindows: - print "ITUNES: Running under windows" import win32com.client class UserInteractionRequired(Exception): @@ -59,19 +58,18 @@ class ITUNES(DevicePlugin): BCD = [0x01] # Properties - add_list = None cached_books = {} cache_dir = os.path.join(config_dir, 'caches', 'itunes') iTunes= None log = Log() path_template = 'iTunes/%s - %s.epub' - presync = True + presync = False update_list = None sources = None update_msg = None update_needed = False use_thumbnail_as_cover = False - verbose = False + verbose = True # Public methods def add_books_to_metadata(self, locations, metadata, booklists): @@ -183,17 +181,22 @@ class ITUNES(DevicePlugin): if isosx: if self.iTunes: # Check for connected book-capable device - names = [s.name() for s in self.iTunes.sources()] - kinds = [str(s.kind()).rpartition('.')[2] for s in self.iTunes.sources()] - self.sources = sources = dict(zip(kinds,names)) - if 'iPod' in sources: - if self.verbose: - sys.stdout.write('.') - sys.stdout.flush() - return True - else: - if self.verbose: - self.log.info("ITUNES.can_handle(): device ejected") + try: + names = [s.name() for s in self.iTunes.sources()] + kinds = [str(s.kind()).rpartition('.')[2] for s in self.iTunes.sources()] + self.sources = sources = dict(zip(kinds,names)) + if 'iPod' in sources: + if self.verbose: + sys.stdout.write('.') + sys.stdout.flush() + return True + else: + if self.verbose: + self.log.info("ITUNES.can_handle(): device ejected") + return False + except: + # iTunes connection failed, probably not running anymore + self.log.error("ITUNES.can_handle(): lost connection to iTunes") return False else: # can_handle() is called once before open(), so need to return True @@ -214,9 +217,16 @@ class ITUNES(DevicePlugin): :param device_info: On windows a device ID string. On Unix a tuple of ``(vendor_id, product_id, bcd)``. ''' - if self.verbose: - self.log.info("ITUNES:can_handle_windows()") - return True + + if self.iTunes: + sys.exit(1) + + else: + # can_handle() is called once before open(), so need to return True + # to keep things going + if self.verbose: + self.log.info("ITUNES:can_handle(): iTunes not yet instantiated") + return True def card_prefix(self, end_session=True): ''' @@ -309,7 +319,9 @@ class ITUNES(DevicePlugin): 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 ''' - raise NotImplementedError() + if self.verbose: + self.log.info("ITUNES.get_file(): exporting '%s'" % path) + outfile.write(open(self.cached_books[path]['lib_book'].location().path).read()) def open(self): ''' @@ -380,6 +392,11 @@ class ITUNES(DevicePlugin): if self.verbose: self.log.info(" existing thumb cache at '%s'" % archive_path) + if iswindows: + # Launch iTunes if not already running + if self.verbose: + self.log.info("ITUNES:open(): Instantiating iTunes") + def post_yank_cleanup(self): ''' Called if the user yanks the device without ejecting it first. @@ -538,7 +555,6 @@ class ITUNES(DevicePlugin): new_booklist = [] self.update_list = [] - self.add_list = [] if isosx: @@ -549,15 +565,12 @@ class ITUNES(DevicePlugin): # for deletion from booklist[0] during add_books_to_metadata if path in self.cached_books: self.update_list.append(self.cached_books[path]) - self.add_list.append({'title':metadata[i].title,'author':metadata[i].author[0]}) if self.verbose: self.log.info("ITUNES.upload_books():") self.log.info( " deleting existing '%s'" % (path)) self._remove_iTunes_dir(self.cached_books[path]) self.iTunes.delete(self.cached_books[path]['lib_book']) - else: - self.add_list.append({'title':metadata[i].title,'author':metadata[i].author[0]}) # Add to iTunes Library|Books added = self.iTunes.add(appscript.mactypes.File(files[i])) From ef82b34381edb0544f623438f6a721f6e9264129 Mon Sep 17 00:00:00 2001 From: GRiker Date: Fri, 28 May 2010 10:07:41 -0600 Subject: [PATCH 09/11] GwR PersistentTemporaryFile fix --- src/calibre/devices/apple/driver.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/calibre/devices/apple/driver.py b/src/calibre/devices/apple/driver.py index 46491695bb..5653e4dfba 100644 --- a/src/calibre/devices/apple/driver.py +++ b/src/calibre/devices/apple/driver.py @@ -5,13 +5,14 @@ 22 May 2010 ''' -import cStringIO, datetime, os, re, shutil, sys, time, zipfile +import atexit, cStringIO, datetime, os, re, shutil, sys, time, zipfile from calibre import fit_image from calibre.constants import isosx, iswindows from calibre.devices.interface import DevicePlugin from calibre.ebooks.metadata import MetaInformation from calibre.library.server.utils import strftime +from calibre.ptempfile import PersistentTemporaryFile, cleanup from calibre.utils.config import Config, config_dir from calibre.utils.date import parse_date from calibre.utils.logging import Log @@ -559,6 +560,7 @@ class ITUNES(DevicePlugin): if isosx: for (i,file) in enumerate(files): + path = self.path_template % (metadata[i].title, metadata[i].author[0]) # Delete existing from Library|Books, add to self.update_list @@ -573,7 +575,10 @@ class ITUNES(DevicePlugin): self.iTunes.delete(self.cached_books[path]['lib_book']) # Add to iTunes Library|Books - added = self.iTunes.add(appscript.mactypes.File(files[i])) + if isinstance(file,PersistentTemporaryFile): + added = self.iTunes.add(appscript.mactypes.File(file._name)) + else: + added = self.iTunes.add(appscript.mactypes.File(file)) thumb = None try: @@ -621,8 +626,9 @@ class ITUNES(DevicePlugin): # Flesh out the iTunes metadata added.comment.set("added by calibre %s" % strftime('%Y-%m-%d %H:%M:%S')) - added.rating.set(metadata[i].rating*10) - added.sort_artist.set(metadata[i].author_sort) + if metadata[i].rating: + added.rating.set(metadata[i].rating*10) + added.sort_artist.set(metadata[i].author_sort.title()) added.sort_name.set(this_book.title_sorter) # Set genre from metadata @@ -696,8 +702,13 @@ class ITUNES(DevicePlugin): ''' Fetch the size of a book stored on the device ''' + if self.verbose: + self.log.info("ITUNES._get_device_book_size(): looking for title: '%s' author: %s" % (title,author)) + device_books = self._get_device_books() for d_book in device_books: + if self.verbose: + self.log.info(" evaluating title: '%s' author: '%s'" % (d_book.name(), d_book.artist())) if d_book.name() == title and d_book.artist() == author: return d_book.size() else: From 8ff15994b82fba5941d0319bd0763bb3d54498dc Mon Sep 17 00:00:00 2001 From: GRiker Date: Fri, 28 May 2010 10:09:05 -0600 Subject: [PATCH 10/11] GwR PersistentTemporaryFile fix --- src/calibre/devices/apple/driver.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/calibre/devices/apple/driver.py b/src/calibre/devices/apple/driver.py index 5653e4dfba..8a81223226 100644 --- a/src/calibre/devices/apple/driver.py +++ b/src/calibre/devices/apple/driver.py @@ -55,7 +55,8 @@ class ITUNES(DevicePlugin): # Product IDs: # 0x129a:iPad # 0x1292:iPhone 3G - PRODUCT_ID = [0x129a,0x1292] + #PRODUCT_ID = [0x129a,0x1292] + PRODUCT_ID = [0x129a] BCD = [0x01] # Properties From 2ec3e3ab1356cb09652d8d254596940e162218f3 Mon Sep 17 00:00:00 2001 From: GRiker Date: Fri, 28 May 2010 13:57:07 -0600 Subject: [PATCH 11/11] GwR beta 0.1 --- src/calibre/devices/apple/driver.py | 146 +++++++++++++--------------- 1 file changed, 67 insertions(+), 79 deletions(-) diff --git a/src/calibre/devices/apple/driver.py b/src/calibre/devices/apple/driver.py index 8a81223226..88c262a4d9 100644 --- a/src/calibre/devices/apple/driver.py +++ b/src/calibre/devices/apple/driver.py @@ -7,6 +7,7 @@ ''' import atexit, cStringIO, datetime, os, re, shutil, sys, time, zipfile +from calibre.constants import DEBUG from calibre import fit_image from calibre.constants import isosx, iswindows from calibre.devices.interface import DevicePlugin @@ -46,6 +47,7 @@ class ITUNES(DevicePlugin): description = _('Communicate with iBooks through iTunes.') supported_platforms = ['windows','osx'] author = 'GRiker' + driver_version = '0.1' OPEN_FEEDBACK_MESSAGE = _('Apple device detected, launching iTunes') @@ -66,12 +68,11 @@ class ITUNES(DevicePlugin): log = Log() path_template = 'iTunes/%s - %s.epub' presync = False - update_list = None + update_list = [] sources = None update_msg = None update_needed = False use_thumbnail_as_cover = False - verbose = True # Public methods def add_books_to_metadata(self, locations, metadata, booklists): @@ -85,12 +86,14 @@ class ITUNES(DevicePlugin): (L{books}(oncard=None), L{books}(oncard='carda'), L{books}(oncard='cardb')). ''' - if self.verbose: + if DEBUG: self.log.info( "ITUNES.add_books_to_metadata()") + task_count = float(len(self.update_list)) + # Delete any obsolete copies of the book from the booklist if self.update_list: - for p_book in self.update_list: + for (j,p_book) in enumerate(self.update_list): #self.log.info("ITUNES.add_books_to_metadata(): looking for %s" % p_book['lib_book']) for i,bl_book in enumerate(booklists[0]): #self.log.info("ITUNES.add_books_to_metadata(): evaluating %s" % bl_book.library_id) @@ -101,6 +104,8 @@ class ITUNES(DevicePlugin): else: self.log.error("ITUNES.add_books_to_metadata(): update_list item '%s' not found in booklists[0]" % p_book['title']) + self.report_progress(j+1/task_count, _('Updating device metadata listing...')) + self.report_progress(1.0, _('Updating device metadata listing...')) # Add new books to booklists[0] for new_book in locations[0]: @@ -121,7 +126,7 @@ class ITUNES(DevicePlugin): list of device books. """ - if self.verbose: + if DEBUG: self.log.info("ITUNES:books(oncard=%s)" % oncard) if not oncard: @@ -134,10 +139,11 @@ class ITUNES(DevicePlugin): if 'iPod' in self.sources: device = self.sources['iPod'] if 'Books' in self.iTunes.sources[device].playlists.name(): - booklist = BookList(self.log,self.verbose) + booklist = BookList(self.log) cached_books = {} device_books = self._get_device_books() - for book in device_books: + book_count = float(len(device_books)) + for (i,book) in enumerate(device_books): this_book = Book(book.name(), book.artist()) this_book.path = self.path_template % (book.name(), book.artist()) this_book.datetime = parse_date(str(book.date_added())).timetuple() @@ -155,8 +161,11 @@ class ITUNES(DevicePlugin): 'lib_book':library_books[this_book.path] if this_book.path in library_books else None } + self.report_progress(i+1/book_count, _('%d of %d' % (i+1, book_count))) + + self.report_progress(1.0, _('finished')) self.cached_books = cached_books - if self.verbose: + if DEBUG: self._dump_cached_books() return booklist else: @@ -188,12 +197,12 @@ class ITUNES(DevicePlugin): kinds = [str(s.kind()).rpartition('.')[2] for s in self.iTunes.sources()] self.sources = sources = dict(zip(kinds,names)) if 'iPod' in sources: - if self.verbose: + if DEBUG: sys.stdout.write('.') sys.stdout.flush() return True else: - if self.verbose: + if DEBUG: self.log.info("ITUNES.can_handle(): device ejected") return False except: @@ -203,7 +212,7 @@ class ITUNES(DevicePlugin): else: # can_handle() is called once before open(), so need to return True # to keep things going - if self.verbose: + if DEBUG: self.log.info("ITUNES:can_handle(): iTunes not yet instantiated") return True @@ -220,15 +229,7 @@ class ITUNES(DevicePlugin): ``(vendor_id, product_id, bcd)``. ''' - if self.iTunes: - sys.exit(1) - - else: - # can_handle() is called once before open(), so need to return True - # to keep things going - if self.verbose: - self.log.info("ITUNES:can_handle(): iTunes not yet instantiated") - return True + return False def card_prefix(self, end_session=True): ''' @@ -240,16 +241,10 @@ class ITUNES(DevicePlugin): ('place', None) (None, None) ''' - if self.verbose: + if DEBUG: self.log.info("ITUNES:card_prefix()") return (None,None) - def config_widget(cls): - ''' - Should return a QWidget. The QWidget contains the settings for the device interface - ''' - raise NotImplementedError() - def delete_books(self, paths, end_session=True): ''' Delete books at paths on device. @@ -261,7 +256,7 @@ class ITUNES(DevicePlugin): undeletable_titles = [] for path in paths: if self.cached_books[path]['lib_book']: - if self.verbose: + if DEBUG: self.log.info("ITUNES:delete_books(): Deleting '%s' from iTunes library" % (path)) self._remove_iTunes_dir(self.cached_books[path]) self.iTunes.delete(self.cached_books[path]['lib_book']) @@ -279,7 +274,7 @@ class ITUNES(DevicePlugin): 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 self.verbose: + if DEBUG: self.log.info("ITUNES:eject(): ejecting '%s'" % self.sources['iPod']) self.iTunes.eject(self.sources['iPod']) self.iTunes = None @@ -295,7 +290,7 @@ class ITUNES(DevicePlugin): @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 self.verbose: + if DEBUG: self.log.info("ITUNES:free_space()") free_space = 0 @@ -311,7 +306,7 @@ class ITUNES(DevicePlugin): Ask device for device information. See L{DeviceInfoQuery}. @return: (device name, device version, software version on device, mime type) """ - if self.verbose: + if DEBUG: self.log.info("ITUNES:get_device_information()") return ('iPad','hw v1.0','sw v1.0', 'mime type') @@ -321,7 +316,7 @@ class ITUNES(DevicePlugin): 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 self.verbose: + if DEBUG: self.log.info("ITUNES.get_file(): exporting '%s'" % path) outfile.write(open(self.cached_books[path]['lib_book'].location().path).read()) @@ -339,22 +334,24 @@ class ITUNES(DevicePlugin): if isosx: # Launch iTunes if not already running - if self.verbose: + if DEBUG: self.log.info("ITUNES:open(): Instantiating iTunes") # Instantiate iTunes running_apps = appscript.app('System Events') if not 'iTunes' in running_apps.processes.name(): - if self.verbose: + if DEBUG: self.log.info( "ITUNES:open(): Launching iTunes" ) self.iTunes = iTunes= appscript.app('iTunes', hide=True) iTunes.run() - if self.verbose: - self.log.info( "%s - %s (launched)" % (self.iTunes.name(), self.iTunes.version())) + if DEBUG: + self.log.info( "%s - %s (launched), driver version %s" % \ + (self.iTunes.name(), self.iTunes.version(), self.driver_version)) else: self.iTunes = appscript.app('iTunes') - if self.verbose: - self.log.info( " %s - %s (already running)" % (self.iTunes.name(), self.iTunes.version())) + if DEBUG: + self.log.info( " %s - %s (already running), driver version %s" % \ + (self.iTunes.name(), self.iTunes.version(), self.driver_version)) # Init the iTunes source list names = [s.name() for s in self.iTunes.sources()] @@ -367,21 +364,21 @@ class ITUNES(DevicePlugin): db_count = len(self._get_device_books()) pb_count = len(self._get_purchased_book_ids()) if db_count != lb_count + pb_count: - if self.verbose: + if DEBUG: self.log.info( "ITUNES.open(): pre-syncing iTunes with device") self.log.info( " Library|Books : %d" % lb_count) self.log.info( " Devices|iPad|Books : %d" % db_count) self.log.info( " Devices|iPad|Purchased: %d" % pb_count) self._update_device(msg="Presyncing iTunes with device, mismatched book count") else: - if self.verbose: + if DEBUG: self.log.info( "Skipping pre-sync check") # Create thumbs archive archive_path = os.path.join(self.cache_dir, "thumbs.zip") if not os.path.exists(self.cache_dir): - if self.verbose: + if DEBUG: self.log.info(" creating thumb cache '%s'" % self.cache_dir) os.makedirs(self.cache_dir) @@ -391,20 +388,14 @@ class ITUNES(DevicePlugin): zfw.writestr("iTunes Thumbs Archive",'') zfw.close() else: - if self.verbose: + if DEBUG: self.log.info(" existing thumb cache at '%s'" % archive_path) if iswindows: # Launch iTunes if not already running - if self.verbose: + if DEBUG: self.log.info("ITUNES:open(): Instantiating iTunes") - def post_yank_cleanup(self): - ''' - Called if the user yanks the device without ejecting it first. - ''' - raise NotImplementedError() - def remove_books_from_metadata(self, paths, booklists): ''' Remove books from the metadata list. This function must not communicate @@ -414,7 +405,7 @@ class ITUNES(DevicePlugin): (L{books}(oncard=None), L{books}(oncard='carda'), L{books}(oncard='cardb')). ''' - if self.verbose: + if DEBUG: self.log.info("ITUNES.remove_books_from_metadata():") for path in paths: if self.cached_books[path]['lib_book']: @@ -428,7 +419,7 @@ class ITUNES(DevicePlugin): self.log.error("ITUNES.remove_books_from_metadata(): '%s' not found in self.cached_book" % path) # Remove from cached_books - if self.verbose: + if DEBUG: self.log.info("ITUNES.remove_books_from_metadata(): Removing '%s' from self.cached_books" % path) self.cached_books.pop(path) @@ -446,16 +437,9 @@ class ITUNES(DevicePlugin): task does not have any progress information :detected_device: Device information from the device scanner """ - if self.verbose: + if DEBUG: self.log.info("ITUNE.reset()") - def save_settings(cls, settings_widget): - ''' - Should save settings to disk. Takes the widget created in config_widget - and saves all settings to disk. - ''' - raise NotImplementedError() - def set_progress_reporter(self, report_progress): ''' @param report_progress: Function that is called with a % progress @@ -463,15 +447,16 @@ class ITUNES(DevicePlugin): If it is called with -1 that means that the task does not have any progress information ''' - if self.verbose: + if DEBUG: self.log.info("ITUNES:set_progress_reporter()") + self.report_progress = report_progress def settings(self): ''' Should return an opts object. The opts object should have one attribute `format_map` which is an ordered list of formats for the device. ''' - if self.verbose: + if DEBUG: self.log.info("ITUNES.settings()") klass = self if isinstance(self, type) else self.__class__ c = Config('device_drivers_%s' % klass.__name__, _('settings for device drivers')) @@ -486,7 +471,7 @@ class ITUNES(DevicePlugin): (L{books}(oncard=None), L{books}(oncard='carda'), L{books}(oncard='cardb')). ''' - if self.verbose: + if DEBUG: self.log.info("ITUNES:sync_booklists():") if self.update_needed: self._update_device(msg=self.update_msg) @@ -494,7 +479,7 @@ class ITUNES(DevicePlugin): # Get actual size of updated books on device if self.update_list: - if self.verbose: + if DEBUG: self.log.info("ITUNES:sync_booklists(): update_list:") for ub in self.update_list: self.log.info(" '%s'" % ub['title']) @@ -512,7 +497,7 @@ class ITUNES(DevicePlugin): else: self.log.error("ITUNES:sync_booklists(): could not find '%s' on device" % updated_book['title']) - self.update_list = None + self.update_list = [] def total_space(self, end_session=True): """ @@ -524,7 +509,7 @@ class ITUNES(DevicePlugin): @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 self.verbose: + if DEBUG: self.log.info("ITUNES:total_space()") capacity = 0 if isosx: @@ -560,6 +545,7 @@ class ITUNES(DevicePlugin): if isosx: + file_count = float(len(files)) for (i,file) in enumerate(files): path = self.path_template % (metadata[i].title, metadata[i].author[0]) @@ -569,7 +555,7 @@ class ITUNES(DevicePlugin): if path in self.cached_books: self.update_list.append(self.cached_books[path]) - if self.verbose: + if DEBUG: self.log.info("ITUNES.upload_books():") self.log.info( " deleting existing '%s'" % (path)) self._remove_iTunes_dir(self.cached_books[path]) @@ -602,7 +588,7 @@ class ITUNES(DevicePlugin): thumb = of.getvalue() # Cache the thumbnail always, could be updated - if self.verbose: + if DEBUG: self.log.info( " refreshing cached thumb for '%s'" % metadata[i].title) archive_path = os.path.join(self.cache_dir, "thumbs.zip") zfw = zipfile.ZipFile(archive_path, mode='a') @@ -647,6 +633,10 @@ class ITUNES(DevicePlugin): 'lib_book': added } + # Report progress + self.report_progress(i+1/file_count, _('%d of %d' % (i+1, file_count))) + + self.report_progress(1.0, _('finished')) # Tell sync_booklists we need a re-sync self.update_needed = True @@ -703,12 +693,12 @@ class ITUNES(DevicePlugin): ''' Fetch the size of a book stored on the device ''' - if self.verbose: + if DEBUG: self.log.info("ITUNES._get_device_book_size(): looking for title: '%s' author: %s" % (title,author)) device_books = self._get_device_books() for d_book in device_books: - if self.verbose: + if DEBUG: self.log.info(" evaluating title: '%s' author: '%s'" % (d_book.name(), d_book.artist())) if d_book.name() == title and d_book.artist() == author: return d_book.size() @@ -741,7 +731,7 @@ class ITUNES(DevicePlugin): except: zfw = zipfile.ZipFile(archive_path, mode='a') else: - if self.verbose: + if DEBUG: self.log.info("ITUNES._generate_thumbnail(): cached thumb found for '%s'" % book.name()) return thumb_data @@ -756,7 +746,7 @@ class ITUNES(DevicePlugin): im.convert('RGB').save(thumb,'JPEG') # Cache the tagged thumb - if self.verbose: + if DEBUG: self.log.info("ITUNES._generate_thumbnail(): generated thumb for '%s', caching" % book.name()) zfw.writestr(thumb_path, thumb.getvalue()) zfw.close() @@ -788,7 +778,7 @@ class ITUNES(DevicePlugin): iTunes does not delete books from storage when removing from database ''' storage_path = os.path.split(cached_book['lib_book'].location().path) - if self.verbose: + if DEBUG: self.log.info( "ITUNES._remove_iTunes_dir():") self.log.info( " removing storage_path: %s" % storage_path[0]) shutil.rmtree(storage_path[0]) @@ -797,16 +787,16 @@ class ITUNES(DevicePlugin): ''' ''' - if self.verbose: + if DEBUG: self.log.info("ITUNES:_update_device(): %s" % msg) self.iTunes.update() if wait: # This works if iTunes has books not yet synced to iPad. - if self.verbose: + if DEBUG: self.log.info("Waiting for iPad sync to complete ...",) while len(self._get_device_books()) != (len(self._get_library_books()) + len(self._get_purchased_book_ids())): - if self.verbose: + if DEBUG: sys.stdout.write('.') sys.stdout.flush() time.sleep(2) @@ -829,11 +819,9 @@ class BookList(list): __getslice__ = None __setslice__ = None log = None - verbose = False - def __init__(self, log, verbose=False): + def __init__(self, log): self.log = log - self.verbose = verbose def supports_collections(self): ''' Return True if the the device supports collections for this book list. ''' @@ -844,7 +832,7 @@ class BookList(list): Add the book to the booklist. Intent is to maintain any device-internal metadata. Return True if booklists must be sync'ed ''' - if self.verbose: + if DEBUG: self.log.info("BookList.add_book(): adding %s" % book) self.append(book)