diff --git a/src/calibre/devices/apple/driver.py b/src/calibre/devices/apple/driver.py index 2559d0149e..0fd2bbcc2c 100644 --- a/src/calibre/devices/apple/driver.py +++ b/src/calibre/devices/apple/driver.py @@ -17,7 +17,7 @@ from calibre.ebooks.metadata import authors_to_string, MetaInformation from calibre.ebooks.metadata.book.base import Metadata from calibre.ebooks.metadata.epub import set_metadata from calibre.library.server.utils import strftime -from calibre.utils.config import config_dir +from calibre.utils.config import config_dir, prefs from calibre.utils.date import isoformat, now, parse_date from calibre.utils.localization import get_lang from calibre.utils.logging import Log @@ -68,6 +68,8 @@ class ITUNES(DriverBase): Delete: delete_books() remove_books_from_metadata() + use_plugboard_ext() + set_plugboard() sync_booklists() card_prefix() free_space() @@ -76,6 +78,8 @@ class ITUNES(DriverBase): set_progress_reporter() upload_books() add_books_to_metadata() + use_plugboard_ext() + set_plugboard() set_progress_reporter() sync_booklists() card_prefix() @@ -106,6 +110,9 @@ class ITUNES(DriverBase): PRODUCT_ID = [0x1292,0x1293,0x1294,0x1297,0x1299,0x129a] BCD = [0x01] + # Plugboard ID + DEVICE_PLUGBOARD_NAME = 'APPLE' + # iTunes enumerations Audiobooks = [ 'Audible file', @@ -164,6 +171,7 @@ class ITUNES(DriverBase): # Properties cached_books = {} cache_dir = os.path.join(config_dir, 'caches', 'itunes') + calibre_library_path = prefs['library_path'] archive_path = os.path.join(cache_dir, "thumbs.zip") description_prefix = "added by calibre" ejected = False @@ -173,6 +181,8 @@ class ITUNES(DriverBase): log = Log() manual_sync_mode = False path_template = 'iTunes/%s - %s.%s' + plugboards = None + plugboard_func = None problem_titles = [] problem_msg = None report_progress = None @@ -814,6 +824,15 @@ class ITUNES(DriverBase): ''' self.report_progress = report_progress + def set_plugboards(self, plugboards, pb_func): + # This method is called with the plugboard that matches the format + # declared in use_plugboard_ext and a device name of ITUNES + if DEBUG: + self.log.info("ITUNES.set_plugboard()") + #self.log.info(' using plugboard %s' % plugboards) + self.plugboards = plugboards + self.plugboard_func = pb_func + def sync_booklists(self, booklists, end_session=True): ''' Update metadata on device. @@ -977,7 +996,6 @@ class ITUNES(DriverBase): self._dump_cached_books(header="after upload_books()",indent=2) return (new_booklist, [], []) - # Private methods def _add_device_book(self,fpath, metadata): ''' @@ -1256,7 +1274,10 @@ class ITUNES(DriverBase): self.problem_titles.append("'%s' by %s" % (metadata.title, metadata.author[0])) self.log.error(" error converting '%s' to thumb for '%s'" % (metadata.cover,metadata.title)) finally: - zfw.close() + try: + zfw.close() + except: + pass else: if DEBUG: self.log.info(" no cover defined in metadata for '%s'" % metadata.title) @@ -1273,10 +1294,10 @@ class ITUNES(DriverBase): this_book.db_id = None this_book.device_collections = [] this_book.format = format - this_book.library_id = lb_added + this_book.library_id = lb_added # ??? GR this_book.path = path this_book.thumbnail = thumb - this_book.iTunes_id = lb_added + this_book.iTunes_id = lb_added # ??? GR this_book.uuid = metadata.uuid if isosx: @@ -1322,8 +1343,8 @@ class ITUNES(DriverBase): plist = None if plist: if DEBUG: - self.log.info(" _delete_iTunesMetadata_plist():") - self.log.info(" deleting '%s'\n from '%s'" % (pl_name,fpath)) + self.log.info(" _delete_iTunesMetadata_plist():") + self.log.info(" deleting '%s'\n from '%s'" % (pl_name,fpath)) zf.delete(pl_name) zf.close() @@ -2213,6 +2234,7 @@ class ITUNES(DriverBase): (self.iTunes.name(), self.iTunes.version(), self.initial_status, self.version[0],self.version[1],self.version[2])) self.log.info(" iTunes_media: %s" % self.iTunes_media) + self.log.info(" calibre_library_path: %s" % self.calibre_library_path) if iswindows: ''' @@ -2266,6 +2288,7 @@ class ITUNES(DriverBase): (self.iTunes.Windows[0].name, self.iTunes.Version, self.initial_status, self.version[0],self.version[1],self.version[2])) self.log.info(" iTunes_media: %s" % self.iTunes_media) + self.log.info(" calibre_library_path: %s" % self.calibre_library_path) def _purge_orphans(self,library_books, cached_books): ''' @@ -2368,7 +2391,8 @@ class ITUNES(DriverBase): ''' iTunes does not delete books from storage when removing from database We only want to delete stored copies if the file is stored in iTunes - We don't want to delete files stored outside of iTunes + We don't want to delete files stored outside of iTunes. + Also confirm that storage_path does not point into calibre's storage. ''' if DEBUG: self.log.info(" ITUNES._remove_from_iTunes():") @@ -2376,7 +2400,8 @@ class ITUNES(DriverBase): if isosx: try: storage_path = os.path.split(cached_book['lib_book'].location().path) - if cached_book['lib_book'].location().path.startswith(self.iTunes_media): + if cached_book['lib_book'].location().path.startswith(self.iTunes_media) and \ + not storage_path[0].startswith(self.calibre_library_path): title_storage_path = storage_path[0] if DEBUG: self.log.info(" removing title_storage_path: %s" % title_storage_path) @@ -2427,7 +2452,8 @@ class ITUNES(DriverBase): path = book.Location if book: - if self.iTunes_media and path.startswith(self.iTunes_media): + if self.iTunes_media and path.startswith(self.iTunes_media) and \ + not path.startswith(self.calibre_library_path): storage_path = os.path.split(path) if DEBUG: self.log.info(" removing '%s' at %s" % @@ -2454,11 +2480,17 @@ class ITUNES(DriverBase): if DEBUG: self.log.info(" unable to remove '%s' from iTunes" % cached_book['title']) + def title_sorter(self, title): + return re.sub('^\s*A\s+|^\s*The\s+|^\s*An\s+', '', title).rstrip() + def _update_epub_metadata(self, fpath, metadata): ''' ''' self.log.info(" ITUNES._update_epub_metadata()") + # Fetch plugboard updates + metadata_x = self._xform_metadata_via_plugboard(metadata, 'epub') + # Refresh epub metadata with open(fpath,'r+b') as zfo: # Touch the OPF timestamp @@ -2490,9 +2522,14 @@ class ITUNES(DriverBase): self.log.info(" add timestamp: %s" % metadata.timestamp) # Force the language declaration for iBooks 1.1 - metadata.language = get_lang().replace('_', '-') + #metadata.language = get_lang().replace('_', '-') + + # Updates from metadata plugboard (ignoring publisher) + metadata.language = metadata_x.language + if DEBUG: - self.log.info(" rewriting language: %s" % metadata.language) + if metadata.language != metadata_x.language: + self.log.info(" rewriting language: %s" % metadata.language) zf_opf.close() @@ -2570,75 +2607,97 @@ class ITUNES(DriverBase): if DEBUG: self.log.info(" ITUNES._update_iTunes_metadata()") - strip_tags = re.compile(r'<[^<]*?/?>') + STRIP_TAGS = re.compile(r'<[^<]*?/?>') + + # Update metadata from plugboard + # If self.plugboard is None (no transforms), original metadata is returned intact + metadata_x = self._xform_metadata_via_plugboard(metadata, this_book.format) if isosx: if lb_added: - lb_added.album.set(metadata.title) - lb_added.artist.set(authors_to_string(metadata.authors)) - lb_added.composer.set(metadata.uuid) + lb_added.name.set(metadata_x.title) + lb_added.album.set(metadata_x.title) + lb_added.artist.set(authors_to_string(metadata_x.authors)) + lb_added.composer.set(metadata_x.uuid) lb_added.description.set("%s %s" % (self.description_prefix,strftime('%Y-%m-%d %H:%M:%S'))) lb_added.enabled.set(True) - lb_added.sort_artist.set(metadata.author_sort.title()) - lb_added.sort_name.set(this_book.title_sorter) - if this_book.format == 'pdf': - lb_added.name.set(metadata.title) + lb_added.sort_artist.set(metadata_x.author_sort.title()) + lb_added.sort_name.set(metadata.title_sort) + if db_added: - db_added.album.set(metadata.title) - db_added.artist.set(authors_to_string(metadata.authors)) - db_added.composer.set(metadata.uuid) + db_added.name.set(metadata_x.title) + db_added.album.set(metadata_x.title) + db_added.artist.set(authors_to_string(metadata_x.authors)) + db_added.composer.set(metadata_x.uuid) db_added.description.set("%s %s" % (self.description_prefix,strftime('%Y-%m-%d %H:%M:%S'))) db_added.enabled.set(True) - db_added.sort_artist.set(metadata.author_sort.title()) - db_added.sort_name.set(this_book.title_sorter) - if this_book.format == 'pdf': - db_added.name.set(metadata.title) + db_added.sort_artist.set(metadata_x.author_sort.title()) + db_added.sort_name.set(metadata.title_sort) - if metadata.comments: + if metadata_x.comments: if lb_added: - lb_added.comment.set(strip_tags.sub('',metadata.comments)) + lb_added.comment.set(STRIP_TAGS.sub('',metadata_x.comments)) if db_added: - db_added.comment.set(strip_tags.sub('',metadata.comments)) + db_added.comment.set(STRIP_TAGS.sub('',metadata_x.comments)) - if metadata.rating: + if metadata_x.rating: if lb_added: - lb_added.rating.set(metadata.rating*10) + lb_added.rating.set(metadata_x.rating*10) # iBooks currently doesn't allow setting rating ... ? try: if db_added: - db_added.rating.set(metadata.rating*10) + db_added.rating.set(metadata_x.rating*10) except: pass # Set genre from series if available, else first alpha tag # Otherwise iTunes grabs the first dc:subject from the opf metadata - if metadata.series and self.settings().read_metadata: + # self.settings().read_metadata is used as a surrogate for "Use Series name as Genre" + if metadata_x.series and self.settings().read_metadata: if DEBUG: + self.log.info(" ITUNES._update_iTunes_metadata()") self.log.info(" using Series name as Genre") # Format the index as a sort key - index = metadata.series_index + index = metadata_x.series_index integer = int(index) fraction = index-integer series_index = '%04d%s' % (integer, str('%0.4f' % fraction).lstrip('0')) if lb_added: - lb_added.sort_name.set("%s %s" % (metadata.series, series_index)) - lb_added.genre.set(metadata.series) - lb_added.episode_ID.set(metadata.series) - lb_added.episode_number.set(metadata.series_index) + lb_added.sort_name.set("%s %s" % (self.title_sorter(metadata_x.series), series_index)) + lb_added.episode_ID.set(metadata_x.series) + lb_added.episode_number.set(metadata_x.series_index) + + # If no plugboard transform applied to tags, change the Genre/Category to Series + if metadata.tags == metadata_x.tags: + lb_added.genre.set(self.title_sorter(metadata_x.series)) + else: + for tag in metadata_x.tags: + if self._is_alpha(tag[0]): + lb_added.genre.set(tag) + break if db_added: - db_added.sort_name.set("%s %s" % (metadata.series, series_index)) - db_added.genre.set(metadata.series) - db_added.episode_ID.set(metadata.series) - db_added.episode_number.set(metadata.series_index) + db_added.sort_name.set("%s %s" % (self.title_sorter(metadata_x.series), series_index)) + db_added.episode_ID.set(metadata_x.series) + db_added.episode_number.set(metadata_x.series_index) - elif metadata.tags: + # If no plugboard transform applied to tags, change the Genre/Category to Series + if metadata.tags == metadata_x.tags: + db_added.genre.set(self.title_sorter(metadata_x.series)) + else: + for tag in metadata_x.tags: + if self._is_alpha(tag[0]): + db_added.genre.set(tag) + break + + + elif metadata_x.tags is not None: if DEBUG: self.log.info(" %susing Tag as Genre" % "no Series name available, " if self.settings().read_metadata else '') - for tag in metadata.tags: + for tag in metadata_x.tags: if self._is_alpha(tag[0]): if lb_added: lb_added.genre.set(tag) @@ -2648,40 +2707,38 @@ class ITUNES(DriverBase): elif iswindows: if lb_added: - lb_added.Album = metadata.title - lb_added.Artist = authors_to_string(metadata.authors) - lb_added.Composer = metadata.uuid + lb_added.Name = metadata_x.title + lb_added.Album = metadata_x.title + lb_added.Artist = authors_to_string(metadata_x.authors) + lb_added.Composer = metadata_x.uuid lb_added.Description = ("%s %s" % (self.description_prefix,strftime('%Y-%m-%d %H:%M:%S'))) lb_added.Enabled = True - lb_added.SortArtist = (metadata.author_sort.title()) - lb_added.SortName = (this_book.title_sorter) - if this_book.format == 'pdf': - lb_added.Name = metadata.title + lb_added.SortArtist = metadata_x.author_sort.title() + lb_added.SortName = metadata.title_sort if db_added: - db_added.Album = metadata.title - db_added.Artist = authors_to_string(metadata.authors) - db_added.Composer = metadata.uuid + db_added.Name = metadata_x.title + db_added.Album = metadata_x.title + db_added.Artist = authors_to_string(metadata_x.authors) + db_added.Composer = metadata_x.uuid db_added.Description = ("%s %s" % (self.description_prefix,strftime('%Y-%m-%d %H:%M:%S'))) db_added.Enabled = True - db_added.SortArtist = (metadata.author_sort.title()) - db_added.SortName = (this_book.title_sorter) - if this_book.format == 'pdf': - db_added.Name = metadata.title + db_added.SortArtist = metadata_x.author_sort.title() + db_added.SortName = metadata.title_sort - if metadata.comments: + if metadata_x.comments: if lb_added: - lb_added.Comment = (strip_tags.sub('',metadata.comments)) + lb_added.Comment = (STRIP_TAGS.sub('',metadata_x.comments)) if db_added: - db_added.Comment = (strip_tags.sub('',metadata.comments)) + db_added.Comment = (STRIP_TAGS.sub('',metadata_x.comments)) - if metadata.rating: + if metadata_x.rating: if lb_added: - lb_added.AlbumRating = (metadata.rating*10) + lb_added.AlbumRating = (metadata_x.rating*10) # iBooks currently doesn't allow setting rating ... ? try: if db_added: - db_added.AlbumRating = (metadata.rating*10) + db_added.AlbumRating = (metadata_x.rating*10) except: if DEBUG: self.log.warning(" iTunes automation interface reported an error" @@ -2691,36 +2748,54 @@ class ITUNES(DriverBase): # Otherwise iBooks uses first from opf # iTunes balks on setting EpisodeNumber, but it sticks (9.1.1.12) - if metadata.series and self.settings().read_metadata: + if metadata_x.series and self.settings().read_metadata: if DEBUG: self.log.info(" using Series name as Genre") # Format the index as a sort key - index = metadata.series_index + index = metadata_x.series_index integer = int(index) fraction = index-integer series_index = '%04d%s' % (integer, str('%0.4f' % fraction).lstrip('0')) if lb_added: - lb_added.SortName = "%s %s" % (metadata.series, series_index) - lb_added.Genre = metadata.series - lb_added.EpisodeID = metadata.series + lb_added.SortName = "%s %s" % (self.title_sorter(metadata_x.series), series_index) + lb_added.EpisodeID = metadata_x.series try: - lb_added.EpisodeNumber = metadata.series_index + lb_added.EpisodeNumber = metadata_x.series_index except: pass + + # If no plugboard transform applied to tags, change the Genre/Category to Series + if metadata.tags == metadata_x.tags: + lb_added.Genre = self.title_sorter(metadata_x.series) + else: + for tag in metadata_x.tags: + if self._is_alpha(tag[0]): + lb_added.Genre = tag + break + if db_added: - db_added.SortName = "%s %s" % (metadata.series, series_index) - db_added.Genre = metadata.series - db_added.EpisodeID = metadata.series + db_added.SortName = "%s %s" % (self.title_sorter(metadata_x.series), series_index) + db_added.EpisodeID = metadata_x.series try: - db_added.EpisodeNumber = metadata.series_index + db_added.EpisodeNumber = metadata_x.series_index except: if DEBUG: self.log.warning(" iTunes automation interface reported an error" " setting EpisodeNumber on iDevice") - elif metadata.tags: + + # If no plugboard transform applied to tags, change the Genre/Category to Series + if metadata.tags == metadata_x.tags: + db_added.Genre = self.title_sorter(metadata_x.series) + else: + for tag in metadata_x.tags: + if self._is_alpha(tag[0]): + db_added.Genre = tag + break + + elif metadata_x.tags is not None: if DEBUG: self.log.info(" using Tag as Genre") - for tag in metadata.tags: + for tag in metadata_x.tags: if self._is_alpha(tag[0]): if lb_added: lb_added.Genre = tag @@ -2728,6 +2803,36 @@ class ITUNES(DriverBase): db_added.Genre = tag break + def _xform_metadata_via_plugboard(self, book, format): + ''' Transform book metadata from plugboard templates ''' + if DEBUG: + self.log.info(" ITUNES._update_metadata_from_plugboard()") + + if self.plugboard_func: + pb = self.plugboard_func(self.DEVICE_PLUGBOARD_NAME, format, self.plugboards) + newmi = book.deepcopy_metadata() + newmi.template_to_attribute(book, pb) + if DEBUG: + self.log.info(" transforming %s using %s:" % (format, pb)) + self.log.info(" title: %s %s" % (book.title, ">>> %s" % + newmi.title if book.title != newmi.title else '')) + self.log.info(" title_sort: %s %s" % (book.title_sort, ">>> %s" % + newmi.title_sort if book.title_sort != newmi.title_sort else '')) + self.log.info(" authors: %s %s" % (book.authors, ">>> %s" % + newmi.authors if book.authors != newmi.authors else '')) + self.log.info(" author_sort: %s %s" % (book.author_sort, ">>> %s" % + newmi.author_sort if book.author_sort != newmi.author_sort else '')) + self.log.info(" language: %s %s" % (book.language, ">>> %s" % + newmi.language if book.language != newmi.language else '')) + self.log.info(" publisher: %s %s" % (book.publisher, ">>> %s" % + newmi.publisher if book.publisher != newmi.publisher else '')) + self.log.info(" tags: %s %s" % (book.tags, ">>> %s" % + newmi.tags if book.tags != newmi.tags else '')) + else: + newmi = book + return newmi + + class ITUNES_ASYNC(ITUNES): ''' This subclass allows the user to interact directly with iTunes via a menu option @@ -2738,6 +2843,9 @@ class ITUNES_ASYNC(ITUNES): icon = I('devices/itunes.png') description = _('Communicate with iTunes.') + # Plugboard ID + DEVICE_PLUGBOARD_NAME = 'APPLE' + connected = False def __init__(self,path): @@ -3012,15 +3120,9 @@ class BookList(list): class Book(Metadata): ''' A simple class describing a book in the iTunes Books Library. - - See ebooks.metadata.__init__ for all fields + See ebooks.metadata.book.base ''' def __init__(self,title,author): Metadata.__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) diff --git a/src/calibre/gui2/preferences/plugboard.ui b/src/calibre/gui2/preferences/plugboard.ui index 8249584678..9f14978ca8 100644 --- a/src/calibre/gui2/preferences/plugboard.ui +++ b/src/calibre/gui2/preferences/plugboard.ui @@ -6,8 +6,8 @@ 0 0 - 707 - 340 + 931 + 389 @@ -23,7 +23,7 @@ Use this dialog to define a 'plugboard' for a format (or all formats) and a devi Often templates will contain simple references to composite columns, but this is not necessary. You can use any template in a source box that you can use elsewhere in calibre. -One possible use for a plugboard is to alter the title to contain series informaton. Another would be to change the author sort, something that mobi users might do to force it to use the ';' that the kindle requires. A third would be to specify the language. +One possible use for a plugboard is to alter the title to contain series information. Another would be to change the author sort, something that mobi users might do to force it to use the ';' that the kindle requires. A third would be to specify the language. Qt::PlainText @@ -41,8 +41,7 @@ One possible use for a plugboard is to alter the title to contain series informa - - +