From 5c316d4d612823dffec58b407f6af3f9e391948f Mon Sep 17 00:00:00 2001 From: GRiker Date: Wed, 9 Jun 2010 12:44:17 -0600 Subject: [PATCH] GwR wip 0.5.0 --- src/calibre/devices/apple/driver.py | 436 ++++++++++++++++++---------- 1 file changed, 289 insertions(+), 147 deletions(-) diff --git a/src/calibre/devices/apple/driver.py b/src/calibre/devices/apple/driver.py index 335b4abf8a..1b4bb8e408 100644 --- a/src/calibre/devices/apple/driver.py +++ b/src/calibre/devices/apple/driver.py @@ -5,7 +5,7 @@ __copyright__ = '2010, Gregory Riker' __docformat__ = 'restructuredtext en' -import cStringIO, os, re, shutil, subprocess, sys, tempfile, time, zipfile +import cStringIO, ctypes, os, re, shutil, subprocess, sys, tempfile, time, zipfile from calibre.constants import DEBUG from calibre import fit_image @@ -14,6 +14,7 @@ from calibre.devices.interface import DevicePlugin from calibre.ebooks.BeautifulSoup import BeautifulSoup from calibre.ebooks.metadata import MetaInformation from calibre.library.server.utils import strftime +from calibre.ptempfile import PersistentTemporaryFile from calibre.utils.config import Config, config_dir from calibre.utils.date import parse_date from calibre.utils.logging import Log @@ -47,7 +48,7 @@ class ITUNES(DevicePlugin): supported_platforms = ['osx','windows'] author = 'GRiker' #: The version of this plugin as a 3-tuple (major, minor, revision) - version = (0, 4, 11) + version = (0, 5, 0) OPEN_FEEDBACK_MESSAGE = _( 'Apple device detected, launching iTunes, please wait ...') @@ -101,6 +102,15 @@ class ITUNES(DevicePlugin): 'Books', ] + SearchField = [ + 'All', + 'Visible', + 'Artists', + 'Albums', + 'Composers', + 'SongNames', + ] + # Properties cached_books = {} cache_dir = os.path.join(config_dir, 'caches', 'itunes') @@ -108,6 +118,7 @@ class ITUNES(DevicePlugin): iTunes= None iTunes_media = None log = Log() + manual_sync_mode = False path_template = 'iTunes/%s - %s.epub' problem_titles = [] problem_msg = None @@ -182,9 +193,6 @@ class ITUNES(DevicePlugin): (new_book.title, new_book.author)) booklists[0].append(new_book) -# if DEBUG: -# self._dump_booklist(booklists[0],'after add_books_to_metadata()') - def books(self, oncard=None, end_session=True): """ Return a list of ebooks on the device. @@ -210,7 +218,6 @@ class ITUNES(DevicePlugin): library_books = self._get_library_books() if 'iPod' in self.sources: - #device = self.sources['iPod'] booklist = BookList(self.log) cached_books = {} @@ -238,7 +245,8 @@ class ITUNES(DevicePlugin): cached_books[this_book.path] = { 'title':book.name(), 'author':[book.artist()], - 'lib_book':library_books[this_book.path] if this_book.path in library_books else None + 'lib_book':library_books[this_book.path] if this_book.path in library_books else None, + 'dev_book':book } if self.report_progress is not None: @@ -342,7 +350,7 @@ class ITUNES(DevicePlugin): self.log.warning(" waiting for identified iPad, attempt #%d" % (10 - attempts)) else: if DEBUG: - self.log.info(' found connected iPad in iTunes') + self.log.info(' found connected iPad') break else: # iTunes running, but not connected iPad @@ -350,9 +358,8 @@ class ITUNES(DevicePlugin): self.log.info(' self.ejected = True') self.ejected = True return False - else: - self.log.info(' found connected iPad in sources') + self._discover_manual_sync_mode() return True def can_handle_windows(self, device_id, debug=False): @@ -384,6 +391,7 @@ class ITUNES(DevicePlugin): if DEBUG: self.log.info('ITUNES.can_handle_windows:\n confirming connected iPad') self.ejected = False + self._discover_manual_sync_mode() return True else: if DEBUG: @@ -399,9 +407,6 @@ class ITUNES(DevicePlugin): pythoncom.CoUninitialize() else: - # This is called at entry - # We need to know if iTunes sees the iPad - # It may have been ejected if DEBUG: self.log.info("ITUNES:can_handle_windows():\n Launching iTunes") @@ -429,8 +434,10 @@ class ITUNES(DevicePlugin): self.log.info(' self.ejected = True') self.ejected = True return False - else: - self.log.info(' found connected iPad in sources') + + self.log.info(' found connected iPad in sources') + self._discover_manual_sync_mode(wait=1.0) + finally: pythoncom.CoUninitialize() @@ -460,23 +467,31 @@ class ITUNES(DevicePlugin): self.problem_msg = _("Some books not found in iTunes database.\n" "Delete using the iBooks app.\n" "Click 'Show Details' for a list.") + self.log.info("ITUNES:delete_books()") for path in paths: if self.cached_books[path]['lib_book']: if DEBUG: - self.log.info("ITUNES:delete_books(): Deleting '%s' from iTunes library" % (path)) + self.log.info(" Deleting '%s' from iTunes library" % (path)) if isosx: self._remove_from_iTunes(self.cached_books[path]) + if self.manual_sync_mode: + self._remove_device_book(self.cached_books[path]) elif iswindows: try: pythoncom.CoInitialize() self.iTunes = win32com.client.Dispatch("iTunes.Application") self._remove_from_iTunes(self.cached_books[path]) + if self.manual_sync_mode: + self._remove_device_book(self.cached_books[path]) finally: pythoncom.CoUninitialize() - self.update_needed = True - self.update_msg = "Deleted books from device" + if not self.manual_sync_mode: + self.update_needed = True + self.update_msg = "Deleted books from device" + else: + self.log.info(" skipping sync phase, manual_sync_mode: True") else: self.problem_titles.append("'%s' by %s" % (self.cached_books[path]['title'],self.cached_books[path]['author'])) @@ -618,7 +633,7 @@ class ITUNES(DevicePlugin): # Remove from cached_books self.cached_books.pop(path) if DEBUG: - self.log.info(" Removing '%s' from self.cached_books" % path) + self.log.info(" removing '%s' from self.cached_books" % path) # self._dump_cached_books('remove_books_from_metadata()') else: self.log.warning(" skipping purchased book, can't remove via automation interface") @@ -740,34 +755,6 @@ class ITUNES(DevicePlugin): # Delete existing from Library|Books # Add to self.update_list for deletion from booklist[0] during add_books_to_metadata - ''' - # --------------------------- - # PROVISIONAL - # Use the cover to find the database storage point of the epub - # Pass database copy to iTunes instead of the temporary file - - if False: - if DEBUG: - self.log.info(" processing '%s'" % metadata[i].title) - self.log.info(" file: %s" % (file._name if isinstance(file,PersistentTemporaryFile) else file)) - self.log.info(" cover: %s" % metadata[i].cover) - - calibre_database_item = False - if metadata[i].cover: - passed_file = file - storage_path = os.path.split(metadata[i].cover)[0] - try: - database_epub = filter(lambda x: x.endswith('.epub'), os.listdir(storage_path))[0] - file = os.path.join(storage_path,database_epub) - calibre_database_item = True - self.log.info(" using database file: %s" % file) - except: - self.log.info(" could not find epub in %s" % storage_path) - else: - self.log.info(" no cover available, using temp file") - # --------------------------- - ''' - path = self.path_template % (metadata[i].title, metadata[i].author[0]) if path in self.cached_books: if DEBUG: @@ -778,6 +765,21 @@ class ITUNES(DevicePlugin): if DEBUG: self.log.info( " deleting existing '%s'" % (path)) self._remove_from_iTunes(self.cached_books[path]) + if self.manual_sync_mode: + dev_book_added = self._remove_device_book(self.cached_books[path]) + + ''' + Old code testing for PTO + Use this with manuals_sync_mode to decide whether to add to Library|Books + if DEBUG: + self.log.info(" file: %s" % (file._name if isinstance(file,PersistentTemporaryFile) else file)) + # Add to iTunes Library|Books + if isinstance(file,PersistentTemporaryFile): + added = self.iTunes.add(appscript.mactypes.File(file._name)) + else: + added = self.iTunes.add(appscript.mactypes.File(file)) + + ''' # Add to iTunes Library|Books fpath = file @@ -785,7 +787,18 @@ class ITUNES(DevicePlugin): fpath = file.orig_file_path elif getattr(file, 'name', None) is not None: fpath = file.name - added = self.iTunes.add(appscript.mactypes.File(fpath)) + + if isinstance(file,PersistentTemporaryFile) and self.manual_sync_mode: + if DEBUG: + self.log.info(" PTF not added to Library|Books") + else: + added = self.iTunes.add(appscript.mactypes.File(fpath)) + if DEBUG: + self.log.info(" file added to Library|Books") + + dev_book_added = None + if self.manual_sync_mode: + dev_book_added = self._add_device_book(fpath) thumb = None if metadata[i].cover: @@ -853,7 +866,8 @@ class ITUNES(DevicePlugin): self.cached_books[this_book.path] = { 'title': this_book.title, 'author': this_book.author, - 'lib_book': added + 'lib_book': added, + 'dev_book': dev_book_added } # Report progress @@ -864,62 +878,12 @@ class ITUNES(DevicePlugin): try: pythoncom.CoInitialize() self.iTunes = win32com.client.Dispatch("iTunes.Application") - - for source in self.iTunes.sources: - if source.Kind == self.Sources.index('Library'): - lib = source - if DEBUG: - self.log.info(" Library source: '%s' kind: %s" % (lib.Name, self.Sources[lib.Kind])) - break - else: - if DEBUG: - self.log.info(" Library source not found") - - if lib is not None: - lib_books = None - for pl in lib.Playlists: - if pl.Kind == self.PlaylistKind.index('User') and \ - pl.SpecialKind == self.PlaylistSpecialKind.index('Books'): - if DEBUG: - self.log.info(" Books playlist: '%s'" % (pl.Name)) - lib_books = pl - break - else: - if DEBUG: - self.log.error(" no Books playlist found") + lib = self.iTunes.LibraryPlaylist for (i,file) in enumerate(files): # Delete existing from Library|Books, add to self.update_list # for deletion from booklist[0] during add_books_to_metadata - ''' - # --------------------------- - # PROVISIONAL - # Use the cover to find the database storage point of the epub - # Pass database copy to iTunes instead of the temporary file - - if False: - if DEBUG: - self.log.info(" processing '%s'" % metadata[i].title) - self.log.info(" file: %s" % (file._name if isinstance(file,PersistentTemporaryFile) else file)) - self.log.info(" cover: %s" % metadata[i].cover) - - calibre_database_item = False - if metadata[i].cover: - passed_file = file - storage_path = os.path.split(metadata[i].cover)[0] - try: - database_epub = filter(lambda x: x.endswith('.epub'), os.listdir(storage_path))[0] - file = os.path.join(storage_path,database_epub) - calibre_database_item = True - self.log.info(" using database file: %s" % file) - except: - self.log.info(" could not find epub in %s" % storage_path) - else: - self.log.info(" no cover available, using temp file") - # --------------------------- - ''' - path = self.path_template % (metadata[i].title, metadata[i].author[0]) if path in self.cached_books: self.update_list.append(self.cached_books[path]) @@ -928,6 +892,9 @@ class ITUNES(DevicePlugin): self.log.info("ITUNES.upload_books():") self.log.info( " deleting existing '%s'" % (path)) self._remove_from_iTunes(self.cached_books[path]) + if self.manual_sync_mode: + dev_book_added = self._remove_device_book(self.cached_books[path]) + else: if DEBUG: self.log.info(" '%s' not in cached_books" % metadata[i].title) @@ -939,41 +906,58 @@ class ITUNES(DevicePlugin): elif getattr(file, 'name', None) is not None: fpath = file.name - op_status = lib_books.AddFile(fpath) - self.log.info("ITUNES.upload_books():\n iTunes adding '%s'" - % fpath) - - if DEBUG: - sys.stdout.write(" iTunes copying '%s' ..." % metadata[i].title) - sys.stdout.flush() - - while op_status.InProgress: - time.sleep(0.5) + # If this file is to be deleted after xfer to device, don't add it to the + # iTunes database, as the file path will be invalid when calibre exits. + # Only possible in manual_sync_mode + if getattr(file, 'deleted_after_upload', False) and self.manual_sync_mode: if DEBUG: - sys.stdout.write('.') - sys.stdout.flush() - if DEBUG: - sys.stdout.write("\n") - sys.stdout.flush() - - if False: - # According to the Apple API, .Tracks should be populated once the xfer - # is complete, but I can't seem to make that work. + self.log.info(" PTF not added to Library|Books") + else: + # Add fpath to Library|Books + file_s = ctypes.c_char_p(fpath) + FileArray = ctypes.c_char_p * 1 + fa = FileArray(file_s) + op_status = lib.AddFiles(fa) if DEBUG: - sys.stdout.write(" waiting for handle to '%s' ..." % metadata[i].title) + self.log.info(" file added to Library|Books") + + self.log.info("ITUNES.upload_books():\n iTunes adding '%s'" + % fpath) + + if DEBUG: + sys.stdout.write(" iTunes copying '%s' ..." % metadata[i].title) sys.stdout.flush() - while op_status.Tracks is None: + + while op_status.InProgress: time.sleep(0.5) if DEBUG: sys.stdout.write('.') sys.stdout.flush() if DEBUG: - print - added = op_status.Tracks.Item[1] - else: - # This approach simply scans Library|Books for the book we just added - added = self._find_library_book( - {'title': metadata[i].title,'author': metadata[i].author[0]}) + sys.stdout.write("\n") + sys.stdout.flush() + + if True: + if DEBUG: + sys.stdout.write(" waiting for handle to added '%s' ..." % metadata[i].title) + sys.stdout.flush() + while op_status.Tracks is None: + time.sleep(0.5) + if DEBUG: + sys.stdout.write('.') + sys.stdout.flush() + if DEBUG: + print + added = op_status.Tracks[0] + else: + # This approach simply scans Library|Books for the book we just added + added = self._find_library_book( + {'title': metadata[i].title, + 'author': metadata[i].author[0]}) + + dev_book_added = None + if self.manual_sync_mode: + dev_book_added = self._add_device_book(fpath) if added: thumb = None @@ -1044,7 +1028,8 @@ class ITUNES(DevicePlugin): self.cached_books[this_book.path] = { 'title': metadata[i].title, 'author': metadata[i].author[0], - 'lib_book': added + 'lib_book': added, + 'dev_book': dev_book_added } # Report progress @@ -1059,12 +1044,115 @@ class ITUNES(DevicePlugin): self.report_progress(1.0, _('finished')) # Tell sync_booklists we need a re-sync - self.update_needed = True - self.update_msg = "Added books to device" + if not self.manual_sync_mode: + self.update_needed = True + self.update_msg = "Added books to device" return (new_booklist, [], []) # Private methods + def _add_device_book(self,fpath): + ''' + ''' + self.log.info("ITUNES._add_device_book()") + if isosx: + if 'iPod' in self.sources: + connected_device = self.sources['iPod'] + device = self.iTunes.sources[connected_device] + for pl in device.playlists(): + if pl.special_kind() == appscript.k.Books: + break + else: + if DEBUG: + self.log.error(" Device|Books playlist not found") + + # Add the passed book to the Device|Books playlist + added = pl.add(appscript.mactypes.File(fpath),to=pl) + if DEBUG: + self.log.info(" adding '%s' to device" % fpath) + return added + + elif iswindows: + if 'iPod' in self.sources: + try: + pythoncom.CoInitialize() + connected_device = self.sources['iPod'] + device = self.iTunes.sources.ItemByName(connected_device) + + dev_books = None + added = None + for pl in device.Playlists: + if pl.Kind == self.PlaylistKind.index('User') and \ + pl.SpecialKind == self.PlaylistSpecialKind.index('Books'): + break + else: + if DEBUG: + self.log.info(" no Books playlist found") + + # Add the passed book to the Device|Books playlist + if pl: + added = pl.AddFile(fpath) + if DEBUG: + self.log.info(" adding '%s' to device" % fpath) + finally: + pythoncom.CoUninitialize() + + return added + + def _discover_manual_sync_mode(self, wait=0): + ''' + Assumes pythoncom for windows + wait is passed when launching iTunes, as it seems to need a moment to come to its senses + + ''' + if DEBUG: + self.log.info("ITUNES._discover_manual_sync_mode()") + if isosx: + connected_device = self.sources['iPod'] + dev_books = None + device = self.iTunes.sources[connected_device] + for pl in device.playlists(): + if pl.special_kind() == appscript.k.Books: + dev_books = pl.file_tracks() + break + else: + self.log.error(" book_playlist not found") + + if len(dev_books): + first_book = dev_books[0] + #if DEBUG: + #self.log.info(" determing manual mode by modifying '%s' by %s" % (first_book.name(), first_book.artist())) + try: + first_book.bpm.set(0) + self.manual_sync_mode = True + except: + self.manual_sync_mode = False + self.log.info(" iTunes.manual_sync_mode: %s" % self.manual_sync_mode) + + elif iswindows: + if wait: + time.sleep(wait) + connected_device = self.sources['iPod'] + device = self.iTunes.sources.ItemByName(connected_device) + + dev_books = None + for pl in device.Playlists: + if pl.Kind == self.PlaylistKind.index('User') and \ + pl.SpecialKind == self.PlaylistSpecialKind.index('Books'): + dev_books = pl.Tracks + break + + if dev_books.Count: + first_book = dev_books.Item(1) + #if DEBUG: + #self.log.info(" determing manual mode by modifying '%s' by %s" % (first_book.Name, first_book.Artist)) + try: + first_book.BPM = 0 + self.manual_sync_mode = True + except: + self.manual_sync_mode = False + self.log.info(" iTunes.manual_sync_mode: %s" % self.manual_sync_mode) + def _dump_booklist(self, booklist, header=None): ''' ''' @@ -1090,10 +1178,11 @@ class ITUNES(DevicePlugin): self.log.info( "%s" % ('-' * len(msg))) if isosx: for cb in self.cached_books.keys(): - self.log.info("%-40.40s %-30.30s %-10.10s" % + self.log.info("%-40.40s %-30.30s %-10.10s %-10.10s" % (self.cached_books[cb]['title'], self.cached_books[cb]['author'], - str(self.cached_books[cb]['lib_book'])[-9:])) + str(self.cached_books[cb]['lib_book'])[-9:], + str(self.cached_books[cb]['dev_book'])[-9:])) elif iswindows: for cb in self.cached_books.keys(): self.log.info("%-40.40s %-30.30s" % @@ -1121,7 +1210,10 @@ class ITUNES(DevicePlugin): self.log.info(msg) self.log.info( "%s" % ('-' * len(msg))) for file in files: - self.log.info(file) + if getattr(file, 'orig_file_path', None) is not None: + self.log.info(" %s" % file.orig_file_path) + elif getattr(file, 'name', None) is not None: + self.log.info(" %s" % file.name) self.log.info() def _dump_update_list(self,header=None): @@ -1147,7 +1239,6 @@ class ITUNES(DevicePlugin): ''' Windows-only method to get a handle to a library book in the current pythoncom session ''' - SearchField = ['All','Visible','Artists','Titles','Composers','SongNames'] if iswindows: if DEBUG: self.log.info("ITUNES._find_library_book()") @@ -1178,8 +1269,8 @@ class ITUNES(DevicePlugin): attempts = 9 while attempts: - # Find all books by this author, then match title - hits = lib_books.Search(cached_book['author'],SearchField.index('Artists')) + # Find book whose Artist field = cached_book['author'] + hits = lib_books.Search(cached_book['author'],self.SearchField.index('Artists')) if hits: for hit in hits: self.log.info(" evaluating '%s' by %s" % (hit.Name, hit.Artist)) @@ -1346,6 +1437,30 @@ class ITUNES(DevicePlugin): return device_books + def _get_device_playlist(self): + ''' + + ''' + if iswindows: + if 'iPod' in self.sources: + pl = None + try: + pythoncom.CoInitialize() + connected_device = self.sources['iPod'] + device = self.iTunes.sources.ItemByName(connected_device) + + dev_books = None + for pl in device.Playlists: + if pl.Kind == self.PlaylistKind.index('User') and \ + pl.SpecialKind == self.PlaylistSpecialKind.index('Books'): + break + else: + if DEBUG: + self.log.error(" no iPad|Books playlist found") + finally: + pythoncom.CoUninitialize() + return pl + def _get_library_books(self): ''' Populate a dict of paths from iTunes Library|Books @@ -1427,17 +1542,20 @@ class ITUNES(DevicePlugin): if DEBUG: self.log.error(" no Library playlists found") - for book in lib_books: - # This may need additional entries for international iTunes users - if book.KindAsString in ['MPEG audio file']: - if DEBUG: - self.log.info(" ignoring %-30.30s of type '%s'" % (book.Name, book.KindAsString)) - else: - if DEBUG: - self.log.info(" adding %-30.30s [%s]" % (book.Name, book.KindAsString)) - path = self.path_template % (book.Name, book.Artist) - library_books[path] = book - + try: + for book in lib_books: + # This may need additional entries for international iTunes users + if book.KindAsString in ['MPEG audio file']: + if DEBUG: + self.log.info(" ignoring %-30.30s of type '%s'" % (book.Name, book.KindAsString)) + else: + if DEBUG: + self.log.info(" adding %-30.30s [%s]" % (book.Name, book.KindAsString)) + path = self.path_template % (book.Name, book.Artist) + library_books[path] = book + except: + if DEBUG: + self.log.info(" no books in library") finally: pythoncom.CoUninitialize() @@ -1553,6 +1671,30 @@ class ITUNES(DevicePlugin): self.version[0],self.version[1],self.version[2])) self.log.info(" iTunes_media: %s" % self.iTunes_media) + def _remove_device_book(self, cached_book): + ''' + Windows assumes pythoncom wrapper + ''' + self.log.info("ITUNES._remove_device_book()") + if isosx: + if DEBUG: + self.log.info(" deleting %s" % cached_book['dev_book']) + result = cached_book['dev_book'].delete() + print "result: %s" % result + + elif iswindows: + dev_pl = self._get_device_playlist() + hits = dev_pl.Search(cached_book['author'],self.SearchField.index('Artists')) + if hits: + for hit in hits: + if DEBUG: + self.log.info(" evaluating '%s' by %s" % (hit.Name, hit.Artist)) + if hit.Name == cached_book['title']: + if DEBUG: + self.log.info(" deleting '%s' by %s" % (hit.Name, hit.Artist)) + results = hit.Delete() + break + def _remove_from_iTunes(self, cached_book): ''' iTunes does not delete books from storage when removing from database