From 4c2a421afff473f46e58a95279eab18576c3c7c3 Mon Sep 17 00:00:00 2001 From: GRiker Date: Thu, 29 Nov 2012 17:43:03 -0700 Subject: [PATCH 01/12] Updated iTunes icon to iTunes 11. Revisions to Apple driver anticipating optional direct connect driver. --- resources/images/devices/itunes.png | Bin 25638 -> 24190 bytes src/calibre/devices/apple/driver.py | 171 ++++++++++++++-------------- 2 files changed, 88 insertions(+), 83 deletions(-) diff --git a/resources/images/devices/itunes.png b/resources/images/devices/itunes.png index cc0493d9eb7f5cd2216323fec6a9b1523b57917d..d83595d691d96d1c3488b13c5b688bcfca7a4824 100644 GIT binary patch literal 24190 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 literal 25638 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%0A^7GlpqpU3D^5tE+mp0MnyiEwrLO|!9o~^w3 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' '

') - # Product IDs: - # 0x1291 iPod Touch - # 0x1293 iPod Touch 2G - # 0x1299 iPod Touch 3G - # 0x1292 iPhone 3G - # 0x1294 iPhone 3GS - # 0x1297 iPhone 4 - # 0x129a iPad - # 0x129f iPad2 (WiFi) - # 0x12a0 iPhone 4S (GSM) - # 0x12a2 iPad2 (GSM) - # 0x12a3 iPad2 (CDMA) - # 0x12a6 iPad3 (GSM) - VENDOR_ID = [0x05ac] - PRODUCT_ID = [0x1292,0x1293,0x1294,0x1297,0x1299,0x129a,0x129f,0x12a2,0x12a3,0x12a6] - BCD = [0x01] + VENDOR_ID = [] + PRODUCT_ID = [] + BCD = [] # Plugboard ID DEVICE_PLUGBOARD_NAME = 'APPLE' @@ -329,7 +318,7 @@ class ITUNES(DriverBase): L{books}(oncard='cardb')). ''' if DEBUG: - logger().info("ITUNES.add_books_to_metadata()") + logger().info("%s.add_books_to_metadata()" % self.__class__.__name__) task_count = float(len(self.update_list)) @@ -414,7 +403,7 @@ class ITUNES(DriverBase): """ if not oncard: if DEBUG: - logger().info("ITUNES:books():") + logger().info("%s.books():" % self.__class__.__name__) if self.settings().extra_customization[self.CACHE_COVERS]: logger().info(" Cover fetching/caching enabled") else: @@ -556,7 +545,7 @@ class ITUNES(DriverBase): # We need to know if iTunes sees the iPad # It may have been ejected if DEBUG: - logger().info("ITUNES.can_handle()") + logger().info("%s.can_handle()" % self.__class__.__name__) self._launch_iTunes() self.sources = self._get_sources() @@ -569,10 +558,10 @@ class ITUNES(DriverBase): attempts -= 1 time.sleep(0.5) if DEBUG: - logger().warning(" waiting for connected iPad, attempt #%d" % (10 - attempts)) + logger().warning(" waiting for connected iDevice, attempt #%d" % (10 - attempts)) else: if DEBUG: - logger().info(' found connected iPad') + logger().info(' found connected iDevice') break else: # iTunes running, but not connected iPad @@ -613,26 +602,26 @@ class ITUNES(DriverBase): sys.stdout.write('.') sys.stdout.flush() if DEBUG: - logger().info('ITUNES.can_handle_windows:\n confirming connected iPad') + logger().info("%s.can_handle_windows:\n confirming connected iPad" % self.__class__.__name__) self.ejected = False self._discover_manual_sync_mode() return True else: if DEBUG: - logger().info("ITUNES.can_handle_windows():\n device ejected") + logger().info("%s.can_handle_windows():\n device ejected" % self.__class__.__name__) self.ejected = True return False except: # iTunes connection failed, probably not running anymore - logger().error("ITUNES.can_handle_windows():\n lost connection to iTunes") + logger().error("%s.can_handle_windows():\n lost connection to iTunes" % self.__class__.__name__) return False finally: pythoncom.CoUninitialize() else: if DEBUG: - logger().info("ITUNES:can_handle_windows():\n Launching iTunes") + logger().info("%s.can_handle_windows():\n Launching iTunes" % self.__class__.__name__) try: pythoncom.CoInitialize() @@ -702,7 +691,7 @@ class ITUNES(DriverBase): self.problem_msg = _("Some books not found in iTunes database.\n" "Delete using the iBooks app.\n" "Click 'Show Details' for a list.") - logger().info("ITUNES:delete_books()") + logger().info("%s.delete_books()" % self.__class__.__name__) for path in paths: if self.cached_books[path]['lib_book']: if DEBUG: @@ -754,7 +743,7 @@ class ITUNES(DriverBase): are pending GUI jobs that need to communicate with the device. ''' if DEBUG: - logger().info("ITUNES:eject(): ejecting '%s'" % self.sources['iPod']) + logger().info("%s:eject(): ejecting '%s'" % (self.__class__.__name__, self.sources['iPod'])) if isosx: self.iTunes.eject(self.sources['iPod']) elif iswindows: @@ -785,7 +774,7 @@ class ITUNES(DriverBase): In Windows, a sync-in-progress blocks this call until sync is complete """ if DEBUG: - logger().info("ITUNES:free_space()") + logger().info("%s.free_space()" % self.__class__.__name__) free_space = 0 if isosx: @@ -818,7 +807,7 @@ class ITUNES(DriverBase): @return: (device name, device version, software version on device, mime type) """ if DEBUG: - logger().info("ITUNES:get_device_information()") + logger().info("%s.get_device_information()" % self.__class__.__name__) return (self.sources['iPod'],'hw v1.0','sw v1.0', 'mime type normally goes here') @@ -828,7 +817,7 @@ class ITUNES(DriverBase): @param outfile: file object like C{sys.stdout} or the result of an C{open} call ''' if DEBUG: - logger().info("ITUNES.get_file(): exporting '%s'" % path) + logger().info("%s.get_file(): exporting '%s'" % (self.__class__.__name__, path)) try: outfile.write(open(self.cached_books[path]['lib_book'].location().path).read()) @@ -859,7 +848,19 @@ class ITUNES(DriverBase): raise OpenFeedback(self.ITUNES_SANDBOX_LOCKOUT_MESSAGE) if DEBUG: - logger().info("ITUNES.open(connected_device: %s)" % repr(connected_device)) + VENDOR_ID = "0x%x" % connected_device[0] + PRODUCT_ID = "0x%x" % connected_device[1] + BCD = "0x%x" % connected_device[2] + MFG = connected_device[3] + MODEL = connected_device[4] + logger().info("%s.open(MFG: %s, VENDOR_ID: %s, MODEL: %s, BCD: %s, PRODUCT_ID: %s)" % + (self.__class__.__name__, + MFG, + VENDOR_ID, + MODEL, + BCD, + PRODUCT_ID + )) # Display a dialog recommending using 'Connect to iTunes' if user hasn't # previously disabled the dialog @@ -867,7 +868,7 @@ class ITUNES(DriverBase): raise AppleOpenFeedback(self) else: if DEBUG: - logger().warning(" %s" % self.UNSUPPORTED_DIRECT_CONNECT_MODE_MESSAGE) + logger().error(" %s" % self.UNSUPPORTED_DIRECT_CONNECT_MODE_MESSAGE) # Confirm/create thumbs archive if not os.path.exists(self.cache_dir): @@ -908,7 +909,7 @@ class ITUNES(DriverBase): as uuids are different ''' if DEBUG: - logger().info("ITUNES.remove_books_from_metadata()") + logger().info("%s.remove_books_from_metadata()" % self.__class__.__name__) for path in paths: if DEBUG: self._dump_cached_book(self.cached_books[path], indent=2) @@ -982,7 +983,7 @@ class ITUNES(DriverBase): :detected_device: Device information from the device scanner """ if DEBUG: - logger().info("ITUNES.reset()") + logger().info("%s.reset()" % self.__class__.__name__) if report_progress: self.set_progress_reporter(report_progress) @@ -994,7 +995,7 @@ class ITUNES(DriverBase): task does not have any progress information ''' if DEBUG: - logger().info("ITUNES.set_progress_reporter()") + logger().info("%s.set_progress_reporter()" % self.__class__.__name__) self.report_progress = report_progress @@ -1002,7 +1003,7 @@ class ITUNES(DriverBase): # This method is called with the plugboard that matches the format # declared in use_plugboard_ext and a device name of ITUNES if DEBUG: - logger().info("ITUNES.set_plugboard()") + logger().info("%s.set_plugboard()" % self.__class__.__name__) #logger().info(' plugboard: %s' % plugboards) self.plugboards = plugboards self.plugboard_func = pb_func @@ -1016,7 +1017,7 @@ class ITUNES(DriverBase): ''' if DEBUG: - logger().info("ITUNES.sync_booklists()") + logger().info("%s.sync_booklists()" % self.__class__.__name__) if self.update_needed: if DEBUG: @@ -1043,7 +1044,7 @@ class ITUNES(DriverBase): particular device doesn't have any of these locations it should return 0. """ if DEBUG: - logger().info("ITUNES:total_space()") + logger().info("%s.total_space()" % self.__class__.__name__) capacity = 0 if isosx: if 'iPod' in self.sources: @@ -1081,7 +1082,7 @@ class ITUNES(DriverBase): "Click 'Show Details' for a list.") if DEBUG: - logger().info("ITUNES.upload_books()") + logger().info("%s.upload_books()" % self.__class__.__name__) if isosx: for (i,fpath) in enumerate(files): @@ -1098,7 +1099,7 @@ class ITUNES(DriverBase): # Add new_book to self.cached_books if DEBUG: - logger().info("ITUNES.upload_books()") + logger().info("%s.upload_books()" % self.__class__.__name__) logger().info(" adding '%s' by '%s' uuid:%s to self.cached_books" % (metadata[i].title, authors_to_string(metadata[i].authors), @@ -1144,7 +1145,7 @@ class ITUNES(DriverBase): # Add new_book to self.cached_books if DEBUG: - logger().info("ITUNES.upload_books()") + logger().info("%s.upload_books()" % self.__class__.__name__) logger().info(" adding '%s' by '%s' uuid:%s to self.cached_books" % (metadata[i].title, authors_to_string(metadata[i].authors), @@ -1182,7 +1183,7 @@ class ITUNES(DriverBase): ''' assumes pythoncom wrapper for windows ''' - logger().info(" ITUNES._add_device_book()") + logger().info(" %s._add_device_book()" % self.__class__.__name__) if isosx: import appscript if 'iPod' in self.sources: @@ -1292,7 +1293,7 @@ class ITUNES(DriverBase): windows assumes pythoncom wrapper ''' if DEBUG: - logger().info(" ITUNES._add_library_book()") + logger().info(" %s._add_library_book()" % self.__class__.__name__) if isosx: import appscript added = self.iTunes.add(appscript.mactypes.File(file)) @@ -1360,7 +1361,7 @@ class ITUNES(DriverBase): fp = cached_book['lib_book'].Location ''' if DEBUG: - logger().info(" ITUNES._add_new_copy()") + logger().info(" %s._add_new_copy()" % self.__class__.__name__) if fpath.rpartition('.')[2].lower() == 'epub': self._update_epub_metadata(fpath, metadata) @@ -1399,7 +1400,7 @@ class ITUNES(DriverBase): from PIL import Image as PILImage if DEBUG: - logger().info(" ITUNES._cover_to_thumb()") + logger().info(" %s._cover_to_thumb()" % self.__class__.__name__) thumb = None if metadata.cover: @@ -1526,7 +1527,7 @@ class ITUNES(DriverBase): ''' ''' if DEBUG: - logger().info(" ITUNES._create_new_book()") + logger().info(" %s._create_new_book()" % self.__class__.__name__) this_book = Book(metadata.title, authors_to_string(metadata.authors)) this_book.datetime = time.gmtime() @@ -1575,7 +1576,7 @@ class ITUNES(DriverBase): wait is passed when launching iTunes, as it seems to need a moment to come to its senses ''' if DEBUG: - logger().info(" ITUNES._discover_manual_sync_mode()") + logger().info(" %s._discover_manual_sync_mode()" % self.__class__.__name__) if wait: time.sleep(wait) if isosx: @@ -1593,7 +1594,7 @@ class ITUNES(DriverBase): if dev_books is not None and len(dev_books): first_book = dev_books[0] if False: - logger().info(" determing manual mode by modifying '%s' by %s" % (first_book.name(), first_book.artist())) + logger().info(" determining manual mode by modifying '%s' by %s" % (first_book.name(), first_book.artist())) try: first_book.bpm.set(0) self.manual_sync_mode = True @@ -1728,7 +1729,7 @@ class ITUNES(DriverBase): ''' from calibre.ebooks.BeautifulSoup import BeautifulSoup - logger().info(" ITUNES.__get_epub_metadata()") + logger().info(" %s.__get_epub_metadata()" % self.__class__.__name__) title = None author = None timestamp = None @@ -1806,7 +1807,7 @@ class ITUNES(DriverBase): if iswindows: dev_books = self._get_device_books_playlist() if DEBUG: - logger().info(" ITUNES._find_device_book()") + logger().info(" %s._find_device_book()" % self.__class__.__name__) logger().info(" searching for '%s' by '%s' (%s)" % (search['title'], search['author'],search['uuid'])) attempts = 9 @@ -1876,7 +1877,7 @@ class ITUNES(DriverBase): ''' if iswindows: if DEBUG: - logger().info(" ITUNES._find_library_book()") + logger().info(" %s._find_library_book()" % self.__class__.__name__) ''' if 'uuid' in search: logger().info(" looking for '%s' by %s (%s)" % @@ -1996,7 +1997,8 @@ class ITUNES(DriverBase): thumb_data = zfr.read(thumb_path) if thumb_data == 'None': if False: - logger().info(" ITUNES._generate_thumbnail()\n returning None from cover cache for '%s'" % title) + logger().info(" %s._generate_thumbnail()\n returning None from cover cache for '%s'" % + (self.__class__.__name__, title)) zfr.close() return None except: @@ -2007,7 +2009,7 @@ class ITUNES(DriverBase): return thumb_data if DEBUG: - logger().info(" ITUNES._generate_thumbnail('%s'):" % title) + logger().info(" %s._generate_thumbnail('%s'):" % (self.__class__.__name__, title)) if isosx: # Fetch the artwork from iTunes @@ -2101,7 +2103,7 @@ class ITUNES(DriverBase): for file in myZipList: exploded_file_size += file.file_size if False: - logger().info(" ITUNES._get_device_book_size()") + logger().info(" %s._get_device_book_size()" % self.__class__.__name__) logger().info(" %d items in archive" % len(myZipList)) logger().info(" compressed: %d exploded: %d" % (compressed_size, exploded_file_size)) myZip.close() @@ -2112,7 +2114,7 @@ class ITUNES(DriverBase): Assumes pythoncom wrapper for Windows ''' if DEBUG: - logger().info("\n ITUNES._get_device_books()") + logger().info("\n %s._get_device_books()" % self.__class__.__name__) device_books = [] if isosx: @@ -2206,7 +2208,7 @@ class ITUNES(DriverBase): Windows assumes pythoncom wrapper ''' if DEBUG: - logger().info("\n ITUNES._get_library_books()") + logger().info("\n %s._get_library_books()" % self.__class__.__name__) library_books = {} library_orphans = {} @@ -2381,7 +2383,7 @@ class ITUNES(DriverBase): ''' ''' if DEBUG: - logger().info(" ITUNES:_launch_iTunes():\n Instantiating iTunes") + logger().info(" %s._launch_iTunes():\n Instantiating iTunes" % self.__class__.__name__) if isosx: import appscript @@ -2394,12 +2396,13 @@ class ITUNES(DriverBase): running_apps = appscript.app('System Events') if not 'iTunes' in running_apps.processes.name(): if DEBUG: - logger().info( "ITUNES:_launch_iTunes(): Launching iTunes" ) + logger().info( "%s:_launch_iTunes(): Launching iTunes" % self.__class__.__name__) try: self.iTunes = iTunes = appscript.app('iTunes', hide=True) except: self.iTunes = None - raise UserFeedback(' ITUNES._launch_iTunes(): unable to find installed iTunes', details=None, level=UserFeedback.WARN) + raise UserFeedback(' %s._launch_iTunes(): unable to find installed iTunes' % + self.__class__.__name__, details=None, level=UserFeedback.WARN) iTunes.run() self.initial_status = 'launched' @@ -2444,10 +2447,10 @@ class ITUNES(DriverBase): if DEBUG: logger().info(" %s %s" % (__appname__, __version__)) - logger().info(" [OSX %s, %s %s (%s), driver version %d.%d.%d]" % + logger().info(" [OSX %s, %s %s (%s), %s driver version %d.%d.%d]" % (platform.mac_ver()[0], self.iTunes.name(), self.iTunes.version(), self.initial_status, - self.version[0],self.version[1],self.version[2])) + self.__class__.__name__, self.version[0],self.version[1],self.version[2])) logger().info(" communicating with iTunes via %s %s using %s binding" % (as_name, as_version, as_binding)) logger().info(" calibre_library_path: %s" % self.calibre_library_path) @@ -2474,7 +2477,8 @@ class ITUNES(DriverBase): self.iTunes = win32com.client.Dispatch("iTunes.Application") except: self.iTunes = None - raise UserFeedback(' ITUNES._launch_iTunes(): unable to find installed iTunes', details=None, level=UserFeedback.WARN) + raise UserFeedback(' %s._launch_iTunes(): unable to find installed iTunes' + % self.__class__.__name__, details=None, level=UserFeedback.WARN) if not DEBUG: self.iTunes.Windows[0].Minimized = True @@ -2525,7 +2529,7 @@ class ITUNES(DriverBase): This occurs when the user deletes a book in iBooks while disconnected ''' if DEBUG: - logger().info(" ITUNES._purge_orphans()") + logger().info(" %s._purge_orphans()" % self.__class__.__name__) #self._dump_library_books(library_books) #logger().info(" cached_books:\n %s" % "\n ".join(cached_books.keys())) @@ -2555,7 +2559,7 @@ class ITUNES(DriverBase): ''' ''' if DEBUG: - logger().info(" ITUNES._remove_existing_copy()") + logger().info(" %s._remove_existing_copy()" % self.__class__.__name__) if self.manual_sync_mode: # Delete existing from Device|Books, add to self.update_list @@ -2598,7 +2602,7 @@ class ITUNES(DriverBase): Windows assumes pythoncom wrapper ''' if DEBUG: - logger().info(" ITUNES._remove_from_device()") + logger().info(" %s._remove_from_device()" % self.__class__.__name__) if isosx: if DEBUG: logger().info(" deleting '%s' from iDevice" % cached_book['title']) @@ -2622,7 +2626,7 @@ class ITUNES(DriverBase): iTunes does not delete books from storage when removing from database via automation ''' if DEBUG: - logger().info(" ITUNES._remove_from_iTunes():") + logger().info(" %s._remove_from_iTunes():" % self.__class__.__name__) if isosx: ''' Manually remove the book from iTunes storage ''' @@ -2739,7 +2743,7 @@ class ITUNES(DriverBase): from lxml import etree if DEBUG: - logger().info(" ITUNES._update_epub_metadata()") + logger().info(" %s._update_epub_metadata()" % self.__class__.__name__) # Fetch plugboard updates metadata_x = self._xform_metadata_via_plugboard(metadata, 'epub') @@ -2807,7 +2811,7 @@ class ITUNES(DriverBase): Trigger a sync, wait for completion ''' if DEBUG: - logger().info(" ITUNES:_update_device():\n %s" % msg) + logger().info(" %s:_update_device():\n %s" % (self.__class__.__name__, msg)) if isosx: self.iTunes.update() @@ -2855,7 +2859,7 @@ class ITUNES(DriverBase): ''' ''' if DEBUG: - logger().info(" ITUNES._update_iTunes_metadata()") + logger().info(" %s._update_iTunes_metadata()" % self.__class__.__name__) STRIP_TAGS = re.compile(r'<[^<]*?/?>') @@ -2907,7 +2911,7 @@ class ITUNES(DriverBase): # If title_sort applied in plugboard, that overrides using series/index as title_sort if metadata_x.series and self.settings().extra_customization[self.USE_SERIES_AS_CATEGORY]: if DEBUG: - logger().info(" ITUNES._update_iTunes_metadata()") + logger().info(" %s._update_iTunes_metadata()" % self.__class__.__name__) logger().info(" using Series name '%s' as Genre" % metadata_x.series) # Format the index as a sort key @@ -3089,7 +3093,7 @@ class ITUNES(DriverBase): Ensure iDevice metadata is writable. Direct connect mode only ''' if DEBUG: - logger().info(" ITUNES._wait_for_writable_metadata()") + logger().info(" %s._wait_for_writable_metadata()" % self.__class__.__name__) logger().warning(" %s" % self.UNSUPPORTED_DIRECT_CONNECT_MODE_MESSAGE) attempts = 9 @@ -3113,7 +3117,7 @@ class ITUNES(DriverBase): def _xform_metadata_via_plugboard(self, book, format): ''' Transform book metadata from plugboard templates ''' if DEBUG: - logger().info(" ITUNES._xform_metadata_via_plugboard()") + logger().info(" %s._xform_metadata_via_plugboard()" % self.__class__.__name__) if self.plugboard_func: pb = self.plugboard_func(self.DEVICE_PLUGBOARD_NAME, format, self.plugboards) @@ -3160,7 +3164,7 @@ class ITUNES_ASYNC(ITUNES): def __init__(self,path): if DEBUG: - logger().info("ITUNES_ASYNC:__init__()") + logger().info("%s.__init__()" % self.__class__.__name__) try: import appscript @@ -3210,7 +3214,7 @@ class ITUNES_ASYNC(ITUNES): """ if not oncard: if DEBUG: - logger().info("ITUNES_ASYNC:books()") + logger().info("%s.books()" % self.__class__.__name__) if self.settings().extra_customization[self.CACHE_COVERS]: logger().info(" Cover fetching/caching enabled") else: @@ -3324,7 +3328,7 @@ class ITUNES_ASYNC(ITUNES): are pending GUI jobs that need to communicate with the device. ''' if DEBUG: - logger().info("ITUNES_ASYNC:eject()") + logger().info("%s.eject()" % self.__class__.__name__) self.iTunes = None self.connected = False @@ -3339,7 +3343,7 @@ class ITUNES_ASYNC(ITUNES): particular device doesn't have any of these locations it should return -1. """ if DEBUG: - logger().info("ITUNES_ASYNC:free_space()") + logger().info("%s.free_space()" % self.__class__.__name__) free_space = 0 if isosx: s = os.statvfs(os.sep) @@ -3356,7 +3360,7 @@ class ITUNES_ASYNC(ITUNES): @return: (device name, device version, software version on device, mime type) """ if DEBUG: - logger().info("ITUNES_ASYNC:get_device_information()") + logger().info("%s.get_device_information()" % self.__class__.__name__) return ('iTunes','hw v1.0','sw v1.0', 'mime type normally goes here') @@ -3382,7 +3386,8 @@ class ITUNES_ASYNC(ITUNES): raise OpenFeedback(self.ITUNES_SANDBOX_LOCKOUT_MESSAGE) if DEBUG: - logger().info("ITUNES_ASYNC.open(connected_device: %s)" % repr(connected_device)) + logger().info("%s.open(connected_device: %s)" % + (self.__class__.__name__, repr(connected_device))) # Confirm/create thumbs archive if not os.path.exists(self.cache_dir): @@ -3419,7 +3424,7 @@ class ITUNES_ASYNC(ITUNES): ''' if DEBUG: - logger().info("ITUNES_ASYNC.sync_booklists()") + logger().info("%s.sync_booklists()" % self.__class__.__name__) # Inform user of any problem books if self.problem_titles: @@ -3433,7 +3438,7 @@ class ITUNES_ASYNC(ITUNES): ''' ''' if DEBUG: - logger().info("ITUNES_ASYNC:unmount_device()") + logger().info("%s.unmount_device()" % self.__class__.__name__) self.connected = False class BookList(list): From fa74983098075c138b7f9187070ed516a68106bb Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 3 Dec 2012 14:57:21 +0530 Subject: [PATCH 02/12] Update Aksiyon Dergisi --- recipes/aksiyon_derigisi.recipe | 1 + 1 file changed, 1 insertion(+) diff --git a/recipes/aksiyon_derigisi.recipe b/recipes/aksiyon_derigisi.recipe index bc15b39095..d7be418413 100644 --- a/recipes/aksiyon_derigisi.recipe +++ b/recipes/aksiyon_derigisi.recipe @@ -20,6 +20,7 @@ class Aksiyon (BasicNewsRecipe): auto_cleanup = True cover_img_url = 'http://www.aksiyon.com.tr/aksiyon/images/aksiyon/top-page/aksiyon_top_r2_c1.jpg' masthead_url = 'http://aksiyon.com.tr/aksiyon/images/aksiyon/top-page/aksiyon_top_r2_c1.jpg' + ignore_duplicate_articles = { 'title', 'url' } remove_empty_feeds= True feeds = [ ( u'KAPAK', u'http://www.aksiyon.com.tr/aksiyon/rss?sectionId=26'), From 8292b1d71dbea20739dae39bf0321f35424cff04 Mon Sep 17 00:00:00 2001 From: GRiker Date: Mon, 3 Dec 2012 16:49:09 -0700 Subject: [PATCH 03/12] Revisions to catalog building code, cleaning up diagnostics, tweaks to TOC section titles. Revisions to Apple driver in anticipation of releasing optional iDevice driver. --- src/calibre/devices/apple/driver.py | 530 ++++++------- src/calibre/library/catalogs/csv_xml.py | 48 +- .../library/catalogs/epub_mobi_builder.py | 722 +++++++++--------- 3 files changed, 681 insertions(+), 619 deletions(-) diff --git a/src/calibre/devices/apple/driver.py b/src/calibre/devices/apple/driver.py index d8c6d03f55..14c2863d6b 100644 --- a/src/calibre/devices/apple/driver.py +++ b/src/calibre/devices/apple/driver.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -__license__ = 'GPL v3' +__license__ = 'GPL v3' __copyright__ = '2010, Gregory Riker' __docformat__ = 'restructuredtext en' @@ -20,6 +20,7 @@ from calibre.utils.config import config_dir, dynamic, prefs from calibre.utils.date import now, parse_date from calibre.utils.zipfile import ZipFile + def strftime(fmt='%Y/%m/%d %H:%M:%S', dt=None): if not hasattr(dt, 'timetuple'): @@ -38,6 +39,7 @@ def logger(): _log = ThreadSafeLog() return _log + class AppleOpenFeedback(OpenFeedback): def __init__(self, plugin): @@ -102,6 +104,7 @@ class AppleOpenFeedback(OpenFeedback): return Dialog(parent, self) + class DriverBase(DeviceConfig, DevicePlugin): # Needed for config_widget to work FORMATS = ['epub', 'pdf'] @@ -116,12 +119,12 @@ class DriverBase(DeviceConfig, DevicePlugin): EXTRA_CUSTOMIZATION_MESSAGE = [ _('Use Series as Category in iTunes/iBooks') + - ':::'+_('Enable to use the series name as the iTunes Genre, ' + ':::' + _('Enable to use the series name as the iTunes Genre, ' 'iBooks Category'), _('Cache covers from iTunes/iBooks') + ':::' + _('Enable to cache and display covers from iTunes/iBooks'), - _(u'"Copy files to iTunes Media folder %s" is enabled in iTunes Preferences|Advanced')%u'\u2026' + + _(u'"Copy files to iTunes Media folder %s" is enabled in iTunes Preferences|Advanced') % u'\u2026' + ':::' + _("

This setting should match your iTunes Preferences|Advanced setting.

" "

Disabling will store copies of books transferred to iTunes in your calibre configuration directory.

" @@ -133,11 +136,11 @@ class DriverBase(DeviceConfig, DevicePlugin): False, ] - @classmethod def _config_base_name(cls): return 'iTunes' + class ITUNES(DriverBase): ''' Calling sequences: @@ -158,6 +161,7 @@ class ITUNES(DriverBase): can_handle() set_progress_reporter() books() (once for each storage point) + (create self.cached_books) settings() settings() can_handle() (~1x per second OSX while idle) @@ -191,11 +195,11 @@ class ITUNES(DriverBase): name = 'Apple iTunes interface' gui_name = _('Apple device') icon = I('devices/ipad.png') - description = _('Communicate with iTunes/iBooks.') - supported_platforms = ['osx','windows'] + description = _('Communicate with iTunes/iBooks.') + supported_platforms = ['osx', 'windows'] author = 'GRiker' #: The version of this plugin as a 3-tuple (major, minor, revision) - version = (1,0,0) + version = (1, 1, 1) DISPLAY_DISABLE_DIALOG = "display_disable_apple_driver_dialog" @@ -290,7 +294,7 @@ class ITUNES(DriverBase): archive_path = os.path.join(cache_dir, "thumbs.zip") description_prefix = "added by calibre" ejected = False - iTunes= None + iTunes = None iTunes_local_storage = None library_orphans = None manual_sync_mode = False @@ -325,21 +329,21 @@ class ITUNES(DriverBase): # Delete any obsolete copies of the book from the booklist if self.update_list: if False: - self._dump_booklist(booklists[0], header='before',indent=2) - self._dump_update_list(header='before',indent=2) - self._dump_cached_books(header='before',indent=2) + self._dump_booklist(booklists[0], header='before', indent=2) + self._dump_update_list(header='before', indent=2) + self._dump_cached_books(header='before', indent=2) - for (j,p_book) in enumerate(self.update_list): + for (j, p_book) in enumerate(self.update_list): if False: if isosx: logger().info(" looking for '%s' by %s uuid:%s" % - (p_book['title'],p_book['author'], p_book['uuid'])) + (p_book['title'], p_book['author'], p_book['uuid'])) elif iswindows: logger().info(" looking for '%s' by %s (%s)" % - (p_book['title'],p_book['author'], p_book['uuid'])) + (p_book['title'], p_book['author'], p_book['uuid'])) # Purge the booklist, self.cached_books - for i,bl_book in enumerate(booklists[0]): + for i, bl_book in enumerate(booklists[0]): if bl_book.uuid == p_book['uuid']: # Remove from booklists[0] booklists[0].pop(i) @@ -363,12 +367,12 @@ class ITUNES(DriverBase): if self.cached_books[cb]['title'] == p_book['title'] and \ self.cached_books[cb]['author'] == p_book['author']: if DEBUG: - self._dump_cached_book(self.cached_books[cb],header="removing from self.cached_books:", indent=2) + self._dump_cached_book(self.cached_books[cb], header="removing from self.cached_books:", indent=2) self.cached_books.pop(cb) break break if self.report_progress is not None: - self.report_progress((j+1)/task_count, _('Updating device metadata listing...')) + self.report_progress((j + 1) / task_count, _('Updating device metadata listing...')) if self.report_progress is not None: self.report_progress(1.0, _('Updating device metadata listing...')) @@ -383,8 +387,8 @@ class ITUNES(DriverBase): booklists[0].append(new_book) if False: - self._dump_booklist(booklists[0],header='after',indent=2) - self._dump_cached_books(header='after',indent=2) + self._dump_booklist(booklists[0], header='after', indent=2) + self._dump_cached_books(header='after', indent=2) def books(self, oncard=None, end_session=True): """ @@ -409,7 +413,7 @@ class ITUNES(DriverBase): else: logger().info(" Cover fetching/caching disabled") - # Fetch a list of books from iPod device connected to iTunes + # Fetch a list of books from iDevice connected to iTunes if 'iPod' in self.sources: booklist = BookList(logger()) cached_books = {} @@ -418,10 +422,10 @@ class ITUNES(DriverBase): library_books = self._get_library_books() device_books = self._get_device_books() book_count = float(len(device_books)) - for (i,book) in enumerate(device_books): + for (i, book) in enumerate(device_books): this_book = Book(book.name(), book.artist()) format = 'pdf' if book.kind().startswith('PDF') else 'epub' - this_book.path = self.path_template % (book.name(), book.artist(),format) + this_book.path = self.path_template % (book.name(), book.artist(), format) try: this_book.datetime = parse_date(str(book.date_added())).timetuple() except: @@ -439,16 +443,17 @@ class ITUNES(DriverBase): booklist.add_book(this_book, False) cached_books[this_book.path] = { - 'title':book.name(), - 'author':book.artist().split(' & '), - 'lib_book':library_books[this_book.path] if this_book.path in library_books else None, - 'dev_book':book, + 'title': book.name(), + 'author': book.artist(), + 'authors': book.artist().split(' & '), + 'lib_book': library_books[this_book.path] if this_book.path in library_books else None, + 'dev_book': book, 'uuid': book.composer() } if self.report_progress is not None: - self.report_progress((i+1)/book_count, - _('%(num)d of %(tot)d') % dict(num=i+1, tot=book_count)) + self.report_progress((i + 1) / book_count, + _('%(num)d of %(tot)d') % dict(num=i + 1, tot=book_count)) self._purge_orphans(library_books, cached_books) elif iswindows: @@ -459,10 +464,10 @@ class ITUNES(DriverBase): library_books = self._get_library_books() device_books = self._get_device_books() book_count = float(len(device_books)) - for (i,book) in enumerate(device_books): + for (i, book) in enumerate(device_books): this_book = Book(book.Name, book.Artist) format = 'pdf' if book.KindAsString.startswith('PDF') else 'epub' - this_book.path = self.path_template % (book.Name, book.Artist,format) + this_book.path = self.path_template % (book.Name, book.Artist, format) try: this_book.datetime = parse_date(str(book.DateAdded)).timetuple() except: @@ -479,16 +484,17 @@ class ITUNES(DriverBase): booklist.add_book(this_book, False) cached_books[this_book.path] = { - 'title':book.Name, - 'author':book.Artist.split(' & '), - 'lib_book':library_books[this_book.path] if this_book.path in library_books else None, + 'title': book.Name, + 'author': book.Artist, + 'authors': book.Artist.split(' & '), + 'lib_book': library_books[this_book.path] if this_book.path in library_books else None, 'uuid': book.Composer, 'format': 'pdf' if book.KindAsString.startswith('PDF') else 'epub' } if self.report_progress is not None: - self.report_progress((i+1)/book_count, - _('%(num)d of %(tot)d') % dict(num=i+1, + self.report_progress((i + 1) / book_count, + _('%(num)d of %(tot)d') % dict(num=i + 1, tot=book_count)) self._purge_orphans(library_books, cached_books) @@ -500,7 +506,7 @@ class ITUNES(DriverBase): self.cached_books = cached_books if DEBUG: self._dump_booklist(booklist, 'returning from books()', indent=2) - self._dump_cached_books('returning from books()',indent=2) + self._dump_cached_books('returning from books()', indent=2) return booklist else: return BookList(logger()) @@ -556,7 +562,7 @@ class ITUNES(DriverBase): self.sources = self._get_sources() if (not 'iPod' in self.sources) or (self.sources['iPod'] == ''): attempts -= 1 - time.sleep(0.5) + time.sleep(1.0) if DEBUG: logger().warning(" waiting for connected iDevice, attempt #%d" % (10 - attempts)) else: @@ -570,7 +576,7 @@ class ITUNES(DriverBase): self.ejected = True return False - self._discover_manual_sync_mode(wait = 2 if self.initial_status == 'launched' else 0) + self._discover_manual_sync_mode(wait=2 if self.initial_status == 'launched' else 0) return True def can_handle_windows(self, device_id, debug=False): @@ -634,9 +640,9 @@ class ITUNES(DriverBase): self.sources = self._get_sources() if (not 'iPod' in self.sources) or (self.sources['iPod'] == ''): attempts -= 1 - time.sleep(0.5) + time.sleep(1.0) if DEBUG: - logger().warning(" waiting for connected iPad, attempt #%d" % (10 - attempts)) + logger().warning(" waiting for connected iDevice, attempt #%d" % (10 - attempts)) else: if DEBUG: logger().info(' found connected iPad in iTunes') @@ -666,7 +672,7 @@ class ITUNES(DriverBase): ('place', None) (None, None) ''' - return (None,None) + return (None, None) @classmethod def config_widget(cls): @@ -720,22 +726,25 @@ class ITUNES(DriverBase): else: if self.manual_sync_mode: metadata = MetaInformation(self.cached_books[path]['title'], - [self.cached_books[path]['author']]) + self.cached_books[path]['authors']) + metadata.author = self.cached_books[path]['author'] metadata.uuid = self.cached_books[path]['uuid'] + if not metadata.uuid: + metadata.uuid = "unknown" if isosx: - self._remove_existing_copy(self.cached_books[path],metadata) + self._remove_existing_copy(self.cached_books[path], metadata) elif iswindows: try: pythoncom.CoInitialize() self.iTunes = win32com.client.Dispatch("iTunes.Application") - self._remove_existing_copy(self.cached_books[path],metadata) + self._remove_existing_copy(self.cached_books[path], metadata) finally: pythoncom.CoUninitialize() else: self.problem_titles.append("'%s' by %s" % - (self.cached_books[path]['title'],self.cached_books[path]['author'])) + (self.cached_books[path]['title'], self.cached_books[path]['author'])) def eject(self): ''' @@ -799,7 +808,7 @@ class ITUNES(DriverBase): except: logger().error(' waiting for free_space() call to go through') - return (free_space,-1,-1) + return (free_space, -1, -1) def get_device_information(self, end_session=True): """ @@ -809,7 +818,7 @@ class ITUNES(DriverBase): if DEBUG: logger().info("%s.get_device_information()" % self.__class__.__name__) - return (self.sources['iPod'],'hw v1.0','sw v1.0', 'mime type normally goes here') + return (self.sources['iPod'], 'hw v1.0', 'sw v1.0', 'mime type normally goes here') def get_file(self, path, outfile, end_session=True): ''' @@ -848,28 +857,32 @@ class ITUNES(DriverBase): raise OpenFeedback(self.ITUNES_SANDBOX_LOCKOUT_MESSAGE) if DEBUG: - VENDOR_ID = "0x%x" % connected_device[0] - PRODUCT_ID = "0x%x" % connected_device[1] - BCD = "0x%x" % connected_device[2] - MFG = connected_device[3] - MODEL = connected_device[4] + vendor_id = "0x%x" % connected_device[0] + product_id = "0x%x" % connected_device[1] + bcd = "0x%x" % connected_device[2] + mfg = connected_device[3] + model = connected_device[4] logger().info("%s.open(MFG: %s, VENDOR_ID: %s, MODEL: %s, BCD: %s, PRODUCT_ID: %s)" % (self.__class__.__name__, - MFG, - VENDOR_ID, - MODEL, - BCD, - PRODUCT_ID + mfg, + vendor_id, + model, + bcd, + product_id )) # Display a dialog recommending using 'Connect to iTunes' if user hasn't # previously disabled the dialog - if dynamic.get(confirm_config_name(self.DISPLAY_DISABLE_DIALOG),True): + if dynamic.get(confirm_config_name(self.DISPLAY_DISABLE_DIALOG), True): raise AppleOpenFeedback(self) else: if DEBUG: logger().error(" %s" % self.UNSUPPORTED_DIRECT_CONNECT_MODE_MESSAGE) + # Log supported DEVICE_IDs and BCDs + logger().info(" BCD: %s" % ['0x%x' % x for x in sorted(self.BCD)]) + logger().info(" PRODUCT_ID: %s" % ['0x%x' % x for x in sorted(self.PRODUCT_ID)]) + # Confirm/create thumbs archive if not os.path.exists(self.cache_dir): if DEBUG: @@ -879,7 +892,7 @@ class ITUNES(DriverBase): if not os.path.exists(self.archive_path): logger().info(" creating zip archive") zfw = ZipFile(self.archive_path, mode='w') - zfw.writestr("iTunes Thumbs Archive",'') + zfw.writestr("iTunes Thumbs Archive", '') zfw.close() else: if DEBUG: @@ -887,7 +900,7 @@ class ITUNES(DriverBase): # If enabled in config options, create/confirm an iTunes storage folder if not self.settings().extra_customization[self.USE_ITUNES_STORAGE]: - self.iTunes_local_storage = os.path.join(config_dir,'iTunes storage') + self.iTunes_local_storage = os.path.join(config_dir, 'iTunes storage') if not os.path.exists(self.iTunes_local_storage): if DEBUG: logger()(" creating iTunes_local_storage at '%s'" % self.iTunes_local_storage) @@ -916,37 +929,41 @@ class ITUNES(DriverBase): logger().info(" looking for '%s' by '%s' uuid:%s" % (self.cached_books[path]['title'], self.cached_books[path]['author'], - self.cached_books[path]['uuid'])) + repr(self.cached_books[path]['uuid']))) # Purge the booklist, self.cached_books, thumb cache - for i,bl_book in enumerate(booklists[0]): + for i, bl_book in enumerate(booklists[0]): if False: - logger().info(" evaluating '%s' by '%s' uuid:%s" % - (bl_book.title, bl_book.author,bl_book.uuid)) + logger().info(" evaluating '%s' by '%s' uuid:%s" % + (bl_book.title, bl_book.author, bl_book.uuid)) found = False - if bl_book.uuid == self.cached_books[path]['uuid']: - if False: - logger().info(" matched with uuid") + if bl_book.uuid and bl_book.uuid == self.cached_books[path]['uuid']: + if True: + logger().info(" --matched uuid") booklists[0].pop(i) found = True elif bl_book.title == self.cached_books[path]['title'] and \ - bl_book.author[0] == self.cached_books[path]['author']: - if False: - logger().info(" matched with title + author") + bl_book.author == self.cached_books[path]['author']: + if True: + logger().info(" --matched title + author") booklists[0].pop(i) found = True if found: # Remove from self.cached_books for cb in self.cached_books: - if self.cached_books[cb]['uuid'] == self.cached_books[path]['uuid']: + if (self.cached_books[cb]['uuid'] == self.cached_books[path]['uuid'] and + self.cached_books[cb]['author'] == self.cached_books[path]['author'] and + self.cached_books[cb]['title'] == self.cached_books[path]['title']): self.cached_books.pop(cb) break + else: + logger().error(" '%s' not found in self.cached_books" % self.cached_books[path]['title']) # Remove from thumb from thumb cache thumb_path = path.rpartition('.')[0] + '.jpg' - zf = ZipFile(self.archive_path,'a') + zf = ZipFile(self.archive_path, 'a') fnames = zf.namelist() try: thumb = [x for x in fnames if thumb_path in x][0] @@ -965,14 +982,16 @@ class ITUNES(DriverBase): else: if DEBUG: logger().error(" unable to find '%s' by '%s' (%s)" % - (bl_book.title, bl_book.author,bl_book.uuid)) + (self.cached_books[path]['title'], + self.cached_books[path]['author'], + self.cached_books[path]['uuid'])) if False: - self._dump_booklist(booklists[0], indent = 2) + self._dump_booklist(booklists[0], indent=2) self._dump_cached_books(indent=2) def reset(self, key='-1', log_packets=False, report_progress=None, - detected_device=None) : + detected_device=None): """ :key: The key to unlock the device :log_packets: If true the packet stream to/from the device is logged @@ -1051,7 +1070,7 @@ class ITUNES(DriverBase): connected_device = self.sources['iPod'] capacity = self.iTunes.sources[connected_device].capacity() - return (capacity,-1,-1) + return (capacity, -1, -1) def upload_books(self, files, names, on_card=None, end_session=True, metadata=None): @@ -1085,7 +1104,7 @@ class ITUNES(DriverBase): logger().info("%s.upload_books()" % self.__class__.__name__) if isosx: - for (i,fpath) in enumerate(files): + for (i, fpath) in enumerate(files): format = fpath.rpartition('.')[2].lower() path = self.path_template % (metadata[i].title, authors_to_string(metadata[i].authors), @@ -1110,12 +1129,12 @@ class ITUNES(DriverBase): 'format': format, 'lib_book': lb_added, 'title': metadata[i].title, - 'uuid': metadata[i].uuid } + 'uuid': metadata[i].uuid} # Report progress if self.report_progress is not None: - self.report_progress((i+1)/file_count, - _('%(num)d of %(tot)d') % dict(num=i+1, tot=file_count)) + self.report_progress((i + 1) / file_count, + _('%(num)d of %(tot)d') % dict(num=i + 1, tot=file_count)) elif iswindows: import pythoncom, win32com.client @@ -1124,7 +1143,7 @@ class ITUNES(DriverBase): pythoncom.CoInitialize() self.iTunes = win32com.client.Dispatch("iTunes.Application") - for (i,fpath) in enumerate(files): + for (i, fpath) in enumerate(files): format = fpath.rpartition('.')[2].lower() path = self.path_template % (metadata[i].title, authors_to_string(metadata[i].authors), @@ -1160,8 +1179,8 @@ class ITUNES(DriverBase): # Report progress if self.report_progress is not None: - self.report_progress((i+1)/file_count, - _('%(num)d of %(tot)d') % dict(num=i+1, tot=file_count)) + self.report_progress((i + 1) / file_count, + _('%(num)d of %(tot)d') % dict(num=i + 1, tot=file_count)) finally: pythoncom.CoUninitialize() @@ -1174,12 +1193,12 @@ class ITUNES(DriverBase): self.update_msg = "Added books to device" if False: - self._dump_booklist(new_booklist,header="after upload_books()",indent=2) - self._dump_cached_books(header="after upload_books()",indent=2) + self._dump_booklist(new_booklist, header="after upload_books()", indent=2) + self._dump_cached_books(header="after upload_books()", indent=2) return (new_booklist, [], []) # Private methods - def _add_device_book(self,fpath, metadata): + def _add_device_book(self, fpath, metadata): ''' assumes pythoncom wrapper for windows ''' @@ -1201,7 +1220,7 @@ class ITUNES(DriverBase): delay = 1.0 while attempts: try: - added = pl.add(appscript.mactypes.File(fpath),to=pl) + added = pl.add(appscript.mactypes.File(fpath), to=pl) if False: logger().info(" '%s' added to Device|Books" % metadata.title) break @@ -1282,13 +1301,13 @@ class ITUNES(DriverBase): base_fn = fpath.rpartition(os.sep)[2] base_fn = base_fn.rpartition('.')[0] db_added = self._find_device_book( - { 'title': base_fn if format == 'pdf' else metadata.title, + {'title': base_fn if format == 'pdf' else metadata.title, 'author': authors_to_string(metadata.authors), 'uuid': metadata.uuid, 'format': format}) return db_added - def _add_library_book(self,file, metadata): + def _add_library_book(self, file, metadata): ''' windows assumes pythoncom wrapper ''' @@ -1349,7 +1368,7 @@ class ITUNES(DriverBase): base_fn = file.rpartition(os.sep)[2] base_fn = base_fn.rpartition('.')[0] added = self._find_library_book( - { 'title': base_fn if format == 'pdf' else metadata.title, + {'title': base_fn if format == 'pdf' else metadata.title, 'author': authors_to_string(metadata.authors), 'uuid': metadata.uuid, 'format': format}) @@ -1372,7 +1391,7 @@ class ITUNES(DriverBase): # If using iTunes_local_storage, copy the file, redirect iTunes to use local copy if not self.settings().extra_customization[self.USE_ITUNES_STORAGE]: local_copy = os.path.join(self.iTunes_local_storage, str(metadata.uuid) + os.path.splitext(fpath)[1]) - shutil.copyfile(fpath,local_copy) + shutil.copyfile(fpath, local_copy) fpath = local_copy if self.manual_sync_mode: @@ -1418,18 +1437,18 @@ class ITUNES(DriverBase): if scaled: if DEBUG: logger().info(" cover scaled from %sx%s to %sx%s" % - (width,height,nwidth,nheight)) + (width, height, nwidth, nheight)) img = img.resize((nwidth, nheight), PILImage.ANTIALIAS) cd = cStringIO.StringIO() img.convert('RGB').save(cd, 'JPEG') cover_data = cd.getvalue() cd.close() else: - with open(metadata.cover,'r+b') as cd: + with open(metadata.cover, 'r+b') as cd: cover_data = cd.read() except: self.problem_titles.append("'%s' by %s" % (metadata.title, authors_to_string(metadata.authors))) - logger().error(" error scaling '%s' for '%s'" % (metadata.cover,metadata.title)) + logger().error(" error scaling '%s' for '%s'" % (metadata.cover, metadata.title)) import traceback traceback.print_exc() @@ -1468,7 +1487,7 @@ class ITUNES(DriverBase): elif iswindows: ''' Write the data to a real file for Windows iTunes ''' tc = os.path.join(tempfile.gettempdir(), "cover.jpg") - with open(tc,'wb') as tmp_cover: + with open(tc, 'wb') as tmp_cover: tmp_cover.write(cover_data) if lb_added: @@ -1506,13 +1525,13 @@ class ITUNES(DriverBase): # Refresh the thumbnail cache if DEBUG: - logger().info( " refreshing cached thumb for '%s'" % metadata.title) + logger().info(" refreshing cached thumb for '%s'" % metadata.title) zfw = ZipFile(self.archive_path, mode='a') thumb_path = path.rpartition('.')[0] + '.jpg' zfw.writestr(thumb_path, thumb) except: self.problem_titles.append("'%s' by %s" % (metadata.title, authors_to_string(metadata.authors))) - logger().error(" error converting '%s' to thumb for '%s'" % (metadata.cover,metadata.title)) + logger().error(" error converting '%s' to thumb for '%s'" % (metadata.cover, metadata.title)) finally: try: zfw.close() @@ -1523,7 +1542,7 @@ class ITUNES(DriverBase): logger().info(" no cover defined in metadata for '%s'" % metadata.title) return thumb - def _create_new_book(self,fpath, metadata, path, db_added, lb_added, thumb, format): + def _create_new_book(self, fpath, metadata, path, db_added, lb_added, thumb, format): ''' ''' if DEBUG: @@ -1604,7 +1623,7 @@ class ITUNES(DriverBase): if DEBUG: logger().info(" adding tracer to empty Books|Playlist") try: - added = pl.add(appscript.mactypes.File(P('tracer.epub')),to=pl) + added = pl.add(appscript.mactypes.File(P('tracer.epub')), to=pl) time.sleep(0.5) added.delete() self.manual_sync_mode = True @@ -1635,9 +1654,9 @@ class ITUNES(DriverBase): if DEBUG: logger().info(" sending tracer to empty Books|Playlist") fpath = P('tracer.epub') - mi = MetaInformation('Tracer',['calibre']) + mi = MetaInformation('Tracer', ['calibre']) try: - added = self._add_device_book(fpath,mi) + added = self._add_device_book(fpath, mi) time.sleep(0.5) added.Delete() self.manual_sync_mode = True @@ -1646,40 +1665,40 @@ class ITUNES(DriverBase): logger().info(" iTunes.manual_sync_mode: %s" % self.manual_sync_mode) - def _dump_booklist(self, booklist, header=None,indent=0): + def _dump_booklist(self, booklist, header=None, indent=0): ''' ''' if header: - msg = '\n%sbooklist %s:' % (' '*indent,header) + msg = '\n%sbooklist %s:' % (' ' * indent, header) logger().info(msg) - logger().info('%s%s' % (' '*indent,'-' * len(msg))) + logger().info('%s%s' % (' ' * indent, '-' * len(msg))) for book in booklist: if isosx: - logger().info("%s%-40.40s %-30.30s %-10.10s %s" % - (' '*indent,book.title, book.author, str(book.library_id)[-9:], book.uuid)) + logger().info("%s%-40.40s %-30.30s %-40.40s %-10.10s" % + (' ' * indent, book.title, book.author, book.uuid, str(book.library_id)[-9:])) elif iswindows: logger().info("%s%-40.40s %-30.30s" % - (' '*indent,book.title, book.author)) + (' ' * indent, book.title, book.author)) logger().info() - def _dump_cached_book(self, cached_book, header=None,indent=0): + def _dump_cached_book(self, cached_book, header=None, indent=0): ''' ''' if isosx: if header: - msg = '%s%s' % (' '*indent,header) + msg = '%s%s' % (' ' * indent, header) logger().info(msg) - logger().info( "%s%s" % (' '*indent, '-' * len(msg))) + logger().info("%s%s" % (' ' * indent, '-' * len(msg))) logger().info("%s%-40.40s %-30.30s %-10.10s %-10.10s %s" % - (' '*indent, + (' ' * indent, 'title', 'author', 'lib_book', 'dev_book', 'uuid')) logger().info("%s%-40.40s %-30.30s %-10.10s %-10.10s %s" % - (' '*indent, + (' ' * indent, cached_book['title'], cached_book['author'], str(cached_book['lib_book'])[-9:], @@ -1687,12 +1706,12 @@ class ITUNES(DriverBase): cached_book['uuid'])) elif iswindows: if header: - msg = '%s%s' % (' '*indent,header) + msg = '%s%s' % (' ' * indent, header) logger().info(msg) - logger().info( "%s%s" % (' '*indent, '-' * len(msg))) + logger().info("%s%s" % (' ' * indent, '-' * len(msg))) logger().info("%s%-40.40s %-30.30s %s" % - (' '*indent, + (' ' * indent, cached_book['title'], cached_book['author'], cached_book['uuid'])) @@ -1701,22 +1720,23 @@ class ITUNES(DriverBase): ''' ''' if header: - msg = '\n%sself.cached_books %s:' % (' '*indent,header) + msg = '\n%sself.cached_books %s:' % (' ' * indent, header) logger().info(msg) - logger().info( "%s%s" % (' '*indent,'-' * len(msg))) + logger().info("%s%s" % (' ' * indent, '-' * len(msg))) if isosx: for cb in self.cached_books.keys(): - logger().info("%s%-40.40s %-30.30s %-10.10s %-10.10s %s" % - (' '*indent, + logger().info("%s%-40.40s %-30.30s %-40.40s %-10.10s %-10.10s" % + (' ' * indent, self.cached_books[cb]['title'], self.cached_books[cb]['author'], + self.cached_books[cb]['uuid'], str(self.cached_books[cb]['lib_book'])[-9:], str(self.cached_books[cb]['dev_book'])[-9:], - self.cached_books[cb]['uuid'])) + )) elif iswindows: for cb in self.cached_books.keys(): logger().info("%s%-40.40s %-30.30s %-4.4s %s" % - (' '*indent, + (' ' * indent, self.cached_books[cb]['title'], self.cached_books[cb]['author'], self.cached_books[cb]['format'], @@ -1733,7 +1753,7 @@ class ITUNES(DriverBase): title = None author = None timestamp = None - zf = ZipFile(fpath,'r') + zf = ZipFile(fpath, 'r') fnames = zf.namelist() opf = [x for x in fnames if '.opf' in x][0] if opf: @@ -1742,14 +1762,14 @@ class ITUNES(DriverBase): opf_raw.close() title = soup.find('dc:title').renderContents() author = soup.find('dc:creator').renderContents() - ts = soup.find('meta',attrs={'name':'calibre:timestamp'}) + ts = soup.find('meta', attrs={'name': 'calibre:timestamp'}) if ts: # Touch existing calibre timestamp timestamp = ts['content'] if not title or not author: if DEBUG: - logger().error(" couldn't extract title/author from %s in %s" % (opf,fpath)) + logger().error(" couldn't extract title/author from %s in %s" % (opf, fpath)) logger().error(" title: %s author: %s timestamp: %s" % (title, author, timestamp)) else: if DEBUG: @@ -1760,14 +1780,15 @@ class ITUNES(DriverBase): def _dump_hex(self, src, length=16): ''' ''' - FILTER=''.join([(len(repr(chr(x)))==3) and chr(x) or '.' for x in range(256)]) - N=0; result='' + FILTER = ''.join([(len(repr(chr(x))) == 3) and chr(x) or '.' for x in range(256)]) + N = 0 + result = '' while src: - s,src = src[:length],src[length:] - hexa = ' '.join(["%02X"%ord(x) for x in s]) - s = s.translate(FILTER) - result += "%04X %-*s %s\n" % (N, length*3, hexa, s) - N+=length + s, src = src[:length], src[length:] + hexa = ' '.join(["%02X" % ord(x) for x in s]) + s = s.translate(FILTER) + result += "%04X %-*s %s\n" % (N, length * 3, hexa, s) + N += length print result def _dump_library_books(self, library_books): @@ -1779,16 +1800,16 @@ class ITUNES(DriverBase): logger().info(" %s" % book) logger().info() - def _dump_update_list(self,header=None,indent=0): + def _dump_update_list(self, header=None, indent=0): if header and self.update_list: - msg = '\n%sself.update_list %s' % (' '*indent,header) + msg = '\n%sself.update_list %s' % (' ' * indent, header) logger().info(msg) - logger().info( "%s%s" % (' '*indent,'-' * len(msg))) + logger().info("%s%s" % (' ' * indent, '-' * len(msg))) if isosx: for ub in self.update_list: logger().info("%s%-40.40s %-30.30s %-10.10s %s" % - (' '*indent, + (' ' * indent, ub['title'], ub['author'], str(ub['lib_book'])[-9:], @@ -1796,7 +1817,7 @@ class ITUNES(DriverBase): elif iswindows: for ub in self.update_list: logger().info("%s%-40.40s %-30.30s" % - (' '*indent, + (' ' * indent, ub['title'], ub['author'])) @@ -1809,14 +1830,14 @@ class ITUNES(DriverBase): if DEBUG: logger().info(" %s._find_device_book()" % self.__class__.__name__) logger().info(" searching for '%s' by '%s' (%s)" % - (search['title'], search['author'],search['uuid'])) + (search['title'], search['author'], search['uuid'])) attempts = 9 while attempts: # Try by uuid - only one hit if 'uuid' in search and search['uuid']: if DEBUG: logger().info(" searching by uuid '%s' ..." % search['uuid']) - hits = dev_books.Search(search['uuid'],self.SearchField.index('All')) + hits = dev_books.Search(search['uuid'], self.SearchField.index('All')) if hits: hit = hits[0] logger().info(" found '%s' by %s (%s)" % (hit.Name, hit.Artist, hit.Composer)) @@ -1826,7 +1847,7 @@ class ITUNES(DriverBase): if search['author']: if DEBUG: logger().info(" searching by author '%s' ..." % search['author']) - hits = dev_books.Search(search['author'],self.SearchField.index('Artists')) + hits = dev_books.Search(search['author'], self.SearchField.index('Artists')) if hits: for hit in hits: if hit.Name == search['title']: @@ -1837,7 +1858,7 @@ class ITUNES(DriverBase): # Search by title if no author available if DEBUG: logger().info(" searching by title '%s' ..." % search['title']) - hits = dev_books.Search(search['title'],self.SearchField.index('All')) + hits = dev_books.Search(search['title'], self.SearchField.index('All')) if hits: for hit in hits: if hit.Name == search['title']: @@ -1851,8 +1872,8 @@ class ITUNES(DriverBase): title = re.sub(r'[^0-9a-zA-Z ]', '_', search['title']) author = re.sub(r'[^0-9a-zA-Z ]', '_', search['author']) if DEBUG: - logger().info(" searching by name: '%s - %s'" % (title,author)) - hits = dev_books.Search('%s - %s' % (title,author), + logger().info(" searching by name: '%s - %s'" % (title, author)) + hits = dev_books.Search('%s - %s' % (title, author), self.SearchField.index('All')) if hits: hit = hits[0] @@ -1910,14 +1931,13 @@ class ITUNES(DriverBase): if DEBUG: logger().error(" no Books playlist found") - attempts = 9 while attempts: # Find book whose Album field = search['uuid'] if 'uuid' in search and search['uuid']: if DEBUG: logger().info(" searching by uuid '%s' ..." % search['uuid']) - hits = lib_books.Search(search['uuid'],self.SearchField.index('All')) + hits = lib_books.Search(search['uuid'], self.SearchField.index('All')) if hits: hit = hits[0] if DEBUG: @@ -1928,7 +1948,7 @@ class ITUNES(DriverBase): if search['author']: if DEBUG: logger().info(" searching by author '%s' ..." % search['author']) - hits = lib_books.Search(search['author'],self.SearchField.index('Artists')) + hits = lib_books.Search(search['author'], self.SearchField.index('Artists')) if hits: for hit in hits: if hit.Name == search['title']: @@ -1939,7 +1959,7 @@ class ITUNES(DriverBase): # Search by title if no author available if DEBUG: logger().info(" searching by title '%s' ..." % search['title']) - hits = lib_books.Search(search['title'],self.SearchField.index('All')) + hits = lib_books.Search(search['title'], self.SearchField.index('All')) if hits: for hit in hits: if hit.Name == search['title']: @@ -1953,8 +1973,8 @@ class ITUNES(DriverBase): title = re.sub(r'[^0-9a-zA-Z ]', '_', search['title']) author = re.sub(r'[^0-9a-zA-Z ]', '_', search['author']) if DEBUG: - logger().info(" searching by name: %s - %s" % (title,author)) - hits = lib_books.Search('%s - %s' % (title,author), + logger().info(" searching by name: %s - %s" % (title, author)) + hits = lib_books.Search('%s - %s' % (title, author), self.SearchField.index('All')) if hits: hit = hits[0] @@ -2027,11 +2047,11 @@ class ITUNES(DriverBase): try: img_data = cStringIO.StringIO(data) im = PILImage.open(img_data) - scaled, width, height = fit_image(im.size[0],im.size[1], 60, 80) - im = im.resize((int(width),int(height)), PILImage.ANTIALIAS) + scaled, width, height = fit_image(im.size[0], im.size[1], 60, 80) + im = im.resize((int(width), int(height)), PILImage.ANTIALIAS) thumb = cStringIO.StringIO() - im.convert('RGB').save(thumb,'JPEG') + im.convert('RGB').save(thumb, 'JPEG') thumb_data = thumb.getvalue() thumb.close() if False: @@ -2051,7 +2071,6 @@ class ITUNES(DriverBase): return thumb_data - elif iswindows: if not book.Artwork.Count: if DEBUG: @@ -2067,10 +2086,10 @@ class ITUNES(DriverBase): book.Artwork.Item(1).SaveArtworkToFile(tmp_thumb) # Resize the cover im = PILImage.open(tmp_thumb) - scaled, width, height = fit_image(im.size[0],im.size[1], 60, 80) - im = im.resize((int(width),int(height)), PILImage.ANTIALIAS) + scaled, width, height = fit_image(im.size[0], im.size[1], 60, 80) + im = im.resize((int(width), int(height)), PILImage.ANTIALIAS) thumb = cStringIO.StringIO() - im.convert('RGB').save(thumb,'JPEG') + im.convert('RGB').save(thumb, 'JPEG') thumb_data = thumb.getvalue() os.remove(tmp_thumb) thumb.close() @@ -2083,7 +2102,7 @@ class ITUNES(DriverBase): logger().error(" error generating thumb for '%s', caching empty marker" % book.Name) thumb_data = None # Cache the empty cover - zfw.writestr(thumb_path,'None') + zfw.writestr(thumb_path, 'None') finally: zfw.close() @@ -2097,7 +2116,7 @@ class ITUNES(DriverBase): exploded_file_size = compressed_size format = file.rpartition('.')[2].lower() if format == 'epub': - myZip = ZipFile(file,'r') + myZip = ZipFile(file, 'r') myZipList = myZip.infolist() exploded_file_size = 0 for file in myZipList: @@ -2133,14 +2152,13 @@ class ITUNES(DriverBase): logger().error(" book_playlist not found") for book in dev_books: - # This may need additional entries for international iTunes users if book.kind() in self.Audiobooks: if DEBUG: logger().info(" ignoring '%s' of type '%s'" % (book.name(), book.kind())) else: if DEBUG: - logger().info(" %-30.30s %-30.30s %-40.40s [%s]" % - (book.name(), book.artist(), book.album(), book.kind())) + logger().info(" %-40.40s %-30.30s %-40.40s [%s]" % + (book.name(), book.artist(), book.composer(), book.kind())) device_books.append(book) if DEBUG: logger().info() @@ -2167,13 +2185,12 @@ class ITUNES(DriverBase): logger().info(" no Books playlist found") for book in dev_books: - # This may need additional entries for international iTunes users if book.KindAsString in self.Audiobooks: if DEBUG: logger().info(" ignoring '%s' of type '%s'" % (book.Name, book.KindAsString)) else: if DEBUG: - logger().info(" %-30.30s %-30.30s %-40.40s [%s]" % (book.Name, book.Artist, book.Album, book.KindAsString)) + logger().info(" %-40.40s %-30.30s %-40.40s [%s]" % (book.Name, book.Artist, book.Composer, book.KindAsString)) device_books.append(book) if DEBUG: logger().info() @@ -2247,7 +2264,7 @@ class ITUNES(DriverBase): else: # Collect calibre orphans - remnants of recipe uploads format = 'pdf' if book.kind().startswith('PDF') else 'epub' - path = self.path_template % (book.name(), book.artist(),format) + path = self.path_template % (book.name(), book.artist(), format) if str(book.description()).startswith(self.description_prefix): try: if book.location() == appscript.k.missing_value: @@ -2304,7 +2321,7 @@ class ITUNES(DriverBase): logger().info(" ignoring %-30.30s of type '%s'" % (book.Name, book.KindAsString)) else: format = 'pdf' if book.KindAsString.startswith('PDF') else 'epub' - path = self.path_template % (book.Name, book.Artist,format) + path = self.path_template % (book.Name, book.Artist, format) # Collect calibre orphans if book.Description.startswith(self.description_prefix): @@ -2356,7 +2373,7 @@ class ITUNES(DriverBase): return {} elif iswindows: # Assumes a pythoncom wrapper - it_sources = ['Unknown','Library','iPod','AudioCD','MP3CD','Device','RadioTuner','SharedLibrary'] + it_sources = ['Unknown', 'Library', 'iPod', 'AudioCD', 'MP3CD', 'Device', 'RadioTuner', 'SharedLibrary'] names = [s.name for s in self.iTunes.sources] kinds = [it_sources[s.kind] for s in self.iTunes.sources] @@ -2369,12 +2386,12 @@ class ITUNES(DriverBase): kinds.pop(index) names.pop(index) - return dict(zip(kinds,names)) + return dict(zip(kinds, names)) - def _is_alpha(self,char): + def _is_alpha(self, char): ''' ''' - if not re.search('[a-zA-Z]',char): + if not re.search('[a-zA-Z]', char): return False else: return True @@ -2396,7 +2413,7 @@ class ITUNES(DriverBase): running_apps = appscript.app('System Events') if not 'iTunes' in running_apps.processes.name(): if DEBUG: - logger().info( "%s:_launch_iTunes(): Launching iTunes" % self.__class__.__name__) + logger().info("%s:_launch_iTunes(): Launching iTunes" % self.__class__.__name__) try: self.iTunes = iTunes = appscript.app('iTunes', hide=True) except: @@ -2422,7 +2439,7 @@ class ITUNES(DriverBase): except: # Try static binding import itunes - self.iTunes = appscript.app('iTunes',terms=itunes) + self.iTunes = appscript.app('iTunes', terms=itunes) try: foo = self.iTunes.name() as_binding = "static" @@ -2450,7 +2467,7 @@ class ITUNES(DriverBase): logger().info(" [OSX %s, %s %s (%s), %s driver version %d.%d.%d]" % (platform.mac_ver()[0], self.iTunes.name(), self.iTunes.version(), self.initial_status, - self.__class__.__name__, self.version[0],self.version[1],self.version[2])) + self.__class__.__name__, self.version[0], self.version[1], self.version[2])) logger().info(" communicating with iTunes via %s %s using %s binding" % (as_name, as_version, as_binding)) logger().info(" calibre_library_path: %s" % self.calibre_library_path) @@ -2519,41 +2536,47 @@ class ITUNES(DriverBase): logger().info(" %s %s" % (__appname__, __version__)) logger().info(" [Windows %s - %s (%s), driver version %d.%d.%d]" % (self.iTunes.Windows[0].name, self.iTunes.Version, self.initial_status, - self.version[0],self.version[1],self.version[2])) + self.version[0], self.version[1], self.version[2])) logger().info(" calibre_library_path: %s" % self.calibre_library_path) - def _purge_orphans(self,library_books, cached_books): + def _purge_orphans(self, library_books, cached_books): ''' Scan library_books for any paths not on device Remove any iTunes orphans originally added by calibre This occurs when the user deletes a book in iBooks while disconnected ''' - if DEBUG: - logger().info(" %s._purge_orphans()" % self.__class__.__name__) - #self._dump_library_books(library_books) - #logger().info(" cached_books:\n %s" % "\n ".join(cached_books.keys())) + PURGE_ORPHANS = False - for book in library_books: - if isosx: - if book not in cached_books and \ - str(library_books[book].description()).startswith(self.description_prefix): - if DEBUG: - logger().info(" '%s' not found on iDevice, removing from iTunes" % book) - btr = { 'title':library_books[book].name(), - 'author':library_books[book].artist(), - 'lib_book':library_books[book]} - self._remove_from_iTunes(btr) - elif iswindows: - if book not in cached_books and \ - library_books[book].Description.startswith(self.description_prefix): - if DEBUG: - logger().info(" '%s' not found on iDevice, removing from iTunes" % book) - btr = { 'title':library_books[book].Name, - 'author':library_books[book].Artist, - 'lib_book':library_books[book]} - self._remove_from_iTunes(btr) - if DEBUG: - logger().info() + if PURGE_ORPHANS: + if DEBUG: + logger().info(" %s._purge_orphans()" % self.__class__.__name__) + #self._dump_library_books(library_books) + #logger().info(" cached_books:\n %s" % "\n ".join(cached_books.keys())) + + for book in library_books: + if isosx: + if book not in cached_books and \ + str(library_books[book].description()).startswith(self.description_prefix): + if DEBUG: + logger().info(" '%s' not found on iDevice, removing from iTunes" % book) + btr = { + 'title': library_books[book].name(), + 'author': library_books[book].artist(), + 'lib_book': library_books[book]} + self._remove_from_iTunes(btr) + elif iswindows: + if book not in cached_books and \ + library_books[book].Description.startswith(self.description_prefix): + if DEBUG: + logger().info(" '%s' not found on iDevice, removing from iTunes" % book) + btr = { + 'title': library_books[book].Name, + 'author': library_books[book].Artist, + 'lib_book': library_books[book]} + self._remove_from_iTunes(btr) + else: + if DEBUG: + logger().info(" %s._purge_orphans(disabled)" % self.__class__.__name__) def _remove_existing_copy(self, path, metadata): ''' @@ -2565,17 +2588,11 @@ class ITUNES(DriverBase): # Delete existing from Device|Books, add to self.update_list # for deletion from booklist[0] during add_books_to_metadata for book in self.cached_books: - if self.cached_books[book]['uuid'] == metadata.uuid or \ - (self.cached_books[book]['title'] == metadata.title and \ - self.cached_books[book]['author'] == authors_to_string(metadata.authors)): + if (self.cached_books[book]['uuid'] == metadata.uuid or + (self.cached_books[book]['title'] == metadata.title and + self.cached_books[book]['author'] == metadata.author)): self.update_list.append(self.cached_books[book]) - - if DEBUG: - logger().info( " deleting device book '%s'" % (metadata.title)) self._remove_from_device(self.cached_books[book]) - - if DEBUG: - logger().info(" deleting library book '%s'" % metadata.title) self._remove_from_iTunes(self.cached_books[book]) break else: @@ -2585,12 +2602,12 @@ class ITUNES(DriverBase): # Delete existing from Library|Books, add to self.update_list # for deletion from booklist[0] during add_books_to_metadata for book in self.cached_books: - if self.cached_books[book]['uuid'] == metadata.uuid or \ - (self.cached_books[book]['title'] == metadata.title and \ - self.cached_books[book]['author'] == authors_to_string(metadata.authors)): + if (self.cached_books[book]['uuid'] == metadata.uuid or + (self.cached_books[book]['title'] == metadata.title and \ + self.cached_books[book]['author'] == metadata.author)): self.update_list.append(self.cached_books[book]) if DEBUG: - logger().info( " deleting library book '%s'" % metadata.title) + logger().info(" deleting library book '%s'" % metadata.title) self._remove_from_iTunes(self.cached_books[book]) break else: @@ -2619,7 +2636,7 @@ class ITUNES(DriverBase): else: if DEBUG: logger().warning(" unable to remove '%s' by '%s' (%s) from device" % - (cached_book['title'],cached_book['author'],cached_book['uuid'])) + (cached_book['title'], cached_book['author'], cached_book['uuid'])) def _remove_from_iTunes(self, cached_book): ''' @@ -2668,7 +2685,8 @@ class ITUNES(DriverBase): except: # We get here if there was an error with .location().path if DEBUG: - logger().info(" '%s' not found in iTunes storage" % cached_book['title']) + logger().info(" '%s' by %s not found in iTunes storage" % + (cached_book['title'], cached_book['author'])) # Delete the book from the iTunes database try: @@ -2749,10 +2767,10 @@ class ITUNES(DriverBase): metadata_x = self._xform_metadata_via_plugboard(metadata, 'epub') # Refresh epub metadata - with open(fpath,'r+b') as zfo: + with open(fpath, 'r+b') as zfo: if False: try: - zf_opf = ZipFile(fpath,'r') + zf_opf = ZipFile(fpath, 'r') fnames = zf_opf.namelist() opf = [x for x in fnames if '.opf' in x][0] except: @@ -2769,7 +2787,7 @@ class ITUNES(DriverBase): timestamp = ts.get('content') old_ts = parse_date(timestamp) metadata.timestamp = datetime.datetime(old_ts.year, old_ts.month, old_ts.day, old_ts.hour, - old_ts.minute, old_ts.second, old_ts.microsecond+1, old_ts.tzinfo) + old_ts.minute, old_ts.second, old_ts.microsecond + 1, old_ts.tzinfo) if DEBUG: logger().info(" existing timestamp: %s" % metadata.timestamp) else: @@ -2789,10 +2807,10 @@ class ITUNES(DriverBase): if _('News') in metadata_x.tags or \ _('Catalog') in metadata_x.tags: if metadata_x.title.find('[') > 0: - metadata_x.title = metadata_x.title[:metadata_x.title.find('[')-1] + metadata_x.title = metadata_x.title[:metadata_x.title.find('[') - 1] date_as_author = '%s, %s %s, %s' % (strftime('%A'), strftime('%B'), strftime('%d').lstrip('0'), strftime('%Y')) metadata_x.author = metadata_x.authors = [date_as_author] - sort_author = re.sub('^\s*A\s+|^\s*The\s+|^\s*An\s+', '', metadata_x.title).rstrip() + sort_author = re.sub('^\s*A\s+|^\s*The\s+|^\s*An\s+', '', metadata_x.title).rstrip() metadata_x.author_sort = '%s %s' % (sort_author, strftime('%Y-%m-%d')) # Remove any non-alpha category tags @@ -2873,7 +2891,7 @@ class ITUNES(DriverBase): 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.description.set("%s %s" % (self.description_prefix, strftime('%Y-%m-%d %H:%M:%S'))) lb_added.enabled.set(True) lb_added.sort_artist.set(icu_title(metadata_x.author_sort)) lb_added.sort_name.set(metadata_x.title_sort) @@ -2884,7 +2902,7 @@ class ITUNES(DriverBase): 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.description.set("%s %s" % (self.description_prefix, strftime('%Y-%m-%d %H:%M:%S'))) db_added.enabled.set(True) db_added.sort_artist.set(icu_title(metadata_x.author_sort)) db_added.sort_name.set(metadata_x.title_sort) @@ -2892,17 +2910,17 @@ class ITUNES(DriverBase): if metadata_x.comments: if lb_added: - lb_added.comment.set(STRIP_TAGS.sub('',metadata_x.comments)) + lb_added.comment.set(STRIP_TAGS.sub('', metadata_x.comments)) if db_added: - db_added.comment.set(STRIP_TAGS.sub('',metadata_x.comments)) + db_added.comment.set(STRIP_TAGS.sub('', metadata_x.comments)) if metadata_x.rating: if lb_added: - lb_added.rating.set(metadata_x.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_x.rating*10) + db_added.rating.set(metadata_x.rating * 10) except: pass @@ -2917,7 +2935,7 @@ class ITUNES(DriverBase): # Format the index as a sort key index = metadata_x.series_index integer = int(index) - fraction = index-integer + fraction = index - integer series_index = '%04d%s' % (integer, str('%0.4f' % fraction).lstrip('0')) if lb_added: # If no title_sort plugboard tweak, create sort_name from series/index @@ -2953,7 +2971,6 @@ class ITUNES(DriverBase): db_added.genre.set(tag) break - elif metadata_x.tags is not None: if DEBUG: logger().info(" %susing Tag as Genre" % @@ -2972,7 +2989,7 @@ class ITUNES(DriverBase): 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.Description = ("%s %s" % (self.description_prefix, strftime('%Y-%m-%d %H:%M:%S'))) lb_added.Enabled = True lb_added.SortArtist = icu_title(metadata_x.author_sort) lb_added.SortName = metadata_x.title_sort @@ -2985,7 +3002,7 @@ class ITUNES(DriverBase): 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.Description = ("%s %s" % (self.description_prefix, strftime('%Y-%m-%d %H:%M:%S'))) db_added.Enabled = True db_added.SortArtist = icu_title(metadata_x.author_sort) db_added.SortName = metadata_x.title_sort @@ -2993,17 +3010,17 @@ class ITUNES(DriverBase): if metadata_x.comments: if lb_added: - lb_added.Comment = (STRIP_TAGS.sub('',metadata_x.comments)) + lb_added.Comment = (STRIP_TAGS.sub('', metadata_x.comments)) if db_added: - db_added.Comment = (STRIP_TAGS.sub('',metadata_x.comments)) + db_added.Comment = (STRIP_TAGS.sub('', metadata_x.comments)) if metadata_x.rating: if lb_added: - lb_added.AlbumRating = (metadata_x.rating*10) + lb_added.AlbumRating = (metadata_x.rating * 10) # iBooks currently doesn't allow setting rating ... ? try: if db_added: - db_added.AlbumRating = (metadata_x.rating*10) + db_added.AlbumRating = (metadata_x.rating * 10) except: if DEBUG: logger().warning(" iTunes automation interface reported an error" @@ -3019,7 +3036,7 @@ class ITUNES(DriverBase): # Format the index as a sort key index = metadata_x.series_index integer = int(index) - fraction = index-integer + fraction = index - integer series_index = '%04d%s' % (integer, str('%0.4f' % fraction).lstrip('0')) if lb_added: # If no title_sort plugboard tweak, create sort_name from series/index @@ -3147,6 +3164,7 @@ class ITUNES(DriverBase): newmi = book return newmi + class ITUNES_ASYNC(ITUNES): ''' This subclass allows the user to interact directly with iTunes via a menu option @@ -3155,14 +3173,14 @@ class ITUNES_ASYNC(ITUNES): name = 'iTunes interface' gui_name = 'Apple iTunes' icon = I('devices/itunes.png') - description = _('Communicate with iTunes.') + description = _('Communicate with iTunes.') # Plugboard ID DEVICE_PLUGBOARD_NAME = 'APPLE' connected = False - def __init__(self,path): + def __init__(self, path): if DEBUG: logger().info("%s.__init__()" % self.__class__.__name__) @@ -3228,7 +3246,7 @@ class ITUNES_ASYNC(ITUNES): if isosx: library_books = self._get_library_books() book_count = float(len(library_books)) - for (i,book) in enumerate(library_books): + for (i, book) in enumerate(library_books): format = 'pdf' if library_books[book].kind().startswith('PDF') else 'epub' this_book = Book(library_books[book].name(), library_books[book].artist()) #this_book.path = library_books[book].location().path @@ -3253,17 +3271,17 @@ class ITUNES_ASYNC(ITUNES): booklist.add_book(this_book, False) cached_books[this_book.path] = { - 'title':library_books[book].name(), - 'author':library_books[book].artist().split(' & '), - 'lib_book':library_books[book], - 'dev_book':None, + 'title': library_books[book].name(), + 'author': library_books[book].artist().split(' & '), + 'lib_book': library_books[book], + 'dev_book': None, 'uuid': library_books[book].composer(), 'format': format } if self.report_progress is not None: - self.report_progress((i+1)/book_count, - _('%(num)d of %(tot)d') % dict(num=i+1, tot=book_count)) + self.report_progress((i + 1) / book_count, + _('%(num)d of %(tot)d') % dict(num=i + 1, tot=book_count)) elif iswindows: import pythoncom, win32com.client @@ -3273,7 +3291,7 @@ class ITUNES_ASYNC(ITUNES): self.iTunes = win32com.client.Dispatch("iTunes.Application") library_books = self._get_library_books() book_count = float(len(library_books)) - for (i,book) in enumerate(library_books): + for (i, book) in enumerate(library_books): this_book = Book(library_books[book].Name, library_books[book].Artist) format = 'pdf' if library_books[book].KindAsString.startswith('PDF') else 'epub' this_book.path = self.path_template % (library_books[book].Name, @@ -3296,16 +3314,16 @@ class ITUNES_ASYNC(ITUNES): booklist.add_book(this_book, False) cached_books[this_book.path] = { - 'title':library_books[book].Name, - 'author':library_books[book].Artist.split(' & '), - 'lib_book':library_books[book], + 'title': library_books[book].Name, + 'author': library_books[book].Artist.split(' & '), + 'lib_book': library_books[book], 'uuid': library_books[book].Composer, 'format': format } if self.report_progress is not None: - self.report_progress((i+1)/book_count, - _('%(num)d of %(tot)d') % dict(num=i+1, + self.report_progress((i + 1) / book_count, + _('%(num)d of %(tot)d') % dict(num=i + 1, tot=book_count)) finally: @@ -3316,7 +3334,7 @@ class ITUNES_ASYNC(ITUNES): self.cached_books = cached_books if DEBUG: self._dump_booklist(booklist, 'returning from books()', indent=2) - self._dump_cached_books('returning from books()',indent=2) + self._dump_cached_books('returning from books()', indent=2) return booklist else: @@ -3352,7 +3370,7 @@ class ITUNES_ASYNC(ITUNES): free_bytes = ctypes.c_ulonglong(0) ctypes.windll.kernel32.GetDiskFreeSpaceExW(ctypes.c_wchar_p(os.sep), None, None, ctypes.pointer(free_bytes)) free_space = free_bytes.value - return (free_space,-1,-1) + return (free_space, -1, -1) def get_device_information(self, end_session=True): """ @@ -3362,7 +3380,7 @@ class ITUNES_ASYNC(ITUNES): if DEBUG: logger().info("%s.get_device_information()" % self.__class__.__name__) - return ('iTunes','hw v1.0','sw v1.0', 'mime type normally goes here') + return ('iTunes', 'hw v1.0', 'sw v1.0', 'mime type normally goes here') def is_usb_connected(self, devices_on_system, debug=False, only_presence=False): @@ -3398,7 +3416,7 @@ class ITUNES_ASYNC(ITUNES): if not os.path.exists(self.archive_path): logger().info(" creating zip archive") zfw = ZipFile(self.archive_path, mode='w') - zfw.writestr("iTunes Thumbs Archive",'') + zfw.writestr("iTunes Thumbs Archive", '') zfw.close() else: if DEBUG: @@ -3406,7 +3424,7 @@ class ITUNES_ASYNC(ITUNES): # If enabled in config options, create/confirm an iTunes storage folder if not self.settings().extra_customization[self.USE_ITUNES_STORAGE]: - self.iTunes_local_storage = os.path.join(config_dir,'iTunes storage') + self.iTunes_local_storage = os.path.join(config_dir, 'iTunes storage') if not os.path.exists(self.iTunes_local_storage): if DEBUG: logger()(" creating iTunes_local_storage at '%s'" % self.iTunes_local_storage) @@ -3441,6 +3459,7 @@ class ITUNES_ASYNC(ITUNES): logger().info("%s.unmount_device()" % self.__class__.__name__) self.connected = False + class BookList(list): ''' A list of books. Each Book object must have the fields: @@ -3493,16 +3512,17 @@ class BookList(list): ''' return {} + class Book(Metadata): ''' A simple class describing a book in the iTunes Books Library. See ebooks.metadata.book.base ''' - def __init__(self,title,author): + def __init__(self, title, author): Metadata.__init__(self, title, authors=author.split(' & ')) + self.author = author self.author_sort = author_to_author_sort(author) @property def title_sorter(self): return title_sort(self.title) - diff --git a/src/calibre/library/catalogs/csv_xml.py b/src/calibre/library/catalogs/csv_xml.py index 49df903320..fd2bb5113b 100644 --- a/src/calibre/library/catalogs/csv_xml.py +++ b/src/calibre/library/catalogs/csv_xml.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai -__license__ = 'GPL v3' +__license__ = 'GPL v3' __copyright__ = '2012, Kovid Goyal ' __docformat__ = 'restructuredtext en' @@ -12,6 +12,7 @@ from calibre.customize import CatalogPlugin from calibre.library.catalogs import FIELDS from calibre.customize.conversion import DummyReporter + class CSV_XML(CatalogPlugin): 'CSV/XML catalog generator' @@ -22,27 +23,27 @@ class CSV_XML(CatalogPlugin): supported_platforms = ['windows', 'osx', 'linux'] author = 'Greg Riker' version = (1, 0, 0) - file_types = set(['csv','xml']) + file_types = set(['csv', 'xml']) cli_options = [ Option('--fields', - default = 'all', - dest = 'fields', - action = None, - help = _('The fields to output when cataloging books in the ' + default='all', + dest='fields', + action=None, + help=_('The fields to output when cataloging books in the ' 'database. Should be a comma-separated list of fields.\n' 'Available fields: %(fields)s,\n' 'plus user-created custom fields.\n' 'Example: %(opt)s=title,authors,tags\n' "Default: '%%default'\n" - "Applies to: CSV, XML output formats")%dict( + "Applies to: CSV, XML output formats") % dict( fields=', '.join(FIELDS), opt='--fields')), Option('--sort-by', - default = 'id', - dest = 'sort_by', - action = None, - help = _('Output field to sort on.\n' + default='id', + dest='sort_by', + action=None, + help=_('Output field to sort on.\n' 'Available fields: author_sort, id, rating, size, timestamp, title_sort\n' "Default: '%default'\n" "Applies to: CSV, XML output formats"))] @@ -97,7 +98,7 @@ class CSV_XML(CatalogPlugin): for entry in data: entry['ondevice'] = db.catalog_plugin_on_device_temp_mapping[entry['id']]['ondevice'] - fm = {x:db.field_metadata.get(x, {}) for x in fields} + fm = {x: db.field_metadata.get(x, {}) for x in fields} if self.fmt == 'csv': outfile = codecs.open(path_to_output, 'w', 'utf8') @@ -113,7 +114,7 @@ class CSV_XML(CatalogPlugin): outstr = [] for field in fields: if field.startswith('#'): - item = db.get_field(entry['id'],field,index_is_id=True) + item = db.get_field(entry['id'], field, index_is_id=True) elif field == 'library_name': item = current_library elif field == 'title_sort': @@ -129,7 +130,7 @@ class CSV_XML(CatalogPlugin): for format in item: fmt_list.append(format.rpartition('.')[2].lower()) item = ', '.join(fmt_list) - elif field in ['authors','tags']: + elif field in ['authors', 'tags']: item = ', '.join(item) elif field == 'isbn': # Could be 9, 10 or 13 digits @@ -137,20 +138,20 @@ class CSV_XML(CatalogPlugin): elif field in ['pubdate', 'timestamp']: item = isoformat(item) elif field == 'comments': - item = item.replace(u'\r\n',u' ') - item = item.replace(u'\n',u' ') + item = item.replace(u'\r\n', u' ') + item = item.replace(u'\n', u' ') elif fm.get(field, {}).get('datatype', None) == 'rating' and item: - item = u'%.2g'%(item/2.0) + item = u'%.2g' % (item / 2.0) # Convert HTML to markdown text if type(item) is unicode: - opening_tag = re.search('<(\w+)(\x20|>)',item) + opening_tag = re.search('<(\w+)(\x20|>)', item) if opening_tag: closing_tag = re.search('<\/%s>$' % opening_tag.group(1), item) if closing_tag: item = html2text(item) - outstr.append(u'"%s"' % unicode(item).replace('"','""')) + outstr.append(u'"%s"' % unicode(item).replace('"', '""')) outfile.write(u','.join(outstr) + u'\n') outfile.close() @@ -165,14 +166,14 @@ class CSV_XML(CatalogPlugin): for field in fields: if field.startswith('#'): - val = db.get_field(r['id'],field,index_is_id=True) + val = db.get_field(r['id'], field, index_is_id=True) if not isinstance(val, (str, unicode)): val = unicode(val) - item = getattr(E, field.replace('#','_'))(val) + item = getattr(E, field.replace('#', '_'))(val) record.append(item) for field in ('id', 'uuid', 'publisher', 'rating', 'size', - 'isbn','ondevice', 'identifiers'): + 'isbn', 'ondevice', 'identifiers'): if field in fields: val = r[field] if not val: @@ -180,7 +181,7 @@ class CSV_XML(CatalogPlugin): if not isinstance(val, (str, unicode)): if (fm.get(field, {}).get('datatype', None) == 'rating' and val): - val = u'%.2g'%(val/2.0) + val = u'%.2g' % (val / 2.0) val = unicode(val) item = getattr(E, field)(val) record.append(item) @@ -227,4 +228,3 @@ class CSV_XML(CatalogPlugin): with open(path_to_output, 'w') as f: f.write(etree.tostring(root, encoding='utf-8', xml_declaration=True, pretty_print=True)) - diff --git a/src/calibre/library/catalogs/epub_mobi_builder.py b/src/calibre/library/catalogs/epub_mobi_builder.py index fa38b2ba83..24049e83f0 100644 --- a/src/calibre/library/catalogs/epub_mobi_builder.py +++ b/src/calibre/library/catalogs/epub_mobi_builder.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -__license__ = 'GPL v3' +__license__ = 'GPL v3' __copyright__ = '2010, Greg Riker' import datetime, htmlentitydefs, os, platform, re, shutil, unicodedata, zlib @@ -25,6 +25,7 @@ from calibre.utils.icu import capitalize, collation_order, sort_key from calibre.utils.magick.draw import thumbnail from calibre.utils.zipfile import ZipFile + class CatalogBuilder(object): ''' Generates catalog source files from calibre database @@ -50,7 +51,7 @@ class CatalogBuilder(object): # Multiple numbers create 'Last x days', 'x to y days ago' ... # e.g, [7,15,30,60] or [30] # [] = No date ranges added - DATE_RANGE=[30] + DATE_RANGE = [30] # Text used in generated catalog for title section with other-than-ASCII leading letter SYMBOLS = _('Symbols') @@ -98,7 +99,6 @@ class CatalogBuilder(object): else: return ' ' - def __init__(self, db, _opts, plugin, report_progress=DummyReporter(), stylesheet="content/stylesheet.css", @@ -120,11 +120,13 @@ class CatalogBuilder(object): _opts.output_profile and _opts.output_profile.startswith("kindle")) else False + self.all_series = set() self.authors = None self.bookmarked_books = None self.bookmarked_books_by_date_read = None self.books_by_author = None self.books_by_date_range = None + self.books_by_description = None self.books_by_month = None self.books_by_series = None self.books_by_title = None @@ -135,11 +137,12 @@ class CatalogBuilder(object): self.generate_recently_read = False self.genres = [] self.genre_tags_dict = \ - self.filter_genre_tags(max_len = 245 - len("%s/Genre_.html" % self.content_dir)) \ + self.filter_genre_tags(max_len=245 - len("%s/Genre_.html" % self.content_dir)) \ if self.opts.generate_genres else None self.html_filelist_1 = [] self.html_filelist_2 = [] - self.merge_comments_rule = dict(zip(['field','position','hr'], + self.individual_authors = None + self.merge_comments_rule = dict(zip(['field', 'position', 'hr'], _opts.merge_comments_rule.split(':'))) self.ncx_soup = None self.output_profile = self.get_output_profile(_opts) @@ -154,6 +157,7 @@ class CatalogBuilder(object): self.total_steps = 6.0 self.use_series_prefix_in_titles_section = False + self.dump_custom_fields() self.books_to_catalog = self.fetch_books_to_catalog() self.compute_total_steps() self.calculate_thumbnail_dimensions() @@ -202,7 +206,7 @@ class CatalogBuilder(object): else: index = book['series_index'] integer = int(index) - fraction = index-integer + fraction = index - integer series_index = '%04d%s' % (integer, str('%0.4f' % fraction).lstrip('0')) key = '%s ~%s %s' % (self._kf_author_to_author_sort(book['author']), self.generate_sort_title(book['series']), @@ -228,7 +232,7 @@ class CatalogBuilder(object): else: index = book['series_index'] integer = int(index) - fraction = index-integer + fraction = index - integer series_index = u'%04d%s' % (integer, str(u'%0.4f' % fraction).lstrip(u'0')) fs = u'{:<%d}~{!s}{!s}' % longest_author_sort key = fs.format(capitalize(book['author_sort']), @@ -239,7 +243,7 @@ class CatalogBuilder(object): def _kf_books_by_series_sorter(self, book): index = book['series_index'] integer = int(index) - fraction = index-integer + fraction = index - integer series_index = '%04d%s' % (integer, str('%0.4f' % fraction).lstrip('0')) key = '%s %s' % (self.generate_sort_title(book['series']), series_index) @@ -335,8 +339,8 @@ class CatalogBuilder(object): self.thumb_height = self.thumb_width * 1.33 if 'kindle' in x.short_name and self.opts.fmt == 'mobi': # Kindle DPI appears to be off by a factor of 2 - self.thumb_width = self.thumb_width/2 - self.thumb_height = self.thumb_height/2 + self.thumb_width = self.thumb_width / 2 + self.thumb_height = self.thumb_height / 2 break if self.opts.verbose: self.opts.log(" Thumbnails:") @@ -395,7 +399,7 @@ class CatalogBuilder(object): self.opts.log.info(' creating thumbnail archive, thumb_width: %1.2f"' % float(self.opts.thumb_width)) with ZipFile(self.thumbs_path, mode='w') as zfw: - zfw.writestr("Catalog Thumbs Archive",'') + zfw.writestr("Catalog Thumbs Archive", '') else: try: with ZipFile(self.thumbs_path, mode='r') as zfr: @@ -410,9 +414,9 @@ class CatalogBuilder(object): if float(cached_thumb_width) != float(self.opts.thumb_width): self.opts.log.warning(" invalidating cache at '%s'" % self.thumbs_path) self.opts.log.warning(' thumb_width changed: %1.2f" => %1.2f"' % - (float(cached_thumb_width),float(self.opts.thumb_width))) + (float(cached_thumb_width), float(self.opts.thumb_width))) with ZipFile(self.thumbs_path, mode='w') as zfw: - zfw.writestr("Catalog Thumbs Archive",'') + zfw.writestr("Catalog Thumbs Archive", '') else: self.opts.log.info(' existing thumb cache at %s, cached_thumb_width: %1.2f"' % (self.thumbs_path, float(cached_thumb_width))) @@ -447,7 +451,7 @@ class CatalogBuilder(object): hits.remove(amp) for hit in hits: name = hit[1:-1] - if htmlentitydefs.name2codepoint.has_key(name): + if htmlentitydefs.name2codepoint in name: s = s.replace(hit, unichr(htmlentitydefs.name2codepoint[name])) s = s.replace(amp, "&") return s @@ -468,17 +472,17 @@ class CatalogBuilder(object): self.create_catalog_directory_structure() catalog_resources = P("catalog") - files_to_copy = [('','DefaultCover.jpg'), - ('content','stylesheet.css')] + files_to_copy = [('', 'DefaultCover.jpg'), + ('content', 'stylesheet.css')] if self.generate_for_kindle_mobi: - files_to_copy.extend([('images','mastheadImage.gif')]) + files_to_copy.extend([('images', 'mastheadImage.gif')]) for file in files_to_copy: if file[0] == '': - shutil.copy(os.path.join(catalog_resources,file[1]), + shutil.copy(os.path.join(catalog_resources, file[1]), self.catalog_path) else: - shutil.copy(os.path.join(catalog_resources,file[1]), + shutil.copy(os.path.join(catalog_resources, file[1]), os.path.join(self.catalog_path, file[0])) if self.generate_for_kindle_mobi: @@ -530,14 +534,14 @@ class CatalogBuilder(object): authors = [(record['author'], record['author_sort']) for record in books_by_author] current_author = authors[0] - for (i,author) in enumerate(authors): + for (i, author) in enumerate(authors): if author != current_author and i: if author[0] == current_author[0]: if self.opts.fmt == 'mobi': # Exit if building MOBI error_msg = _("

Inconsistent Author Sort values for Author
" + "'{!s}':

".format(author[0]) + - "

{!s} != {!s}

".format(author[1],current_author[1]) + + "

{!s} != {!s}

".format(author[1], current_author[1]) + "

Unable to build MOBI catalog.
" + "Select all books by '{!s}', apply correct Author Sort value in Edit Metadata dialog, then rebuild the catalog.\n

".format(author[0])) @@ -553,7 +557,7 @@ class CatalogBuilder(object): self.error.append('Author Sort mismatch') error_msg = _("Warning: Inconsistent Author Sort values for Author '{!s}':\n".format(author[0]) + - " {!s} != {!s}\n".format(author[1],current_author[1])) + " {!s} != {!s}\n".format(author[1], current_author[1])) self.opts.log.warn('\n*** Metadata warning ***') self.opts.log.warn(error_msg) self.error.append(error_msg) @@ -576,7 +580,7 @@ class CatalogBuilder(object): """ def _log_prefix_rule_match_info(rule, record, matched): self.opts.log.info(" %s '%s' by %s (%s: '%s' contains '%s')" % - (rule['prefix'],record['title'], + (rule['prefix'], record['title'], record['authors'][0], rule['name'], self.db.metadata_for_field(rule['field'])['name'], matched)) @@ -585,10 +589,10 @@ class CatalogBuilder(object): for rule in self.prefix_rules: # Literal comparison for Tags field if rule['field'].lower() == 'tags': - if rule['pattern'].lower() in map(unicode.lower,record['tags']): - if self.opts.verbose: + if rule['pattern'].lower() in map(unicode.lower, record['tags']): + if self.DEBUG and self.opts.verbose: self.opts.log.info(" %s '%s' by %s (%s: Tags includes '%s')" % - (rule['prefix'],record['title'], + (rule['prefix'], record['title'], record['authors'][0], rule['name'], rule['pattern'])) return rule['prefix'] @@ -602,7 +606,7 @@ class CatalogBuilder(object): if field_contents == '': field_contents = None - if (self.db.metadata_for_field(rule['field'])['datatype'] == 'bool' and + if (self.db.metadata_for_field(rule['field'])['datatype'] == 'bool' and field_contents is None): # Handle condition where field is a bool and contents is None, # which is displayed as No @@ -616,7 +620,7 @@ class CatalogBuilder(object): try: if re.search(rule['pattern'], unicode(field_contents), re.IGNORECASE) is not None: - if self.opts.verbose: + if self.DEBUG: _log_prefix_rule_match_info(rule, record, field_contents) return rule['prefix'] except: @@ -624,12 +628,24 @@ class CatalogBuilder(object): self.opts.log.error("pattern failed to compile: %s" % rule['pattern']) pass elif field_contents is None and rule['pattern'] == 'None': - if self.opts.verbose: + if self.DEBUG: _log_prefix_rule_match_info(rule, record, field_contents) return rule['prefix'] return None + def dump_custom_fields(self): + """ + Dump custom field mappings for debugging + """ + if self.opts.verbose: + self.opts.log.info(" Custom fields:") + all_custom_fields = self.db.custom_field_keys() + for cf in all_custom_fields: + self.opts.log.info(" %-20s %-20s %s" % + (cf, "'%s'" % self.db.metadata_for_field(cf)['name'], + self.db.metadata_for_field(cf)['datatype'])) + def establish_equivalencies(self, item_list, key=None): """ Return icu equivalent sort letter. @@ -647,9 +663,9 @@ class CatalogBuilder(object): # Hack to force the cataloged leading letter to be # an unadorned character if the accented version sorts before the unaccented exceptions = { - u'Ä':u'A', - u'Ö':u'O', - u'Ü':u'U' + u'Ä': u'A', + u'Ö': u'O', + u'Ü': u'U' } if key is not None: @@ -697,7 +713,7 @@ class CatalogBuilder(object): print(" establish_equivalencies():") if key: for idx, item in enumerate(item_list): - print(" %s %s" % (cl_list[idx],item[sort_field])) + print(" %s %s" % (cl_list[idx], item[sort_field])) else: print(" %s %s" % (cl_list[idx], item)) @@ -716,7 +732,8 @@ class CatalogBuilder(object): Outputs: books_by_author: database, sorted by author - authors: list of unique authors + authors: list of book authors. Two credited authors are considered an + individual entity error: author_sort mismatches Return: @@ -728,6 +745,12 @@ class CatalogBuilder(object): books_by_author = list(self.books_to_catalog) self.detect_author_sort_mismatches(books_by_author) + + # Assumes books_by_title already populated + # init books_by_description before relisting multiple authors + books_by_description = list(books_by_author) if self.opts.sort_descriptions_by_author \ + else list(self.books_by_title) + if self.opts.cross_reference_authors: books_by_author = self.relist_multiple_authors(books_by_author) @@ -737,16 +760,19 @@ class CatalogBuilder(object): asl = [i['author_sort'] for i in books_by_author] las = max(asl, key=len) + self.books_by_description = sorted(books_by_description, + key=lambda x: sort_key(self._kf_books_by_author_sorter_author_sort(x, len(las)))) + books_by_author = sorted(books_by_author, key=lambda x: sort_key(self._kf_books_by_author_sorter_author_sort(x, len(las)))) if self.DEBUG and self.opts.verbose: tl = [i['title'] for i in books_by_author] lt = max(tl, key=len) - fs = '{:<6}{:<%d} {:<%d} {!s}' % (len(lt),len(las)) - print(fs.format('','Title','Author','Series')) + fs = '{:<6}{:<%d} {:<%d} {!s}' % (len(lt), len(las)) + print(fs.format('', 'Title', 'Author', 'Series')) for i in books_by_author: - print(fs.format('', i['title'],i['author_sort'],i['series'])) + print(fs.format('', i['title'], i['author_sort'], i['series'])) # Build the unique_authors set from existing data authors = [(record['author'], capitalize(record['author_sort'])) for record in books_by_author] @@ -758,7 +784,8 @@ class CatalogBuilder(object): current_author = authors[0] multiple_authors = False unique_authors = [] - for (i,author) in enumerate(authors): + individual_authors = set() + for (i, author) in enumerate(authors): if author != current_author: # Note that current_author and author are tuples: (friendly, sort) multiple_authors = True @@ -768,7 +795,7 @@ class CatalogBuilder(object): books_by_current_author)) current_author = author books_by_current_author = 1 - elif i==0 and len(authors) == 1: + elif i == 0 and len(authors) == 1: # Allow for single-book lists unique_authors.append((current_author[0], icu_title(current_author[1]), books_by_current_author)) @@ -780,14 +807,23 @@ class CatalogBuilder(object): unique_authors.append((current_author[0], icu_title(current_author[1]), books_by_current_author)) + self.authors = list(unique_authors) + self.books_by_author = books_by_author + + for ua in unique_authors: + for ia in ua[0].replace(' & ', ' & ').split(' & '): + individual_authors.add(ia) + self.individual_authors = list(individual_authors) + if self.DEBUG and self.opts.verbose: self.opts.log.info("\nfetch_books_by_author(): %d unique authors" % len(unique_authors)) for author in unique_authors: self.opts.log.info((u" %-50s %-25s %2d" % (author[0][0:45], author[1][0:20], author[2])).encode('utf-8')) + self.opts.log.info("\nfetch_books_by_author(): %d individual authors" % len(individual_authors)) + for author in sorted(individual_authors): + self.opts.log.info("%s" % author) - self.authors = unique_authors - self.books_by_author = books_by_author return True def fetch_books_by_title(self): @@ -869,6 +905,7 @@ class CatalogBuilder(object): this_title['title'] = self.convert_html_entities(record['title']) if record['series']: this_title['series'] = record['series'] + self.all_series.add(this_title['series']) this_title['series_index'] = record['series_index'] else: this_title['series'] = None @@ -969,11 +1006,11 @@ class CatalogBuilder(object): index_is_id=True) if notes: if field_md['datatype'] == 'text': - if isinstance(notes,list): + if isinstance(notes, list): notes = ' · '.join(notes) elif field_md['datatype'] == 'datetime': - notes = format_date(notes,'dd MMM yyyy') - this_title['notes'] = {'source':field_md['name'],'content':notes} + notes = format_date(notes, 'dd MMM yyyy') + this_title['notes'] = {'source': field_md['name'], 'content': notes} return this_title @@ -1000,7 +1037,7 @@ class CatalogBuilder(object): data = self.plugin.search_sort_db(self.db, self.opts) data = self.process_exclusions(data) - if self.opts.verbose and self.prefix_rules: + if self.prefix_rules and self.DEBUG: self.opts.log.info(" Added prefixes:") # Populate this_title{} from data[{},{}] @@ -1042,6 +1079,7 @@ class CatalogBuilder(object): def initialize(self, save_template): self._save_template = save_template self.SUPPORTS_SUB_DIRS = True + def save_template(self): return self._save_template @@ -1069,8 +1107,8 @@ class CatalogBuilder(object): if bookmark_extension: for vol in storage: - bkmk_path = path_map[id]['path'].replace(os.path.abspath('/'),vol) - bkmk_path = bkmk_path.replace('bookmark',bookmark_extension) + bkmk_path = path_map[id]['path'].replace(os.path.abspath('/'), vol) + bkmk_path = bkmk_path.replace('bookmark', bookmark_extension) if os.path.exists(bkmk_path): path_map[id] = bkmk_path book_ext[id] = book_extension @@ -1109,14 +1147,14 @@ class CatalogBuilder(object): bookmark_ext = path_map[id].rpartition('.')[2] myBookmark = Bookmark(path_map[id], id, book_ext[id], bookmark_ext) try: - book['percent_read'] = min(float(100*myBookmark.last_read / myBookmark.book_length),100) + book['percent_read'] = min(float(100 * myBookmark.last_read / myBookmark.book_length), 100) except: book['percent_read'] = 0 - dots = int((book['percent_read'] + 5)/10) + dots = int((book['percent_read'] + 5) / 10) dot_string = self.SYMBOL_PROGRESS_READ * dots empty_dots = self.SYMBOL_PROGRESS_UNREAD * (10 - dots) - book['reading_progress'] = '%s%s' % (dot_string,empty_dots) - bookmarks[id] = ((myBookmark,book)) + book['reading_progress'] = '%s%s' % (dot_string, empty_dots) + bookmarks[id] = ((myBookmark, book)) self.bookmarked_books = bookmarks @@ -1142,7 +1180,7 @@ class CatalogBuilder(object): else: yield tag - ans = '%s%d %s:\n' % (' ' * indent, len(tags), header) + ans = '%s%d %s:\n' % (' ' * indent, len(tags), header) ans += ' ' * (indent + 1) out_str = '' sorted_tags = sorted(tags, key=sort_key) @@ -1167,11 +1205,11 @@ class CatalogBuilder(object): clipped to max_len """ - normalized = massaged = re.sub('\s','',ascii_text(tag).lower()) - if re.search('\W',normalized): + normalized = massaged = re.sub('\s', '', ascii_text(tag).lower()) + if re.search('\W', normalized): normalized = '' for c in massaged: - if re.search('\W',c): + if re.search('\W', c): normalized += self.generate_unicode_name(c) else: normalized += c @@ -1190,11 +1228,11 @@ class CatalogBuilder(object): else: # Validate custom field is usable as a genre source field_md = self.db.metadata_for_field(self.opts.genre_source_field) - if not field_md['datatype'] in ['enumeration','text']: + if not field_md['datatype'] in ['enumeration', 'text']: all_custom_fields = self.db.custom_field_keys() eligible_custom_fields = [] for cf in all_custom_fields: - if self.db.metadata_for_field(cf)['datatype'] in ['enumeration','text']: + if self.db.metadata_for_field(cf)['datatype'] in ['enumeration', 'text']: eligible_custom_fields.append(cf) self.opts.log.error("Custom genre_source_field must be either:\n" " 'Comma separated text, like tags, shown in the browser',\n" @@ -1224,7 +1262,7 @@ class CatalogBuilder(object): normalized_tags.append(_normalize_tag(tag, max_len)) friendly_tags.append(tag) - genre_tags_dict = dict(zip(friendly_tags,normalized_tags)) + genre_tags_dict = dict(zip(friendly_tags, normalized_tags)) # Test for multiple genres resolving to same normalized form normalized_set = set(normalized_tags) @@ -1286,7 +1324,7 @@ class CatalogBuilder(object): massaged = unicode(BeautifulStoneSoup(description, convertEntities=BeautifulStoneSoup.HTML_ENTITIES)) # Replace '&' with '&' - massaged = re.sub("&","&", massaged) + massaged = re.sub("&", "&", massaged) if massaged.strip() and dest: #print traceback.print_stack(limit=3) @@ -1311,16 +1349,16 @@ class CatalogBuilder(object): if self.opts.fmt == 'mobi': codeTag = Tag(soup, "code") if prefix_char is None: - codeTag.insert(0,NavigableString(' ')) + codeTag.insert(0, NavigableString(' ')) else: - codeTag.insert(0,NavigableString(prefix_char)) + codeTag.insert(0, NavigableString(prefix_char)) return codeTag else: spanTag = Tag(soup, "span") spanTag['class'] = "prefix" if prefix_char is None: prefix_char = " " - spanTag.insert(0,NavigableString(prefix_char)) + spanTag.insert(0, NavigableString(prefix_char)) return spanTag def generate_author_anchor(self, author): @@ -1335,7 +1373,7 @@ class CatalogBuilder(object): Return: (str): asciized version of author """ - return re.sub("\W","", ascii_text(author)) + return re.sub("\W", "", ascii_text(author)) def generate_format_args(self, book): """ Generate the format args for template substitution. @@ -1399,11 +1437,11 @@ class CatalogBuilder(object): current_letter = '' current_series = None # Establish initial letter equivalencies - sort_equivalents = self.establish_equivalencies(self.books_by_author,key='author_sort') + sort_equivalents = self.establish_equivalencies(self.books_by_author, key='author_sort') for idx, book in enumerate(self.books_by_author): book_count += 1 - if self.letter_or_symbol(sort_equivalents[idx]) != current_letter : + if self.letter_or_symbol(sort_equivalents[idx]) != current_letter: # Start a new letter with Index letter if divOpeningTag is not None: divTag.insert(dtc, divOpeningTag) @@ -1427,13 +1465,13 @@ class CatalogBuilder(object): current_letter = self.letter_or_symbol(sort_equivalents[idx]) if current_letter == self.SYMBOLS: aTag['id'] = self.SYMBOLS + '_authors' - pIndexTag.insert(0,aTag) - pIndexTag.insert(1,NavigableString(self.SYMBOLS)) + pIndexTag.insert(0, aTag) + pIndexTag.insert(1, NavigableString(self.SYMBOLS)) else: aTag['id'] = self.generate_unicode_name(current_letter) + '_authors' - pIndexTag.insert(0,aTag) - pIndexTag.insert(1,NavigableString(sort_equivalents[idx])) - divOpeningTag.insert(dotc,pIndexTag) + pIndexTag.insert(0, aTag) + pIndexTag.insert(1, NavigableString(sort_equivalents[idx])) + divOpeningTag.insert(dotc, pIndexTag) dotc += 1 if book['author'] != current_author: @@ -1463,36 +1501,36 @@ class CatalogBuilder(object): pAuthorTag['class'] = "author_index" aTag = Tag(soup, "a") aTag['id'] = "%s" % self.generate_author_anchor(current_author) - aTag.insert(0,NavigableString(current_author)) - pAuthorTag.insert(0,aTag) + aTag.insert(0, NavigableString(current_author)) + pAuthorTag.insert(0, aTag) if author_count == 1: divOpeningTag.insert(dotc, pAuthorTag) dotc += 1 else: - divRunningTag.insert(drtc,pAuthorTag) + divRunningTag.insert(drtc, pAuthorTag) drtc += 1 # Check for series if book['series'] and book['series'] != current_series: # Start a new series current_series = book['series'] - pSeriesTag = Tag(soup,'p') + pSeriesTag = Tag(soup, 'p') pSeriesTag['class'] = "series" if self.opts.fmt == 'mobi': pSeriesTag['class'] = "series_mobi" if self.opts.generate_series: - aTag = Tag(soup,'a') - aTag['href'] = "%s.html#%s" % ('BySeries',self.generate_series_anchor(book['series'])) + aTag = Tag(soup, 'a') + aTag['href'] = "%s.html#%s" % ('BySeries', self.generate_series_anchor(book['series'])) aTag.insert(0, book['series']) pSeriesTag.insert(0, aTag) else: - pSeriesTag.insert(0,NavigableString('%s' % book['series'])) + pSeriesTag.insert(0, NavigableString('%s' % book['series'])) if author_count == 1: divOpeningTag.insert(dotc, pSeriesTag) dotc += 1 elif divRunningTag is not None: - divRunningTag.insert(drtc,pSeriesTag) + divRunningTag.insert(drtc, pSeriesTag) drtc += 1 if current_series and not book['series']: current_series = None @@ -1522,7 +1560,7 @@ class CatalogBuilder(object): #aTag.insert(0,'%s%s' % (escape(book['title']), pubyear)) formatted_title = self.by_authors_normal_title_template.format(**args).rstrip() non_series_books += 1 - aTag.insert(0,NavigableString(escape(formatted_title))) + aTag.insert(0, NavigableString(escape(formatted_title))) spanTag.insert(ptc, aTag) stc += 1 @@ -1533,7 +1571,7 @@ class CatalogBuilder(object): divOpeningTag.insert(dotc, pBookTag) dotc += 1 elif divRunningTag: - divRunningTag.insert(drtc,pBookTag) + divRunningTag.insert(drtc, pBookTag) drtc += 1 # loop ends here @@ -1541,7 +1579,7 @@ class CatalogBuilder(object): pTag = Tag(soup, "p") pTag['class'] = 'title' ptc = 0 - aTag = Tag(soup,'a') + aTag = Tag(soup, 'a') aTag['id'] = 'section_start' pTag.insert(ptc, aTag) ptc += 1 @@ -1550,12 +1588,12 @@ class CatalogBuilder(object): # Kindle don't need this because it shows section titles in Periodical format aTag = Tag(soup, "a") anchor_name = friendly_name.lower() - aTag['id'] = anchor_name.replace(" ","") - pTag.insert(ptc,aTag) + aTag['id'] = anchor_name.replace(" ", "") + pTag.insert(ptc, aTag) ptc += 1 - pTag.insert(ptc,NavigableString('%s' % (friendly_name))) + pTag.insert(ptc, NavigableString('%s' % (friendly_name))) - body.insert(btc,pTag) + body.insert(btc, pTag) btc += 1 if author_count == 1: @@ -1601,9 +1639,9 @@ class CatalogBuilder(object): pIndexTag['class'] = "date_index" aTag = Tag(soup, "a") aTag['id'] = "bda_%s-%s" % (current_date.year, current_date.month) - pIndexTag.insert(0,aTag) - pIndexTag.insert(1,NavigableString(date_string)) - divTag.insert(dtc,pIndexTag) + pIndexTag.insert(0, aTag) + pIndexTag.insert(1, NavigableString(date_string)) + divTag.insert(dtc, pIndexTag) dtc += 1 current_author = None current_series = None @@ -1619,27 +1657,27 @@ class CatalogBuilder(object): aTag = Tag(soup, "a") if self.opts.generate_authors: aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor", self.generate_author_anchor(current_author)) - aTag.insert(0,NavigableString(current_author)) - pAuthorTag.insert(0,aTag) - divTag.insert(dtc,pAuthorTag) + aTag.insert(0, NavigableString(current_author)) + pAuthorTag.insert(0, aTag) + divTag.insert(dtc, pAuthorTag) dtc += 1 # Check for series if new_entry['series'] and new_entry['series'] != current_series: # Start a new series current_series = new_entry['series'] - pSeriesTag = Tag(soup,'p') + pSeriesTag = Tag(soup, 'p') pSeriesTag['class'] = "series" if self.opts.fmt == 'mobi': pSeriesTag['class'] = "series_mobi" if self.opts.generate_series: - aTag = Tag(soup,'a') - aTag['href'] = "%s.html#%s" % ('BySeries',self.generate_series_anchor(new_entry['series'])) + aTag = Tag(soup, 'a') + aTag['href'] = "%s.html#%s" % ('BySeries', self.generate_series_anchor(new_entry['series'])) aTag.insert(0, new_entry['series']) pSeriesTag.insert(0, aTag) else: - pSeriesTag.insert(0,NavigableString('%s' % new_entry['series'])) - divTag.insert(dtc,pSeriesTag) + pSeriesTag.insert(0, NavigableString('%s' % new_entry['series'])) + divTag.insert(dtc, pSeriesTag) dtc += 1 if current_series and not new_entry['series']: current_series = None @@ -1667,7 +1705,7 @@ class CatalogBuilder(object): else: formatted_title = self.by_month_added_normal_title_template.format(**args).rstrip() non_series_books += 1 - aTag.insert(0,NavigableString(escape(formatted_title))) + aTag.insert(0, NavigableString(escape(formatted_title))) spanTag.insert(stc, aTag) stc += 1 @@ -1683,10 +1721,10 @@ class CatalogBuilder(object): pIndexTag = Tag(soup, "p") pIndexTag['class'] = "date_index" aTag = Tag(soup, "a") - aTag['id'] = "bda_%s" % date_range.replace(' ','') - pIndexTag.insert(0,aTag) - pIndexTag.insert(1,NavigableString(date_range)) - divTag.insert(dtc,pIndexTag) + aTag['id'] = "bda_%s" % date_range.replace(' ', '') + pIndexTag.insert(0, aTag) + pIndexTag.insert(1, NavigableString(date_range)) + divTag.insert(dtc, pIndexTag) dtc += 1 for new_entry in date_range_list: @@ -1712,7 +1750,7 @@ class CatalogBuilder(object): formatted_title = self.by_recently_added_series_title_template.format(**args).rstrip() else: formatted_title = self.by_recently_added_normal_title_template.format(**args).rstrip() - aTag.insert(0,NavigableString(escape(formatted_title))) + aTag.insert(0, NavigableString(escape(formatted_title))) spanTag.insert(stc, aTag) stc += 1 @@ -1726,7 +1764,7 @@ class CatalogBuilder(object): if self.opts.generate_authors: aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor", self.generate_author_anchor(new_entry['author'])) aTag.insert(0, NavigableString(new_entry['author'])) - emTag.insert(0,aTag) + emTag.insert(0, aTag) spanTag.insert(stc, emTag) stc += 1 @@ -1749,7 +1787,7 @@ class CatalogBuilder(object): pTag['class'] = 'title' ptc = 0 - aTag = Tag(soup,'a') + aTag = Tag(soup, 'a') aTag['id'] = 'section_start' pTag.insert(ptc, aTag) ptc += 1 @@ -1758,13 +1796,13 @@ class CatalogBuilder(object): # Kindle don't need this because it shows section titles in Periodical format aTag = Tag(soup, "a") anchor_name = friendly_name.lower() - aTag['id'] = anchor_name.replace(" ","") + aTag['id'] = anchor_name.replace(" ", "") - pTag.insert(ptc,aTag) + pTag.insert(ptc, aTag) ptc += 1 pTag.insert(ptc, NavigableString('%s' % friendly_name)) - body.insert(btc,pTag) + body.insert(btc, pTag) btc += 1 divTag = Tag(soup, "div") @@ -1773,23 +1811,23 @@ class CatalogBuilder(object): # >>> Books by date range <<< if self.use_series_prefix_in_titles_section: self.books_by_date_range = sorted(self.books_to_catalog, - key=lambda x:(x['timestamp'], x['timestamp']),reverse=True) + key=lambda x: (x['timestamp'], x['timestamp']), reverse=True) else: nspt = deepcopy(self.books_to_catalog) - self.books_by_date_range = sorted(nspt, key=lambda x:(x['timestamp'], x['timestamp']),reverse=True) + self.books_by_date_range = sorted(nspt, key=lambda x: (x['timestamp'], x['timestamp']), reverse=True) date_range_list = [] today_time = nowf().replace(hour=23, minute=59, second=59) for (i, date) in enumerate(self.DATE_RANGE): date_range_limit = self.DATE_RANGE[i] if i: - date_range = '%d to %d days ago' % (self.DATE_RANGE[i-1], self.DATE_RANGE[i]) + date_range = '%d to %d days ago' % (self.DATE_RANGE[i - 1], self.DATE_RANGE[i]) else: date_range = 'Last %d days' % (self.DATE_RANGE[i]) for book in self.books_by_date_range: book_time = book['timestamp'] - delta = today_time-book_time + delta = today_time - book_time if delta.days <= date_range_limit: date_range_list.append(book) else: @@ -1801,7 +1839,7 @@ class CatalogBuilder(object): # >>>> Books by month <<<< # Sort titles case-insensitive for by month using series prefix self.books_by_month = sorted(self.books_to_catalog, - key=lambda x:(x['timestamp'], x['timestamp']),reverse=True) + key=lambda x: (x['timestamp'], x['timestamp']), reverse=True) # Loop through books by date current_date = datetime.date.fromordinal(1) @@ -1848,9 +1886,9 @@ class CatalogBuilder(object): pIndexTag['class'] = "date_index" aTag = Tag(soup, "a") aTag['name'] = "bdr_%s-%s-%s" % (current_date.year, current_date.month, current_date.day) - pIndexTag.insert(0,aTag) - pIndexTag.insert(1,NavigableString(date_string)) - divTag.insert(dtc,pIndexTag) + pIndexTag.insert(0, aTag) + pIndexTag.insert(1, NavigableString(date_string)) + divTag.insert(dtc, pIndexTag) dtc += 1 for new_entry in todays_list: @@ -1865,7 +1903,7 @@ class CatalogBuilder(object): aTag = Tag(soup, "a") if self.opts.generate_descriptions: aTag['href'] = "book_%d.html" % (int(float(new_entry['id']))) - aTag.insert(0,escape(new_entry['title'])) + aTag.insert(0, escape(new_entry['title'])) pBookTag.insert(ptc, aTag) ptc += 1 @@ -1879,7 +1917,7 @@ class CatalogBuilder(object): if self.opts.generate_authors: aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor", self.generate_author_anchor(new_entry['author'])) aTag.insert(0, NavigableString(new_entry['author'])) - emTag.insert(0,aTag) + emTag.insert(0, aTag) pBookTag.insert(ptc, emTag) ptc += 1 @@ -1892,10 +1930,10 @@ class CatalogBuilder(object): pIndexTag = Tag(soup, "p") pIndexTag['class'] = "date_index" aTag = Tag(soup, "a") - aTag['name'] = "bdr_%s" % date_range.replace(' ','') - pIndexTag.insert(0,aTag) - pIndexTag.insert(1,NavigableString(date_range)) - divTag.insert(dtc,pIndexTag) + aTag['name'] = "bdr_%s" % date_range.replace(' ', '') + pIndexTag.insert(0, aTag) + pIndexTag.insert(1, NavigableString(date_range)) + divTag.insert(dtc, pIndexTag) dtc += 1 for new_entry in date_range_list: @@ -1905,16 +1943,16 @@ class CatalogBuilder(object): ptc = 0 # Percent read - dots = int((new_entry['percent_read'] + 5)/10) + dots = int((new_entry['percent_read'] + 5) / 10) dot_string = self.SYMBOL_PROGRESS_READ * dots empty_dots = self.SYMBOL_PROGRESS_UNREAD * (10 - dots) - pBookTag.insert(ptc, NavigableString('%s%s' % (dot_string,empty_dots))) + pBookTag.insert(ptc, NavigableString('%s%s' % (dot_string, empty_dots))) ptc += 1 aTag = Tag(soup, "a") if self.opts.generate_descriptions: aTag['href'] = "book_%d.html" % (int(float(new_entry['id']))) - aTag.insert(0,escape(new_entry['title'])) + aTag.insert(0, escape(new_entry['title'])) pBookTag.insert(ptc, aTag) ptc += 1 @@ -1928,7 +1966,7 @@ class CatalogBuilder(object): if self.opts.generate_authors: aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor", self.generate_author_anchor(new_entry['author'])) aTag.insert(0, NavigableString(new_entry['author'])) - emTag.insert(0,aTag) + emTag.insert(0, aTag) pBookTag.insert(ptc, emTag) ptc += 1 @@ -1948,7 +1986,7 @@ class CatalogBuilder(object): btc = 0 # Insert section tag - aTag = Tag(soup,'a') + aTag = Tag(soup, 'a') aTag['name'] = 'section_start' body.insert(btc, aTag) btc += 1 @@ -1956,7 +1994,7 @@ class CatalogBuilder(object): # Insert the anchor aTag = Tag(soup, "a") anchor_name = friendly_name.lower() - aTag['name'] = anchor_name.replace(" ","") + aTag['name'] = anchor_name.replace(" ", "") body.insert(btc, aTag) btc += 1 @@ -1970,13 +2008,13 @@ class CatalogBuilder(object): #print "bm_book: %s" % bm_book book[1]['bookmark_timestamp'] = book[0].timestamp try: - book[1]['percent_read'] = min(float(100*book[0].last_read / book[0].book_length),100) + book[1]['percent_read'] = min(float(100 * book[0].last_read / book[0].book_length), 100) except: book[1]['percent_read'] = 0 bookmarked_books.append(book[1]) self.bookmarked_books_by_date_read = sorted(bookmarked_books, - key=lambda x:(x['bookmark_timestamp'], x['bookmark_timestamp']),reverse=True) + key=lambda x: (x['bookmark_timestamp'], x['bookmark_timestamp']), reverse=True) # >>>> Recently read by day <<<< current_date = datetime.date.fromordinal(1) @@ -2070,7 +2108,6 @@ class CatalogBuilder(object): len(genre[key]), 'titles' if len(genre[key]) > 1 else 'title')) - # Write the results # genre_list = [ {friendly_tag:[{book},{book}]}, {friendly_tag:[{book},{book}]}, ...] master_genre_list = [] @@ -2081,19 +2118,19 @@ class CatalogBuilder(object): # Create sorted_authors[0] = friendly, [1] = author_sort for NCX creation authors = [] for book in genre_tag_set[genre]: - authors.append((book['author'],book['author_sort'])) + authors.append((book['author'], book['author_sort'])) # authors[] contains a list of all book authors, with multiple entries for multiple books by author # Create unique_authors with a count of books per author as the third tuple element books_by_current_author = 1 current_author = authors[0] unique_authors = [] - for (i,author) in enumerate(authors): + for (i, author) in enumerate(authors): if author != current_author and i: unique_authors.append((current_author[0], current_author[1], books_by_current_author)) current_author = author books_by_current_author = 1 - elif i==0 and len(authors) == 1: + elif i == 0 and len(authors) == 1: # Allow for single-book lists unique_authors.append((current_author[0], current_author[1], books_by_current_author)) else: @@ -2102,16 +2139,17 @@ class CatalogBuilder(object): # Write the genre book list as an article outfile = "%s/Genre_%s.html" % (self.content_dir, genre) titles_spanned = self.generate_html_by_genre(genre, - True if index==0 else False, + True if index == 0 else False, genre_tag_set[genre], outfile) tag_file = "content/Genre_%s.html" % genre - master_genre_list.append({'tag':genre, - 'file':tag_file, - 'authors':unique_authors, - 'books':genre_tag_set[genre], - 'titles_spanned':titles_spanned}) + master_genre_list.append({ + 'tag': genre, + 'file': tag_file, + 'authors': unique_authors, + 'books': genre_tag_set[genre], + 'titles_spanned': titles_spanned}) self.genres = master_genre_list @@ -2142,7 +2180,7 @@ class CatalogBuilder(object): # Insert section tag if this is the section start - first article only if section_head: - aTag = Tag(soup,'a') + aTag = Tag(soup, 'a') aTag['id'] = 'section_start' divTag.insert(dtc, aTag) dtc += 1 @@ -2153,14 +2191,14 @@ class CatalogBuilder(object): aTag = Tag(soup, 'a') aTag['id'] = "Genre_%s" % genre divTag.insert(dtc, aTag) - body.insert(btc,divTag) + body.insert(btc, divTag) btc += 1 - titleTag = body.find(attrs={'class':'title'}) - titleTag.insert(0,NavigableString('%s' % escape(self.get_friendly_genre_tag(genre)))) + titleTag = body.find(attrs={'class': 'title'}) + titleTag.insert(0, NavigableString('%s' % escape(self.get_friendly_genre_tag(genre)))) # Insert the books by author list - divTag = body.find(attrs={'class':'authors'}) + divTag = body.find(attrs={'class': 'authors'}) dtc = 0 current_author = '' @@ -2177,26 +2215,26 @@ class CatalogBuilder(object): if self.opts.generate_authors: aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor", self.generate_author_anchor(book['author'])) aTag.insert(0, book['author']) - pAuthorTag.insert(0,aTag) - divTag.insert(dtc,pAuthorTag) + pAuthorTag.insert(0, aTag) + divTag.insert(dtc, pAuthorTag) dtc += 1 # Check for series if book['series'] and book['series'] != current_series: # Start a new series current_series = book['series'] - pSeriesTag = Tag(soup,'p') + pSeriesTag = Tag(soup, 'p') pSeriesTag['class'] = "series" if self.opts.fmt == 'mobi': pSeriesTag['class'] = "series_mobi" if self.opts.generate_series: - aTag = Tag(soup,'a') + aTag = Tag(soup, 'a') aTag['href'] = "%s.html#%s" % ('BySeries', self.generate_series_anchor(book['series'])) aTag.insert(0, book['series']) pSeriesTag.insert(0, aTag) else: - pSeriesTag.insert(0,NavigableString('%s' % book['series'])) - divTag.insert(dtc,pSeriesTag) + pSeriesTag.insert(0, NavigableString('%s' % book['series'])) + divTag.insert(dtc, pSeriesTag) dtc += 1 if current_series and not book['series']: @@ -2228,7 +2266,7 @@ class CatalogBuilder(object): #aTag.insert(0,escape(book['title'])) formatted_title = self.by_genres_normal_title_template.format(**args).rstrip() non_series_books += 1 - aTag.insert(0,NavigableString(escape(formatted_title))) + aTag.insert(0, NavigableString(escape(formatted_title))) spanTag.insert(stc, aTag) stc += 1 @@ -2245,9 +2283,9 @@ class CatalogBuilder(object): outfile.close() if len(books) > 1: - titles_spanned = [(books[0]['author'],books[0]['title']), (books[-1]['author'],books[-1]['title'])] + titles_spanned = [(books[0]['author'], books[0]['title']), (books[-1]['author'], books[-1]['title'])] else: - titles_spanned = [(books[0]['author'],books[0]['title'])] + titles_spanned = [(books[0]['author'], books[0]['title'])] return titles_spanned @@ -2297,7 +2335,7 @@ class CatalogBuilder(object): series_count = 0 for idx, book in enumerate(self.books_by_series): # Check for initial letter change - if self.letter_or_symbol(sort_equivalents[idx]) != current_letter : + if self.letter_or_symbol(sort_equivalents[idx]) != current_letter: # Start a new letter with Index letter current_letter = self.letter_or_symbol(sort_equivalents[idx]) pIndexTag = Tag(soup, "p") @@ -2305,28 +2343,28 @@ class CatalogBuilder(object): aTag = Tag(soup, "a") if current_letter == self.SYMBOLS: aTag['id'] = self.SYMBOLS + "_series" - pIndexTag.insert(0,aTag) - pIndexTag.insert(1,NavigableString(self.SYMBOLS)) + pIndexTag.insert(0, aTag) + pIndexTag.insert(1, NavigableString(self.SYMBOLS)) else: aTag['id'] = self.generate_unicode_name(current_letter) + "_series" - pIndexTag.insert(0,aTag) - pIndexTag.insert(1,NavigableString(sort_equivalents[idx])) - divTag.insert(dtc,pIndexTag) + pIndexTag.insert(0, aTag) + pIndexTag.insert(1, NavigableString(sort_equivalents[idx])) + divTag.insert(dtc, pIndexTag) dtc += 1 # Check for series change if book['series'] != current_series: # Start a new series series_count += 1 current_series = book['series'] - pSeriesTag = Tag(soup,'p') + pSeriesTag = Tag(soup, 'p') pSeriesTag['class'] = "series" if self.opts.fmt == 'mobi': pSeriesTag['class'] = "series_mobi" aTag = Tag(soup, 'a') aTag['id'] = self.generate_series_anchor(book['series']) - pSeriesTag.insert(0,aTag) - pSeriesTag.insert(1,NavigableString('%s' % book['series'])) - divTag.insert(dtc,pSeriesTag) + pSeriesTag.insert(0, aTag) + pSeriesTag.insert(1, NavigableString('%s' % book['series'])) + divTag.insert(dtc, pSeriesTag) dtc += 1 # Add books @@ -2350,7 +2388,7 @@ class CatalogBuilder(object): args = self.generate_format_args(book) formatted_title = self.by_series_title_template.format(**args).rstrip() - aTag.insert(0,NavigableString(escape(formatted_title))) + aTag.insert(0, NavigableString(escape(formatted_title))) spanTag.insert(stc, aTag) stc += 1 @@ -2377,7 +2415,7 @@ class CatalogBuilder(object): pTag = Tag(soup, "p") pTag['class'] = 'title' ptc = 0 - aTag = Tag(soup,'a') + aTag = Tag(soup, 'a') aTag['id'] = 'section_start' pTag.insert(ptc, aTag) ptc += 1 @@ -2386,10 +2424,10 @@ class CatalogBuilder(object): # Insert the

tag with book_count at the head aTag = Tag(soup, "a") anchor_name = friendly_name.lower() - aTag['id'] = anchor_name.replace(" ","") - pTag.insert(0,aTag) - pTag.insert(1,NavigableString('%s' % friendly_name)) - body.insert(btc,pTag) + aTag['id'] = anchor_name.replace(" ", "") + pTag.insert(0, aTag) + pTag.insert(1, NavigableString('%s' % friendly_name)) + body.insert(btc, pTag) btc += 1 # Add the divTag to the body @@ -2423,7 +2461,7 @@ class CatalogBuilder(object): pTag = Tag(soup, "p") pTag['class'] = 'title' ptc = 0 - aTag = Tag(soup,'a') + aTag = Tag(soup, 'a') aTag['id'] = 'section_start' pTag.insert(ptc, aTag) ptc += 1 @@ -2432,11 +2470,11 @@ class CatalogBuilder(object): # Kindle don't need this because it shows section titles in Periodical format aTag = Tag(soup, "a") aTag['id'] = "bytitle" - pTag.insert(ptc,aTag) + pTag.insert(ptc, aTag) ptc += 1 - pTag.insert(ptc,NavigableString(_('Titles'))) + pTag.insert(ptc, NavigableString(_('Titles'))) - body.insert(btc,pTag) + body.insert(btc, pTag) btc += 1 divTag = Tag(soup, "div") @@ -2478,13 +2516,13 @@ class CatalogBuilder(object): current_letter = self.letter_or_symbol(sort_equivalents[idx]) if current_letter == self.SYMBOLS: aTag['id'] = self.SYMBOLS + "_titles" - pIndexTag.insert(0,aTag) - pIndexTag.insert(1,NavigableString(self.SYMBOLS)) + pIndexTag.insert(0, aTag) + pIndexTag.insert(1, NavigableString(self.SYMBOLS)) else: aTag['id'] = self.generate_unicode_name(current_letter) + "_titles" - pIndexTag.insert(0,aTag) - pIndexTag.insert(1,NavigableString(sort_equivalents[idx])) - divRunningTag.insert(dtc,pIndexTag) + pIndexTag.insert(0, aTag) + pIndexTag.insert(1, NavigableString(sort_equivalents[idx])) + divRunningTag.insert(dtc, pIndexTag) drtc += 1 # Add books @@ -2510,7 +2548,7 @@ class CatalogBuilder(object): formatted_title = self.by_titles_series_title_template.format(**args).rstrip() else: formatted_title = self.by_titles_normal_title_template.format(**args).rstrip() - aTag.insert(0,NavigableString(escape(formatted_title))) + aTag.insert(0, NavigableString(escape(formatted_title))) spanTag.insert(stc, aTag) stc += 1 @@ -2524,7 +2562,7 @@ class CatalogBuilder(object): if self.opts.generate_authors: aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor", self.generate_author_anchor(book['author'])) aTag.insert(0, NavigableString(book['author'])) - emTag.insert(0,aTag) + emTag.insert(0, aTag) spanTag.insert(stc, emTag) stc += 1 @@ -2611,7 +2649,7 @@ class CatalogBuilder(object): author = book['author'] if book['prefix']: - author_prefix = book['prefix'] + ' ' + _("by ") + author_prefix = book['prefix'] + ' ' + _("by ") elif self.opts.connected_kindle and book['id'] in self.bookmarked_books: author_prefix = self.SYMBOL_READING + ' ' + _("by ") else: @@ -2621,16 +2659,16 @@ class CatalogBuilder(object): genres = '' if 'genres' in book: _soup = BeautifulSoup('') - genresTag = Tag(_soup,'p') + genresTag = Tag(_soup, 'p') gtc = 0 for (i, tag) in enumerate(sorted(book.get('genres', []))): - aTag = Tag(_soup,'a') + aTag = Tag(_soup, 'a') if self.opts.generate_genres: aTag['href'] = "Genre_%s.html" % self.genre_tags_dict[tag] - aTag.insert(0,escape(NavigableString(tag))) + aTag.insert(0, escape(NavigableString(tag))) genresTag.insert(gtc, aTag) gtc += 1 - if i < len(book['genres'])-1: + if i < len(book['genres']) - 1: genresTag.insert(gtc, NavigableString(' · ')) gtc += 1 genres = genresTag.renderContents() @@ -2650,12 +2688,12 @@ class CatalogBuilder(object): pubdate = pubyear = pubmonth = '' # Thumb - _soup = BeautifulSoup('',selfClosingTags=['img']) - thumb = Tag(_soup,"img") + _soup = BeautifulSoup('', selfClosingTags=['img']) + thumb = Tag(_soup, "img") if 'cover' in book and book['cover']: - thumb['src'] = "../images/thumbnail_%d.jpg" % int(book['id']) + thumb['src'] = "../images/thumbnail_%d.jpg" % int(book['id']) else: - thumb['src'] = "../images/thumbnail_default.jpg" + thumb['src'] = "../images/thumbnail_default.jpg" thumb['alt'] = "cover thumbnail" # Publisher @@ -2669,7 +2707,7 @@ class CatalogBuilder(object): if stars: star_string = self.SYMBOL_FULL_RATING * stars empty_stars = self.SYMBOL_EMPTY_RATING * (5 - stars) - rating = '%s%s
' % (star_string,empty_stars) + rating = '%s%s
' % (star_string, empty_stars) # Notes note_source = '' @@ -2698,44 +2736,44 @@ class CatalogBuilder(object): btc += 1 # Insert the link to the series or remove - aTag = body.find('a', attrs={'class':'series_id'}) + aTag = body.find('a', attrs={'class': 'series_id'}) if aTag: if book['series']: if self.opts.generate_series: - aTag['href'] = "%s.html#%s" % ('BySeries',self.generate_series_anchor(book['series'])) + aTag['href'] = "%s.html#%s" % ('BySeries', self.generate_series_anchor(book['series'])) else: aTag.extract() # Insert the author link - aTag = body.find('a', attrs={'class':'author'}) + aTag = body.find('a', attrs={'class': 'author'}) if self.opts.generate_authors and aTag: aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor", self.generate_author_anchor(book['author'])) if publisher == ' ': - publisherTag = body.find('td', attrs={'class':'publisher'}) + publisherTag = body.find('td', attrs={'class': 'publisher'}) if publisherTag: publisherTag.contents[0].replaceWith(' ') if not genres: - genresTag = body.find('p',attrs={'class':'genres'}) + genresTag = body.find('p', attrs={'class': 'genres'}) if genresTag: genresTag.extract() if not formats: - formatsTag = body.find('p',attrs={'class':'formats'}) + formatsTag = body.find('p', attrs={'class': 'formats'}) if formatsTag: formatsTag.extract() if note_content == '': - tdTag = body.find('td', attrs={'class':'notes'}) + tdTag = body.find('td', attrs={'class': 'notes'}) if tdTag: tdTag.contents[0].replaceWith(' ') - emptyTags = body.findAll('td', attrs={'class':'empty'}) + emptyTags = body.findAll('td', attrs={'class': 'empty'}) for mt in emptyTags: - newEmptyTag = Tag(BeautifulSoup(),'td') - newEmptyTag.insert(0,NavigableString(' ')) + newEmptyTag = Tag(BeautifulSoup(), 'td') + newEmptyTag.insert(0, NavigableString(' ')) mt.replaceWith(newEmptyTag) return soup @@ -2758,7 +2796,7 @@ class CatalogBuilder(object): self.update_progress_micro_step("%s %d of %d" % (_("Description HTML"), title_num, len(self.books_by_title)), - float(title_num*100/len(self.books_by_title))/100) + float(title_num * 100 / len(self.books_by_title)) / 100) # Generate the header from user-customizable template soup = self.generate_html_description_header(title) @@ -2795,7 +2833,7 @@ class CatalogBuilder(object): # Insert the supplied title soup = BeautifulSoup(header) titleTag = soup.find('title') - titleTag.insert(0,NavigableString(title)) + titleTag.insert(0, NavigableString(title)) return soup def generate_html_genre_header(self, title): @@ -2814,10 +2852,10 @@ class CatalogBuilder(object): bodyTag = soup.find('body') pTag = Tag(soup, 'p') pTag['class'] = 'title' - bodyTag.insert(0,pTag) + bodyTag.insert(0, pTag) divTag = Tag(soup, 'div') divTag['class'] = 'authors' - bodyTag.insert(1,divTag) + bodyTag.insert(1, divTag) return soup def generate_masthead_image(self, out_path): @@ -2869,9 +2907,9 @@ class CatalogBuilder(object): font = ImageFont.truetype(default_font, 48) text = self.opts.catalog_title.encode('utf-8') width, height = draw.textsize(text, font=font) - left = max(int((MI_WIDTH - width)/2.), 0) - top = max(int((MI_HEIGHT - height)/2.), 0) - draw.text((left, top), text, fill=(0,0,0), font=font) + left = max(int((MI_WIDTH - width) / 2.), 0) + top = max(int((MI_HEIGHT - height) / 2.), 0) + draw.text((left, top), text, fill=(0, 0, 0), font=font) img.save(open(out_path, 'wb'), 'GIF') def generate_ncx_header(self): @@ -2896,7 +2934,7 @@ class CatalogBuilder(object): ''' - soup = BeautifulStoneSoup(header, selfClosingTags=['content','calibre:meta-img']) + soup = BeautifulStoneSoup(header, selfClosingTags=['content', 'calibre:meta-img']) ncx = soup.find('ncx') navMapTag = Tag(soup, 'navMap') @@ -2937,20 +2975,18 @@ class CatalogBuilder(object): navPointTag.insert(1, contentTag) else: # Descriptions only - sort_descriptions_by = self.books_by_author if self.opts.sort_descriptions_by_author \ - else self.books_by_title contentTag = Tag(soup, 'content') - contentTag['src'] = "content/book_%d.html" % int(sort_descriptions_by[0]['id']) + contentTag['src'] = "content/book_%d.html" % int(self.books_by_description[0]['id']) navPointTag.insert(1, contentTag) if self.generate_for_kindle_mobi: cmiTag = Tag(soup, '%s' % 'calibre:meta-img') cmiTag['id'] = "mastheadImage" cmiTag['src'] = "images/mastheadImage.gif" - navPointTag.insert(2,cmiTag) - navMapTag.insert(0,navPointTag) + navPointTag.insert(2, cmiTag) + navMapTag.insert(0, navPointTag) - ncx.insert(0,navMapTag) + ncx.insert(0, navMapTag) self.ncx_soup = soup def generate_ncx_descriptions(self, tocTitle): @@ -2970,9 +3006,6 @@ class CatalogBuilder(object): self.update_progress_full_step(_("NCX for Descriptions")) - sort_descriptions_by = self.books_by_author if self.opts.sort_descriptions_by_author \ - else self.books_by_title - # --- Construct the 'Descriptions' section --- ncx_soup = self.ncx_soup if self.generate_for_kindle_mobi: @@ -2990,19 +3023,22 @@ class CatalogBuilder(object): self.play_order += 1 navLabelTag = Tag(ncx_soup, 'navLabel') textTag = Tag(ncx_soup, 'text') - textTag.insert(0, NavigableString(tocTitle)) + section_header = '%s [%d]' % (tocTitle, len(self.books_by_description)) + if self.generate_for_kindle_mobi: + section_header = tocTitle + textTag.insert(0, NavigableString(section_header)) navLabelTag.insert(0, textTag) nptc = 0 navPointTag.insert(nptc, navLabelTag) nptc += 1 - contentTag = Tag(ncx_soup,"content") - contentTag['src'] = "content/book_%d.html" % int(sort_descriptions_by[0]['id']) + contentTag = Tag(ncx_soup, "content") + contentTag['src'] = "content/book_%d.html" % int(self.books_by_description[0]['id']) navPointTag.insert(nptc, contentTag) nptc += 1 # Loop over the titles - for book in sort_descriptions_by: + for book in self.books_by_description: navPointVolumeTag = Tag(ncx_soup, 'navPoint') if self.generate_for_kindle_mobi: navPointVolumeTag['class'] = "article" @@ -3040,8 +3076,8 @@ class CatalogBuilder(object): # Include Author for non-Kindle textTag.insert(0, NavigableString(self.format_ncx_text('%s · %s' % \ (book['title'], book['author']), dest='title'))) - navLabelTag.insert(0,textTag) - navPointVolumeTag.insert(0,navLabelTag) + navLabelTag.insert(0, textTag) + navPointVolumeTag.insert(0, navLabelTag) contentTag = Tag(ncx_soup, "content") contentTag['src'] = "content/book_%d.html#book%d" % (int(book['id']), int(book['id'])) @@ -3119,12 +3155,15 @@ class CatalogBuilder(object): self.play_order += 1 navLabelTag = Tag(ncx_soup, 'navLabel') textTag = Tag(ncx_soup, 'text') - textTag.insert(0, NavigableString(tocTitle)) + section_header = '%s [%d]' % (tocTitle, len(self.all_series)) + if self.generate_for_kindle_mobi: + section_header = tocTitle + textTag.insert(0, NavigableString(section_header)) navLabelTag.insert(0, textTag) nptc = 0 navPointTag.insert(nptc, navLabelTag) nptc += 1 - contentTag = Tag(ncx_soup,"content") + contentTag = Tag(ncx_soup, "content") contentTag['src'] = "content/%s.html#section_start" % (output) navPointTag.insert(nptc, contentTag) nptc += 1 @@ -3159,7 +3198,7 @@ class CatalogBuilder(object): current_series_list = [book['series']] else: if len(current_series_list) < self.opts.description_clip and \ - book['series'] != current_series : + book['series'] != current_series: current_series = book['series'] current_series_list.append(book['series']) @@ -3167,7 +3206,7 @@ class CatalogBuilder(object): _add_to_series_by_letter(current_series_list) # Add *article* entries for each populated series title letter - for (i,books) in enumerate(series_by_letter): + for (i, books) in enumerate(series_by_letter): navPointByLetterTag = Tag(ncx_soup, 'navPoint') if self.generate_for_kindle_mobi: navPointByLetterTag['class'] = "article" @@ -3176,14 +3215,14 @@ class CatalogBuilder(object): self.play_order += 1 navLabelTag = Tag(ncx_soup, 'navLabel') textTag = Tag(ncx_soup, 'text') - if len(title_letters[i])>1: + if len(title_letters[i]) > 1: fmt_string = _(u"Series beginning with %s") else: fmt_string = _(u"Series beginning with '%s'") textTag.insert(0, NavigableString(fmt_string % - (title_letters[i] if len(title_letters[i])>1 else title_letters[i]))) + (title_letters[i] if len(title_letters[i]) > 1 else title_letters[i]))) navLabelTag.insert(0, textTag) - navPointByLetterTag.insert(0,navLabelTag) + navPointByLetterTag.insert(0, navLabelTag) contentTag = Tag(ncx_soup, 'content') #contentTag['src'] = "content/%s.html#%s_series" % (output, title_letters[i]) if title_letters[i] == self.SYMBOLS: @@ -3191,7 +3230,7 @@ class CatalogBuilder(object): else: contentTag['src'] = "content/%s.html#%s_series" % (output, self.generate_unicode_name(title_letters[i])) - navPointByLetterTag.insert(1,contentTag) + navPointByLetterTag.insert(1, contentTag) if self.generate_for_kindle_mobi: cmTag = Tag(ncx_soup, '%s' % 'calibre:meta') @@ -3247,12 +3286,15 @@ class CatalogBuilder(object): self.play_order += 1 navLabelTag = Tag(ncx_soup, 'navLabel') textTag = Tag(ncx_soup, 'text') - textTag.insert(0, NavigableString(tocTitle)) + section_header = '%s [%d]' % (tocTitle, len(self.books_by_title)) + if self.generate_for_kindle_mobi: + section_header = tocTitle + textTag.insert(0, NavigableString(section_header)) navLabelTag.insert(0, textTag) nptc = 0 navPointTag.insert(nptc, navLabelTag) nptc += 1 - contentTag = Tag(ncx_soup,"content") + contentTag = Tag(ncx_soup, "content") contentTag['src'] = "content/%s.html#section_start" % (output) navPointTag.insert(nptc, contentTag) nptc += 1 @@ -3289,7 +3331,7 @@ class CatalogBuilder(object): current_book_list = [book['title']] else: if len(current_book_list) < self.opts.description_clip and \ - book['title'] != current_book : + book['title'] != current_book: current_book = book['title'] current_book_list.append(book['title']) @@ -3297,7 +3339,7 @@ class CatalogBuilder(object): _add_to_books_by_letter(current_book_list) # Add *article* entries for each populated title letter - for (i,books) in enumerate(books_by_letter): + for (i, books) in enumerate(books_by_letter): navPointByLetterTag = Tag(ncx_soup, 'navPoint') if self.generate_for_kindle_mobi: navPointByLetterTag['class'] = "article" @@ -3306,20 +3348,20 @@ class CatalogBuilder(object): self.play_order += 1 navLabelTag = Tag(ncx_soup, 'navLabel') textTag = Tag(ncx_soup, 'text') - if len(title_letters[i])>1: + if len(title_letters[i]) > 1: fmt_string = _(u"Titles beginning with %s") else: fmt_string = _(u"Titles beginning with '%s'") textTag.insert(0, NavigableString(fmt_string % - (title_letters[i] if len(title_letters[i])>1 else title_letters[i]))) + (title_letters[i] if len(title_letters[i]) > 1 else title_letters[i]))) navLabelTag.insert(0, textTag) - navPointByLetterTag.insert(0,navLabelTag) + navPointByLetterTag.insert(0, navLabelTag) contentTag = Tag(ncx_soup, 'content') if title_letters[i] == self.SYMBOLS: contentTag['src'] = "content/%s.html#%s_titles" % (output, self.SYMBOLS) else: contentTag['src'] = "content/%s.html#%s_titles" % (output, self.generate_unicode_name(title_letters[i])) - navPointByLetterTag.insert(1,contentTag) + navPointByLetterTag.insert(1, contentTag) if self.generate_for_kindle_mobi: cmTag = Tag(ncx_soup, '%s' % 'calibre:meta') @@ -3371,18 +3413,21 @@ class CatalogBuilder(object): if self.generate_for_kindle_mobi: navPointTag['class'] = "section" file_ID = "%s" % tocTitle.lower() - file_ID = file_ID.replace(" ","") + file_ID = file_ID.replace(" ", "") navPointTag['id'] = "%s-ID" % file_ID navPointTag['playOrder'] = self.play_order self.play_order += 1 navLabelTag = Tag(ncx_soup, 'navLabel') textTag = Tag(ncx_soup, 'text') - textTag.insert(0, NavigableString('%s' % tocTitle)) + section_header = '%s [%d]' % (tocTitle, len(self.individual_authors)) + if self.generate_for_kindle_mobi: + section_header = tocTitle + textTag.insert(0, NavigableString(section_header)) navLabelTag.insert(0, textTag) nptc = 0 navPointTag.insert(nptc, navLabelTag) nptc += 1 - contentTag = Tag(ncx_soup,"content") + contentTag = Tag(ncx_soup, "content") contentTag['src'] = "%s#section_start" % HTML_file navPointTag.insert(nptc, contentTag) nptc += 1 @@ -3426,19 +3471,19 @@ class CatalogBuilder(object): self.play_order += 1 navLabelTag = Tag(ncx_soup, 'navLabel') textTag = Tag(ncx_soup, 'text') - if len(authors_by_letter[1])>1: + if len(authors_by_letter[1]) > 1: fmt_string = _(u"Authors beginning with %s") else: fmt_string = _(u"Authors beginning with '%s'") - textTag.insert(0, NavigableString(fmt_string % (authors_by_letter[1]))) + textTag.insert(0, NavigableString(fmt_string % authors_by_letter[1])) navLabelTag.insert(0, textTag) - navPointByLetterTag.insert(0,navLabelTag) + navPointByLetterTag.insert(0, navLabelTag) contentTag = Tag(ncx_soup, 'content') if authors_by_letter[1] == self.SYMBOLS: contentTag['src'] = "%s#%s_authors" % (HTML_file, authors_by_letter[1]) else: contentTag['src'] = "%s#%s_authors" % (HTML_file, self.generate_unicode_name(authors_by_letter[1])) - navPointByLetterTag.insert(1,contentTag) + navPointByLetterTag.insert(1, contentTag) if self.generate_for_kindle_mobi: cmTag = Tag(ncx_soup, '%s' % 'calibre:meta') @@ -3497,7 +3542,7 @@ class CatalogBuilder(object): if self.generate_for_kindle_mobi: navPointTag['class'] = "section" file_ID = "%s" % tocTitle.lower() - file_ID = file_ID.replace(" ","") + file_ID = file_ID.replace(" ", "") navPointTag['id'] = "%s-ID" % file_ID navPointTag['playOrder'] = self.play_order self.play_order += 1 @@ -3508,7 +3553,7 @@ class CatalogBuilder(object): nptc = 0 navPointTag.insert(nptc, navLabelTag) nptc += 1 - contentTag = Tag(ncx_soup,"content") + contentTag = Tag(ncx_soup, "content") contentTag['src'] = "%s#section_start" % HTML_file navPointTag.insert(nptc, contentTag) nptc += 1 @@ -3518,15 +3563,15 @@ class CatalogBuilder(object): master_date_range_list = [] today = datetime.datetime.now() today_time = datetime.datetime(today.year, today.month, today.day) - for (i,date) in enumerate(self.DATE_RANGE): + for (i, date) in enumerate(self.DATE_RANGE): if i: - date_range = '%d to %d days ago' % (self.DATE_RANGE[i-1], self.DATE_RANGE[i]) + date_range = '%d to %d days ago' % (self.DATE_RANGE[i - 1], self.DATE_RANGE[i]) else: date_range = 'Last %d days' % (self.DATE_RANGE[i]) date_range_limit = self.DATE_RANGE[i] for book in self.books_by_date_range: book_time = datetime.datetime(book['timestamp'].year, book['timestamp'].month, book['timestamp'].day) - if (today_time-book_time).days <= date_range_limit: + if (today_time - book_time).days <= date_range_limit: #print "generate_ncx_by_date_added: %s added %d days ago" % (book['title'], (today_time-book_time).days) current_titles_list.append(book['title']) else: @@ -3541,19 +3586,19 @@ class CatalogBuilder(object): navPointByDateRangeTag = Tag(ncx_soup, 'navPoint') if self.generate_for_kindle_mobi: navPointByDateRangeTag['class'] = "article" - navPointByDateRangeTag['id'] = "%s-ID" % books_by_date_range[1].replace(' ','') + navPointByDateRangeTag['id'] = "%s-ID" % books_by_date_range[1].replace(' ', '') navPointTag['playOrder'] = self.play_order self.play_order += 1 navLabelTag = Tag(ncx_soup, 'navLabel') textTag = Tag(ncx_soup, 'text') textTag.insert(0, NavigableString(books_by_date_range[1])) navLabelTag.insert(0, textTag) - navPointByDateRangeTag.insert(0,navLabelTag) + navPointByDateRangeTag.insert(0, navLabelTag) contentTag = Tag(ncx_soup, 'content') contentTag['src'] = "%s#bda_%s" % (HTML_file, - books_by_date_range[1].replace(' ','')) + books_by_date_range[1].replace(' ', '')) - navPointByDateRangeTag.insert(1,contentTag) + navPointByDateRangeTag.insert(1, contentTag) if self.generate_for_kindle_mobi: cmTag = Tag(ncx_soup, '%s' % 'calibre:meta') @@ -3601,19 +3646,19 @@ class CatalogBuilder(object): navPointByMonthTag = Tag(ncx_soup, 'navPoint') if self.generate_for_kindle_mobi: navPointByMonthTag['class'] = "article" - navPointByMonthTag['id'] = "bda_%s-%s-ID" % (books_by_month[1].year,books_by_month[1].month ) + navPointByMonthTag['id'] = "bda_%s-%s-ID" % (books_by_month[1].year, books_by_month[1].month) navPointTag['playOrder'] = self.play_order self.play_order += 1 navLabelTag = Tag(ncx_soup, 'navLabel') textTag = Tag(ncx_soup, 'text') textTag.insert(0, NavigableString(datestr)) navLabelTag.insert(0, textTag) - navPointByMonthTag.insert(0,navLabelTag) + navPointByMonthTag.insert(0, navLabelTag) contentTag = Tag(ncx_soup, 'content') contentTag['src'] = "%s#bda_%s-%s" % (HTML_file, - books_by_month[1].year,books_by_month[1].month) + books_by_month[1].year, books_by_month[1].month) - navPointByMonthTag.insert(1,contentTag) + navPointByMonthTag.insert(1, contentTag) if self.generate_for_kindle_mobi: cmTag = Tag(ncx_soup, '%s' % 'calibre:meta') @@ -3681,7 +3726,7 @@ class CatalogBuilder(object): if self.generate_for_kindle_mobi: navPointTag['class'] = "section" file_ID = "%s" % tocTitle.lower() - file_ID = file_ID.replace(" ","") + file_ID = file_ID.replace(" ", "") navPointTag['id'] = "%s-ID" % file_ID navPointTag['playOrder'] = self.play_order self.play_order += 1 @@ -3692,7 +3737,7 @@ class CatalogBuilder(object): nptc = 0 navPointTag.insert(nptc, navLabelTag) nptc += 1 - contentTag = Tag(ncx_soup,"content") + contentTag = Tag(ncx_soup, "content") contentTag['src'] = "%s#section_start" % HTML_file navPointTag.insert(nptc, contentTag) nptc += 1 @@ -3702,15 +3747,15 @@ class CatalogBuilder(object): master_date_range_list = [] today = datetime.datetime.now() today_time = datetime.datetime(today.year, today.month, today.day) - for (i,date) in enumerate(self.DATE_RANGE): + for (i, date) in enumerate(self.DATE_RANGE): if i: - date_range = '%d to %d days ago' % (self.DATE_RANGE[i-1], self.DATE_RANGE[i]) + date_range = '%d to %d days ago' % (self.DATE_RANGE[i - 1], self.DATE_RANGE[i]) else: date_range = 'Last %d days' % (self.DATE_RANGE[i]) date_range_limit = self.DATE_RANGE[i] for book in self.bookmarked_books_by_date_read: bookmark_time = datetime.datetime.utcfromtimestamp(book['bookmark_timestamp']) - if (today_time-bookmark_time).days <= date_range_limit: + if (today_time - bookmark_time).days <= date_range_limit: #print "generate_ncx_by_date_added: %s added %d days ago" % (book['title'], (today_time-book_time).days) current_titles_list.append(book['title']) else: @@ -3753,21 +3798,21 @@ class CatalogBuilder(object): navPointByDayTag['class'] = "article" navPointByDayTag['id'] = "bdr_%s-%s-%sID" % (books_by_day[1].year, books_by_day[1].month, - books_by_day[1].day ) + books_by_day[1].day) navPointTag['playOrder'] = self.play_order self.play_order += 1 navLabelTag = Tag(ncx_soup, 'navLabel') textTag = Tag(ncx_soup, 'text') textTag.insert(0, NavigableString(datestr)) navLabelTag.insert(0, textTag) - navPointByDayTag.insert(0,navLabelTag) + navPointByDayTag.insert(0, navLabelTag) contentTag = Tag(ncx_soup, 'content') contentTag['src'] = "%s#bdr_%s-%s-%s" % (HTML_file, books_by_day[1].year, books_by_day[1].month, books_by_day[1].day) - navPointByDayTag.insert(1,contentTag) + navPointByDayTag.insert(1, contentTag) if self.generate_for_kindle_mobi: cmTag = Tag(ncx_soup, '%s' % 'calibre:meta') @@ -3808,7 +3853,7 @@ class CatalogBuilder(object): self.update_progress_full_step(_("NCX for Genres")) if not len(self.genres): - self.opts.log.warn(" No genres found in tags.\n" + self.opts.log.warn(" No genres found\n" " No Genre section added to Catalog") return @@ -3824,19 +3869,21 @@ class CatalogBuilder(object): if self.generate_for_kindle_mobi: navPointTag['class'] = "section" file_ID = "%s" % tocTitle.lower() - file_ID = file_ID.replace(" ","") + file_ID = file_ID.replace(" ", "") navPointTag['id'] = "%s-ID" % file_ID navPointTag['playOrder'] = self.play_order self.play_order += 1 navLabelTag = Tag(ncx_soup, 'navLabel') textTag = Tag(ncx_soup, 'text') - # textTag.insert(0, NavigableString('%s (%d)' % (section_title, len(genre_list)))) - textTag.insert(0, NavigableString('%s' % tocTitle)) + section_header = '%s [%d]' % (tocTitle, len(self.genres)) + if self.generate_for_kindle_mobi: + section_header = tocTitle + textTag.insert(0, NavigableString(section_header)) navLabelTag.insert(0, textTag) nptc = 0 navPointTag.insert(nptc, navLabelTag) nptc += 1 - contentTag = Tag(ncx_soup,"content") + contentTag = Tag(ncx_soup, "content") contentTag['src'] = "content/Genre_%s.html#section_start" % self.genres[0]['tag'] navPointTag.insert(nptc, contentTag) nptc += 1 @@ -3859,8 +3906,8 @@ class CatalogBuilder(object): normalized_tag = self.genre_tags_dict[friendly_tag] break textTag.insert(0, self.format_ncx_text(NavigableString(friendly_tag), dest='description')) - navLabelTag.insert(0,textTag) - navPointVolumeTag.insert(0,navLabelTag) + navLabelTag.insert(0, textTag) + navPointVolumeTag.insert(0, navLabelTag) contentTag = Tag(ncx_soup, "content") contentTag['src'] = "content/Genre_%s.html#Genre_%s" % (normalized_tag, normalized_tag) navPointVolumeTag.insert(1, contentTag) @@ -3871,9 +3918,9 @@ class CatalogBuilder(object): cmTag['name'] = "author" # First - Last author - if len(genre['titles_spanned']) > 1 : + if len(genre['titles_spanned']) > 1: author_range = "%s - %s" % (genre['titles_spanned'][0][0], genre['titles_spanned'][1][0]) - else : + else: author_range = "%s" % (genre['titles_spanned'][0][0]) cmTag.insert(0, NavigableString(author_range)) @@ -3895,7 +3942,7 @@ class CatalogBuilder(object): titles = [] for title in genre['books']: titles.append(title['title']) - titles = sorted(titles, key=lambda x:(self.generate_sort_title(x),self.generate_sort_title(x))) + titles = sorted(titles, key=lambda x: (self.generate_sort_title(x), self.generate_sort_title(x))) titles_list = self.generate_short_description(u" • ".join(titles), dest="description") cmTag.insert(0, NavigableString(self.format_ncx_text(titles_list, dest='description'))) @@ -3942,12 +3989,12 @@ class CatalogBuilder(object): ''' # Add the supplied metadata tags - soup = BeautifulStoneSoup(header, selfClosingTags=['item','itemref', 'meta', 'reference']) + soup = BeautifulStoneSoup(header, selfClosingTags=['item', 'itemref', 'meta', 'reference']) metadata = soup.find('metadata') mtc = 0 titleTag = Tag(soup, "dc:title") - titleTag.insert(0,escape(self.opts.catalog_title)) + titleTag.insert(0, escape(self.opts.catalog_title)) metadata.insert(mtc, titleTag) mtc += 1 @@ -3993,7 +4040,6 @@ class CatalogBuilder(object): mtc += 1 # Write the thumbnail images, descriptions to the manifest - sort_descriptions_by = [] if self.opts.generate_descriptions: for thumb in self.thumbs: itemTag = Tag(soup, "item") @@ -4004,9 +4050,6 @@ class CatalogBuilder(object): manifest.insert(mtc, itemTag) mtc += 1 - # HTML files - add descriptions to manifest and spine - sort_descriptions_by = self.books_by_author if self.opts.sort_descriptions_by_author \ - else self.books_by_title # Add html_files to manifest and spine for file in self.html_filelist_1: @@ -4060,20 +4103,21 @@ class CatalogBuilder(object): spine.insert(stc, itemrefTag) stc += 1 - for book in sort_descriptions_by: - # manifest - itemTag = Tag(soup, "item") - itemTag['href'] = "content/book_%d.html" % int(book['id']) - itemTag['id'] = "book%d" % int(book['id']) - itemTag['media-type'] = "application/xhtml+xml" - manifest.insert(mtc, itemTag) - mtc += 1 + if self.opts.generate_descriptions: + for book in self.books_by_description: + # manifest + itemTag = Tag(soup, "item") + itemTag['href'] = "content/book_%d.html" % int(book['id']) + itemTag['id'] = "book%d" % int(book['id']) + itemTag['media-type'] = "application/xhtml+xml" + manifest.insert(mtc, itemTag) + mtc += 1 - # spine - itemrefTag = Tag(soup, "itemref") - itemrefTag['idref'] = "book%d" % int(book['id']) - spine.insert(stc, itemrefTag) - stc += 1 + # spine + itemrefTag = Tag(soup, "itemref") + itemrefTag['idref'] = "book%d" % int(book['id']) + spine.insert(stc, itemrefTag) + stc += 1 # Guide if self.generate_for_kindle_mobi: @@ -4081,7 +4125,7 @@ class CatalogBuilder(object): referenceTag['type'] = 'masthead' referenceTag['title'] = 'mastheadimage-image' referenceTag['href'] = 'images/mastheadImage.gif' - guide.insert(0,referenceTag) + guide.insert(0, referenceTag) # Write the OPF file outfile = open("%s/%s.opf" % (self.catalog_path, self.opts.basename), 'w') @@ -4107,7 +4151,7 @@ class CatalogBuilder(object): if stars: star_string = self.SYMBOL_FULL_RATING * stars empty_stars = self.SYMBOL_EMPTY_RATING * (5 - stars) - rating = '%s%s' % (star_string,empty_stars) + rating = '%s%s' % (star_string, empty_stars) except: # Rating could be None pass @@ -4127,9 +4171,9 @@ class CatalogBuilder(object): # Generate a legal XHTML id/href string if self.letter_or_symbol(series) == self.SYMBOLS: - return "symbol_%s_series" % re.sub('\W','',series).lower() + return "symbol_%s_series" % re.sub('\W', '', series).lower() else: - return "%s_series" % re.sub('\W','',ascii_text(series)).lower() + return "%s_series" % re.sub('\W', '', ascii_text(series)).lower() def generate_short_description(self, description, dest=None): """ Generate a truncated version of the supplied string. @@ -4199,36 +4243,36 @@ class CatalogBuilder(object): title_words = title_sort(title).split() translated = [] - for (i,word) in enumerate(title_words): + for (i, word) in enumerate(title_words): # Leading numbers optionally translated to text equivalent # Capitalize leading sort word - if i==0: + if i == 0: # *** Keep this code in case we need to restore numbers_as_text *** if False: #if self.opts.numbers_as_text and re.match('[0-9]+',word[0]): translated.append(NumberToText(word).text.capitalize()) else: - if re.match('[0-9]+',word[0]): - word = word.replace(',','') + if re.match('[0-9]+', word[0]): + word = word.replace(',', '') suffix = re.search('[\D]', word) if suffix: - word = '%10.0f%s' % (float(word[:suffix.start()]),word[suffix.start():]) + word = '%10.0f%s' % (float(word[:suffix.start()]), word[suffix.start():]) else: word = '%10.0f' % (float(word)) # If leading char > 'A', insert symbol as leading forcing lower sort # '/' sorts below numbers, g if self.letter_or_symbol(word[0]) != word[0]: - if word[0] > 'A' or (ord('9') < ord(word[0]) < ord('A')) : + if word[0] > 'A' or (ord('9') < ord(word[0]) < ord('A')): translated.append('/') translated.append(capitalize(word)) else: - if re.search('[0-9]+',word[0]): - word = word.replace(',','') + if re.search('[0-9]+', word[0]): + word = word.replace(',', '') suffix = re.search('[\D]', word) if suffix: - word = '%10.0f%s' % (float(word[:suffix.start()]),word[suffix.start():]) + word = '%10.0f%s' % (float(word[:suffix.start()]), word[suffix.start():]) else: word = '%10.0f' % (float(word)) translated.append(word) @@ -4269,12 +4313,12 @@ class CatalogBuilder(object): if zf is not None: with zf: try: - zf.getinfo(title['uuid']+cover_crc) + zf.getinfo(title['uuid'] + cover_crc) except: pass else: # uuid found in cache with matching crc - thumb_data = zf.read(title['uuid']+cover_crc) + thumb_data = zf.read(title['uuid'] + cover_crc) with open(os.path.join(image_dir, thumb_file), 'wb') as f: f.write(thumb_data) return @@ -4286,13 +4330,14 @@ class CatalogBuilder(object): f.write(thumb_data) # Save thumb to archive - if zf is not None: # Ensure that the read succeeded + if zf is not None: + # Ensure that the read succeeded # If we failed to open the zip file for reading, # we dont know if it contained the thumb or not zf = _open_archive('a') if zf is not None: with zf: - zf.writestr(title['uuid']+cover_crc, thumb_data) + zf.writestr(title['uuid'] + cover_crc, thumb_data) def generate_thumbnails(self): """ Generate a thumbnail cover for each book. @@ -4311,11 +4356,11 @@ class CatalogBuilder(object): self.update_progress_full_step(_("Thumbnails")) thumbs = ['thumbnail_default.jpg'] image_dir = "%s/images" % self.catalog_path - for (i,title) in enumerate(self.books_by_title): + for (i, title) in enumerate(self.books_by_title): # Update status self.update_progress_micro_step("%s %d of %d" % (_("Thumbnail"), i, len(self.books_by_title)), - i/float(len(self.books_by_title))) + i / float(len(self.books_by_title))) thumb_file = 'thumbnail_%d.jpg' % int(title['id']) thumb_generated = True @@ -4337,7 +4382,7 @@ class CatalogBuilder(object): if not thumb_generated: self.opts.log.warn(" using default cover for '%s' (%d)" % (title['title'], title['id'])) # Confirm thumb exists, default is current - default_thumb_fp = os.path.join(image_dir,"thumbnail_default.jpg") + default_thumb_fp = os.path.join(image_dir, "thumbnail_default.jpg") cover = os.path.join(self.catalog_path, "DefaultCover.png") title['cover'] = cover @@ -4363,7 +4408,6 @@ class CatalogBuilder(object): # Clear the book's cover property title['cover'] = None - # Write thumb_width to the file, validating cache contents # Allows detection of aborted catalog builds with ZipFile(self.thumbs_path, mode='a') as zfw: @@ -4477,7 +4521,7 @@ class CatalogBuilder(object): raise return pr - def letter_or_symbol(self,char): + def letter_or_symbol(self, char): """ Test asciized char for A-z. Convert char to ascii, test for A-z. @@ -4551,7 +4595,7 @@ class CatalogBuilder(object): # confusion with decimal points. # Explode lost CRs to \n\n - for lost_cr in re.finditer('([a-z])([\.\?!])([A-Z])',comments): + for lost_cr in re.finditer('([a-z])([\.\?!])([A-Z])', comments): comments = comments.replace(lost_cr.group(), '%s%s\n\n%s' % (lost_cr.group(1), lost_cr.group(2), @@ -4573,17 +4617,17 @@ class CatalogBuilder(object): split_ps = comments.split(u'\n\n') tsc = 0 for p in split_ps: - pTag = Tag(soup,'p') - pTag.insert(0,p) - soup.insert(tsc,pTag) + pTag = Tag(soup, 'p') + pTag.insert(0, p) + soup.insert(tsc, pTag) tsc += 1 comments = soup.renderContents(None) # Convert solo returns to
- comments = re.sub('[\r\n]','
', comments) + comments = re.sub('[\r\n]', '
', comments) # Convert two hypens to emdash - comments = re.sub('--','—',comments) + comments = re.sub('--', '—', comments) soup = BeautifulSoup(comments) result = BeautifulSoup() rtc = 0 @@ -4593,15 +4637,15 @@ class CatalogBuilder(object): for token in all_tokens: if type(token) is NavigableString: if not open_pTag: - pTag = Tag(result,'p') + pTag = Tag(result, 'p') open_pTag = True ptc = 0 - pTag.insert(ptc,prepare_string_for_xml(token)) + pTag.insert(ptc, prepare_string_for_xml(token)) ptc += 1 - elif token.name in ['br','b','i','em']: + elif token.name in ['br', 'b', 'i', 'em']: if not open_pTag: - pTag = Tag(result,'p') + pTag = Tag(result, 'p') open_pTag = True ptc = 0 pTag.insert(ptc, token) @@ -4631,7 +4675,7 @@ class CatalogBuilder(object): # Add back
elems initially removed for elem in elems: - result.insert(rtc,elem) + result.insert(rtc, elem) rtc += 1 return result.renderContents(encoding=None) @@ -4704,7 +4748,7 @@ class CatalogBuilder(object): if rule[1].startswith('#') and rule[2] != '': field = rule[1] pat = rule[2] - exclusion_pairs.append((field,pat)) + exclusion_pairs.append((field, pat)) else: continue if exclusion_pairs: @@ -4713,12 +4757,12 @@ class CatalogBuilder(object): for record in data_set: for exclusion_pair in exclusion_pairs: - field,pat = exclusion_pair + field, pat = exclusion_pair field_contents = self.db.get_field(record['id'], field, index_is_id=True) - if (self.db.metadata_for_field(field)['datatype'] == 'bool' and + if (self.db.metadata_for_field(field)['datatype'] == 'bool' and field_contents is None): # Handle condition where field is a bool and contents is None, # which is displayed as No @@ -4787,7 +4831,7 @@ class CatalogBuilder(object): new_book = deepcopy(book) new_book['author'] = ' & '.join(cloned_authors) new_book['authors'] = list(cloned_authors) - asl = [author_to_author_sort(auth) for auth in cloned_authors] + asl = [author_to_author_sort(auth) for auth in cloned_authors] new_book['author_sort'] = ' & '.join(asl) books_by_author.append(new_book) @@ -4807,12 +4851,12 @@ class CatalogBuilder(object): self.current_step += 1 self.progress_string = description - self.progress_int = float((self.current_step-1)/self.total_steps) + self.progress_int = float((self.current_step - 1) / self.total_steps) if not self.progress_int: self.progress_int = 0.01 self.reporter(self.progress_int, self.progress_string) if self.opts.cli_environment: - self.opts.log(u"%3.0f%% %s" % (self.progress_int*100, self.progress_string)) + self.opts.log(u"%3.0f%% %s" % (self.progress_int * 100, self.progress_string)) def update_progress_micro_step(self, description, micro_step_pct): """ Update calibre's job status UI. @@ -4829,10 +4873,10 @@ class CatalogBuilder(object): (UI): Jobs UI updated """ - step_range = 100/self.total_steps + step_range = 100 / self.total_steps self.progress_string = description - coarse_progress = float((self.current_step-1)/self.total_steps) - fine_progress = float((micro_step_pct*step_range)/100) + coarse_progress = float((self.current_step - 1) / self.total_steps) + fine_progress = float((micro_step_pct * step_range) / 100) self.progress_int = coarse_progress + fine_progress self.reporter(self.progress_int, self.progress_string) @@ -4853,5 +4897,3 @@ class CatalogBuilder(object): outfile = open("%s/%s.ncx" % (self.catalog_path, self.opts.basename), 'w') outfile.write(self.ncx_soup.prettify()) - - From c1a75d585a6987b10b2f9940dd840dae68a127ff Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 4 Dec 2012 09:24:43 +0530 Subject: [PATCH 04/12] Updated Zaman Gazetesi --- recipes/zaman.recipe | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/recipes/zaman.recipe b/recipes/zaman.recipe index c322febab1..50a17e2112 100644 --- a/recipes/zaman.recipe +++ b/recipes/zaman.recipe @@ -9,15 +9,15 @@ class Zaman (BasicNewsRecipe): __author__ = u'thomass' oldest_article = 2 max_articles_per_feed =50 - # no_stylesheets = True + no_stylesheets = True #delay = 1 - #use_embedded_content = False - encoding = 'ISO 8859-9' - publisher = 'Zaman' + use_embedded_content = False + encoding = 'utf-8' + publisher = 'Feza Gazetecilik' category = 'news, haberler,TR,gazete' language = 'tr' publication_type = 'newspaper ' - extra_css = '.buyukbaslik{font-weight: bold; font-size: 18px;color:#0000FF}'#body{ font-family: Verdana,Helvetica,Arial,sans-serif } .introduction{font-weight: bold} .story-feature{display: block; padding: 0; border: 1px solid; width: 40%; font-size: small} .story-feature h2{text-align: center; text-transform: uppercase} ' + extra_css = 'h1{text-transform: capitalize; font-weight: bold; font-size: 22px;color:#0000FF} p{text-align:justify} ' #.introduction{font-weight: bold} .story-feature{display: block; padding: 0; border: 1px solid; width: 40%; font-size: small} .story-feature h2{text-align: center; text-transform: uppercase} ' conversion_options = { 'tags' : category ,'language' : language @@ -26,25 +26,26 @@ class Zaman (BasicNewsRecipe): } cover_img_url = 'https://fbcdn-profile-a.akamaihd.net/hprofile-ak-snc4/188140_81722291869_2111820_n.jpg' masthead_url = 'http://medya.zaman.com.tr/extentions/zaman.com.tr/img/section/logo-section.png' + ignore_duplicate_articles = { 'title', 'url' } + auto_cleanup = False + remove_empty_feeds= True - #keep_only_tags = [dict(name='div', attrs={'id':[ 'news-detail-content']}), dict(name='td', attrs={'class':['columnist-detail','columnist_head']}) ] - remove_tags = [ dict(name='img', attrs={'src':['http://medya.zaman.com.tr/zamantryeni/pics/zamanonline.gif']})]#,dict(name='div', attrs={'class':['radioEmbedBg','radyoProgramAdi']}),dict(name='a', attrs={'class':['webkit-html-attribute-value webkit-html-external-link']}),dict(name='table', attrs={'id':['yaziYorumTablosu']}),dict(name='img', attrs={'src':['http://medya.zaman.com.tr/pics/paylas.gif','http://medya.zaman.com.tr/extentions/zaman.com.tr/img/columnist/ma-16.png']}) + #keep_only_tags = [dict(name='div', attrs={'id':[ 'contentposition19']})]#,dict(name='div', attrs={'id':[ 'xxx']}),dict(name='div', attrs={'id':[ 'xxx']}),dict(name='div', attrs={'id':[ 'xxx']}),dict(name='div', attrs={'id':[ 'xxx']}),dict(name='div', attrs={'id':[ 'xxx']}),dict(name='div', attrs={'id':[ 'xxx']}),dict(name='div', attrs={'id':[ 'news-detail-content']}), dict(name='td', attrs={'class':['columnist-detail','columnist_head']}), ] + remove_tags = [ dict(name='img', attrs={'src':['http://cmsmedya.zaman.com.tr/images/logo/logo.bmp']}),dict(name='hr', attrs={'class':['interactive-hr']})]# remove_tags = [ dict(name='div', attrs={'class':[ 'detayUyari']}),dict(name='div', attrs={'class':[ 'detayYorum']}),dict(name='div', attrs={'class':[ 'addthis_toolbox addthis_default_style ']}),dict(name='div', attrs={'id':[ 'tumYazi']})]#,dict(name='div', attrs={'id':[ 'xxx']}),dict(name='div', attrs={'id':[ 'xxx']}),dict(name='div', attrs={'id':[ 'xxx']}),dict(name='div', attrs={'id':[ 'xxx']}),dict(name='div', attrs={'id':[ 'xxx']}),dict(name='div', attrs={'id':[ 'xxx']}),dict(name='img', attrs={'src':['http://medya.zaman.com.tr/zamantryeni/pics/zamanonline.gif']}),dict(name='div', attrs={'class':['radioEmbedBg','radyoProgramAdi']}),dict(name='a', attrs={'class':['webkit-html-attribute-value webkit-html-external-link']}),dict(name='table', attrs={'id':['yaziYorumTablosu']}),dict(name='img', attrs={'src':['http://medya.zaman.com.tr/pics/paylas.gif','http://medya.zaman.com.tr/extentions/zaman.com.tr/img/columnist/ma-16.png']}),dict(name='div', attrs={'id':[ 'news-detail-gallery']}),dict(name='div', attrs={'id':[ 'news-detail-title-bottom-part']}),dict(name='div', attrs={'id':[ 'news-detail-news-paging-main']})]# #remove_attributes = ['width','height'] remove_empty_feeds= True feeds = [ - ( u'Anasayfa', u'http://www.zaman.com.tr/anasayfa.rss'), - ( u'Son Dakika', u'http://www.zaman.com.tr/sondakika.rss'), - #( u'En çok Okunanlar', u'http://www.zaman.com.tr/max_all.rss'), - #( u'Manşet', u'http://www.zaman.com.tr/manset.rss'), - ( u'Gündem', u'http://www.zaman.com.tr/gundem.rss'), + ( u'Manşet', u'http://www.zaman.com.tr/manset.rss'), ( u'Yazarlar', u'http://www.zaman.com.tr/yazarlar.rss'), ( u'Politika', u'http://www.zaman.com.tr/politika.rss'), ( u'Ekonomi', u'http://www.zaman.com.tr/ekonomi.rss'), ( u'Dış Haberler', u'http://www.zaman.com.tr/dishaberler.rss'), + ( u'Son Dakika', u'http://www.zaman.com.tr/sondakika.rss'), + ( u'Gündem', u'http://www.zaman.com.tr/gundem.rss'), ( u'Yorumlar', u'http://www.zaman.com.tr/yorumlar.rss'), ( u'Röportaj', u'http://www.zaman.com.tr/roportaj.rss'), ( u'Dizi Yazı', u'http://www.zaman.com.tr/dizi.rss'), @@ -59,8 +60,9 @@ class Zaman (BasicNewsRecipe): ( u'Cuma Eki', u'http://www.zaman.com.tr/cuma.rss'), ( u'Cumaertesi Eki', u'http://www.zaman.com.tr/cumaertesi.rss'), ( u'Pazar Eki', u'http://www.zaman.com.tr/pazar.rss'), + ( u'En çok Okunanlar', u'http://www.zaman.com.tr/max_all.rss'), + ( u'Anasayfa', u'http://www.zaman.com.tr/anasayfa.rss'), ] def print_version(self, url): - return url.replace('http://www.zaman.com.tr/haber.do?haberno=', 'http://www.zaman.com.tr/yazdir.do?haberno=') - + return url.replace('http://www.zaman.com.tr/newsDetail_getNewsById.action?newsId=', 'http://www.zaman.com.tr/newsDetail_openPrintPage.action?newsId=') From e26b9f770ef0856fc2556d922a861cd0f6ed86a0 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 4 Dec 2012 09:26:40 +0530 Subject: [PATCH 05/12] Update Weblogs SL --- recipes/weblogs_sl.recipe | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/recipes/weblogs_sl.recipe b/recipes/weblogs_sl.recipe index 8622cccef8..b260d2dde5 100644 --- a/recipes/weblogs_sl.recipe +++ b/recipes/weblogs_sl.recipe @@ -2,8 +2,8 @@ __license__ = 'GPL v3' __copyright__ = '4 February 2011, desUBIKado' __author__ = 'desUBIKado' -__version__ = 'v0.08' -__date__ = '30, June 2012' +__version__ = 'v0.09' +__date__ = '02, December 2012' ''' http://www.weblogssl.com/ ''' @@ -37,6 +37,7 @@ class weblogssl(BasicNewsRecipe): ,(u'Xataka Mexico', u'http://feeds.weblogssl.com/xatakamx') ,(u'Xataka M\xf3vil', u'http://feeds.weblogssl.com/xatakamovil') ,(u'Xataka Android', u'http://feeds.weblogssl.com/xatakandroid') + ,(u'Xataka Windows', u'http://feeds.weblogssl.com/xatakawindows') ,(u'Xataka Foto', u'http://feeds.weblogssl.com/xatakafoto') ,(u'Xataka ON', u'http://feeds.weblogssl.com/xatakaon') ,(u'Xataka Ciencia', u'http://feeds.weblogssl.com/xatakaciencia') @@ -80,19 +81,31 @@ class weblogssl(BasicNewsRecipe): keep_only_tags = [dict(name='div', attrs={'id':'infoblock'}), dict(name='div', attrs={'class':'post'}), - dict(name='div', attrs={'id':'blog-comments'}) + dict(name='div', attrs={'id':'blog-comments'}), + dict(name='div', attrs={'class':'container'}) #m.xataka.com ] - remove_tags = [dict(name='div', attrs={'id':'comment-nav'})] + remove_tags = [dict(name='div', attrs={'id':'comment-nav'}), + dict(name='menu', attrs={'class':'social-sharing'}), #m.xataka.com + dict(name='section' , attrs={'class':'comments'}), #m.xataka.com + dict(name='div' , attrs={'class':'article-comments'}), #m.xataka.com + dict(name='nav' , attrs={'class':'article-taxonomy'}) #m.xataka.com + ] + + remove_tags_after = dict(name='section' , attrs={'class':'comments'}) def print_version(self, url): return url.replace('http://www.', 'http://m.') preprocess_regexps = [ # Para poner una linea en blanco entre un comentario y el siguiente - (re.compile(r'
  • ', re.DOTALL|re.IGNORECASE), lambda m: ''), + (re.compile(r'', re.DOTALL|re.IGNORECASE), lambda m: '') ] + # Para sustituir el video incrustado de YouTube por una imagen def preprocess_html(self, soup): @@ -108,14 +121,16 @@ class weblogssl(BasicNewsRecipe): # Para obtener la url original del articulo a partir de la de "feedsportal" # El siguiente código es gracias al usuario "bosplans" de www.mobileread.com - # http://www.mobileread.com/forums/sho...d.php?t=130297 + # http://www.mobileread.com/forums/showthread.php?t=130297 def get_article_url(self, article): link = article.get('link', None) if link is None: return article + # if link.split('/')[-4]=="xataka2": + # return article.get('feedburner_origlink', article.get('link', article.get('guid'))) if link.split('/')[-4]=="xataka2": - return article.get('feedburner_origlink', article.get('link', article.get('guid'))) + return article.get('guid', None) if link.split('/')[-1]=="story01.htm": link=link.split('/')[-2] a=['0B','0C','0D','0E','0F','0G','0N' ,'0L0S','0A'] From f53daffda9868b94cd2db74e4d1a5e0a73c43637 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 4 Dec 2012 10:09:42 +0530 Subject: [PATCH 06/12] Conversion: Fix a bug in removal of invalid entries from the spine, where not all invalid entries were removed, causing conversion to fail. Fixes #1086054 (ePub->MOBI conversion fails (url of input included)) --- src/calibre/ebooks/oeb/reader.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/calibre/ebooks/oeb/reader.py b/src/calibre/ebooks/oeb/reader.py index 8124d49c6a..0461491d2f 100644 --- a/src/calibre/ebooks/oeb/reader.py +++ b/src/calibre/ebooks/oeb/reader.py @@ -320,13 +320,11 @@ class OEBReader(object): self.logger.warn(u'Spine item %r not found' % idref) continue item = manifest.ids[idref] - spine.add(item, elem.get('linear')) - for item in spine: - if item.media_type.lower() not in OEB_DOCS: - if not hasattr(item.data, 'xpath'): - self.oeb.log.warn('The item %s is not a XML document.' - ' Removing it from spine.'%item.href) - spine.remove(item) + if item.media_type.lower() in OEB_DOCS and hasattr(item.data, 'xpath'): + spine.add(item, elem.get('linear')) + else: + self.oeb.log.warn('The item %s is not a XML document.' + ' Removing it from spine.'%item.href) if len(spine) == 0: raise OEBError("Spine is empty") self._spine_add_extra() From 710f6fc94deacbb07ed9e62567eabd22d36d180c Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 4 Dec 2012 12:08:49 +0530 Subject: [PATCH 07/12] Faster signing of windows installers --- setup/installer/windows/__init__.py | 18 ------------------ setup/installer/windows/freeze.py | 12 ++++++++++++ 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/setup/installer/windows/__init__.py b/setup/installer/windows/__init__.py index eea0f9b487..5e1ad4a7c0 100644 --- a/setup/installer/windows/__init__.py +++ b/setup/installer/windows/__init__.py @@ -39,18 +39,6 @@ class Win32(WinBase): def msi64(self): return installer_name('msi', is64bit=True) - def sign_msi(self): - import xattr - print ('Signing installers ...') - sign64 = False - msi64 = self.msi64 - if os.path.exists(msi64) and 'user.signed' not in xattr.list(msi64): - subprocess.check_call(['scp', msi64, self.VM_NAME + - ':build/%s/%s'%(__appname__, msi64)]) - sign64 = True - subprocess.check_call(['ssh', self.VM_NAME, '~/sign.sh'], shell=False) - return sign64 - def do_dl(self, installer, errmsg): subprocess.check_call(('scp', '%s:build/%s/%s'%(self.VM_NAME, __appname__, installer), 'dist')) @@ -62,14 +50,8 @@ class Win32(WinBase): installer = self.installer() if os.path.exists('build/winfrozen'): shutil.rmtree('build/winfrozen') - sign64 = self.sign_msi() - if sign64: - self.do_dl(self.msi64, 'Failed to d/l signed 64 bit installer') - import xattr - xattr.set(self.msi64, 'user.signed', 'true') self.do_dl(installer, 'Failed to freeze') - installer = 'dist/%s-portable-installer-%s.exe'%(__appname__, __version__) self.do_dl(installer, 'Failed to get portable installer') diff --git a/setup/installer/windows/freeze.py b/setup/installer/windows/freeze.py index bd05fb06c9..4008317341 100644 --- a/setup/installer/windows/freeze.py +++ b/setup/installer/windows/freeze.py @@ -91,6 +91,7 @@ class Win32Freeze(Command, WixMixIn): if not is64bit: self.build_portable() self.build_portable_installer() + self.sign_installers() def remove_CRT_from_manifests(self): ''' @@ -488,6 +489,17 @@ class Win32Freeze(Command, WixMixIn): subprocess.check_call([LZMA + r'\bin\elzma.exe', '-9', '--lzip', name]) + def sign_installers(self): + self.info('Signing installers...') + files = glob.glob(self.j('dist', '*.msi')) + glob.glob(self.j('dist', + '*.exe')) + if not files: + raise ValueError('No installers found') + subprocess.check_call(['signtool.exe', 'sign', '/a', '/d', + 'calibre - E-book management', '/du', + 'http://calibre-ebook.com', '/t', + 'http://timestamp.verisign.com/scripts/timstamp.dll'] + files) + def add_dir_to_zip(self, zf, path, prefix=''): ''' Add a directory recursively to the zip file with an optional prefix. From 9c977058aafb70334cebb351584c75871e67ee25 Mon Sep 17 00:00:00 2001 From: GRiker Date: Tue, 4 Dec 2012 09:25:02 -0700 Subject: [PATCH 08/12] Added verbosity switch for plumber when building MOBI catalogs. Improved logic for handling books_by_description. --- src/calibre/library/catalogs/epub_mobi.py | 90 ++++++++++--------- .../library/catalogs/epub_mobi_builder.py | 41 ++++----- 2 files changed, 67 insertions(+), 64 deletions(-) diff --git a/src/calibre/library/catalogs/epub_mobi.py b/src/calibre/library/catalogs/epub_mobi.py index a50c7ba861..9a7e728220 100644 --- a/src/calibre/library/catalogs/epub_mobi.py +++ b/src/calibre/library/catalogs/epub_mobi.py @@ -3,7 +3,7 @@ from __future__ import (unicode_literals, division, absolute_import, print_function) -__license__ = 'GPL v3' +__license__ = 'GPL v3' __copyright__ = '2012, Kovid Goyal ' __docformat__ = 'restructuredtext en' @@ -21,6 +21,7 @@ from calibre.utils.localization import get_lang Option = namedtuple('Option', 'option, default, dest, action, help') + class EPUB_MOBI(CatalogPlugin): 'ePub catalog generator' @@ -30,29 +31,29 @@ class EPUB_MOBI(CatalogPlugin): minimum_calibre_version = (0, 7, 40) author = 'Greg Riker' version = (1, 0, 0) - file_types = set(['azw3','epub','mobi']) + file_types = set(['azw3', 'epub', 'mobi']) THUMB_SMALLEST = "1.0" THUMB_LARGEST = "2.0" - cli_options = [Option('--catalog-title', # {{{ - default = 'My Books', - dest = 'catalog_title', - action = None, - help = _('Title of generated catalog used as title in metadata.\n' + cli_options = [Option('--catalog-title', # {{{ + default='My Books', + dest='catalog_title', + action=None, + help=_('Title of generated catalog used as title in metadata.\n' "Default: '%default'\n" "Applies to: AZW3, ePub, MOBI output formats")), Option('--cross-reference-authors', default=False, dest='cross_reference_authors', - action = 'store_true', + action='store_true', help=_("Create cross-references in Authors section for books with multiple authors.\n" "Default: '%default'\n" "Applies to: AZW3, ePub, MOBI output formats")), Option('--debug-pipeline', default=None, dest='debug_pipeline', - action = None, + action=None, help=_("Save the output from different stages of the conversion " "pipeline to the specified " "directory. Useful if you are unsure at which stage " @@ -62,7 +63,7 @@ class EPUB_MOBI(CatalogPlugin): Option('--exclude-genre', default='\[.+\]|^\+$', dest='exclude_genre', - action = None, + action=None, help=_("Regex describing tags to exclude as genres.\n" "Default: '%default' excludes bracketed tags, e.g. '[Project Gutenberg]', and '+', the default tag for read books.\n" "Applies to: AZW3, ePub, MOBI output formats")), @@ -82,63 +83,63 @@ class EPUB_MOBI(CatalogPlugin): Option('--generate-authors', default=False, dest='generate_authors', - action = 'store_true', + action='store_true', help=_("Include 'Authors' section in catalog.\n" "Default: '%default'\n" "Applies to: AZW3, ePub, MOBI output formats")), Option('--generate-descriptions', default=False, dest='generate_descriptions', - action = 'store_true', + action='store_true', help=_("Include 'Descriptions' section in catalog.\n" "Default: '%default'\n" "Applies to: AZW3, ePub, MOBI output formats")), Option('--generate-genres', default=False, dest='generate_genres', - action = 'store_true', + action='store_true', help=_("Include 'Genres' section in catalog.\n" "Default: '%default'\n" "Applies to: AZW3, ePub, MOBI output formats")), Option('--generate-titles', default=False, dest='generate_titles', - action = 'store_true', + action='store_true', help=_("Include 'Titles' section in catalog.\n" "Default: '%default'\n" "Applies to: AZW3, ePub, MOBI output formats")), Option('--generate-series', default=False, dest='generate_series', - action = 'store_true', + action='store_true', help=_("Include 'Series' section in catalog.\n" "Default: '%default'\n" "Applies to: AZW3, ePub, MOBI output formats")), Option('--generate-recently-added', default=False, dest='generate_recently_added', - action = 'store_true', + action='store_true', help=_("Include 'Recently Added' section in catalog.\n" "Default: '%default'\n" "Applies to: AZW3, ePub, MOBI output formats")), Option('--genre-source-field', default='Tags', dest='genre_source_field', - action = None, + action=None, help=_("Source field for Genres section.\n" "Default: '%default'\n" "Applies to: AZW3, ePub, MOBI output formats")), Option('--header-note-source-field', default='', dest='header_note_source_field', - action = None, + action=None, help=_("Custom field containing note text to insert in Description header.\n" "Default: '%default'\n" "Applies to: AZW3, ePub, MOBI output formats")), Option('--merge-comments-rule', default='::', dest='merge_comments_rule', - action = None, + action=None, help=_("#:[before|after]:[True|False] specifying:\n" " Custom field containing notes to merge with Comments\n" " [before|after] Placement of notes with respect to Comments\n" @@ -148,7 +149,7 @@ class EPUB_MOBI(CatalogPlugin): Option('--output-profile', default=None, dest='output_profile', - action = None, + action=None, help=_("Specifies the output profile. In some cases, an output profile is required to optimize the catalog for the device. For example, 'kindle' or 'kindle_dx' creates a structured Table of Contents with Sections and Articles.\n" "Default: '%default'\n" "Applies to: AZW3, ePub, MOBI output formats")), @@ -164,14 +165,14 @@ class EPUB_MOBI(CatalogPlugin): Option('--use-existing-cover', default=False, dest='use_existing_cover', - action = 'store_true', + action='store_true', help=_("Replace existing cover when generating the catalog.\n" "Default: '%default'\n" "Applies to: AZW3, ePub, MOBI output formats")), Option('--thumb-width', default='1.0', dest='thumb_width', - action = None, + action=None, help=_("Size hint (in inches) for book covers in catalog.\n" "Range: 1.0 - 2.0\n" "Default: '%default'\n" @@ -199,7 +200,7 @@ class EPUB_MOBI(CatalogPlugin): if opts.connected_device['name'] and 'kindle' in opts.connected_device['name'].lower(): opts.connected_kindle = True if opts.connected_device['serial'] and \ - opts.connected_device['serial'][:4] in ['B004','B005']: + opts.connected_device['serial'][:4] in ['B004', 'B005']: op = "kindle_dx" else: op = "kindle" @@ -209,7 +210,7 @@ class EPUB_MOBI(CatalogPlugin): opts.output_profile = op opts.basename = "Catalog" - opts.cli_environment = not hasattr(opts,'sync') + opts.cli_environment = not hasattr(opts, 'sync') # Hard-wired to always sort descriptions by author, with series after non-series opts.sort_descriptions_by_author = True @@ -278,14 +279,14 @@ class EPUB_MOBI(CatalogPlugin): opts.generate_genres = True opts.generate_recently_added = True opts.generate_descriptions = True - sections_list = ['Authors','Titles','Series','Genres','Recently Added','Descriptions'] + sections_list = ['Authors', 'Titles', 'Series', 'Genres', 'Recently Added', 'Descriptions'] else: opts.log.warn('\n*** No enabled Sections, terminating catalog generation ***') - return ["No Included Sections","No enabled Sections.\nCheck E-book options tab\n'Included sections'\n"] + return ["No Included Sections", "No enabled Sections.\nCheck E-book options tab\n'Included sections'\n"] if opts.fmt == 'mobi' and sections_list == ['Descriptions']: warning = _("\n*** Adding 'By Authors' Section required for MOBI output ***") opts.log.warn(warning) - sections_list.insert(0,'Authors') + sections_list.insert(0, 'Authors') opts.generate_authors = True opts.log(u" Sections: %s" % ', '.join(sections_list)) @@ -294,14 +295,14 @@ class EPUB_MOBI(CatalogPlugin): # Limit thumb_width to 1.0" - 2.0" try: if float(opts.thumb_width) < float(self.THUMB_SMALLEST): - log.warning("coercing thumb_width from '%s' to '%s'" % (opts.thumb_width,self.THUMB_SMALLEST)) + log.warning("coercing thumb_width from '%s' to '%s'" % (opts.thumb_width, self.THUMB_SMALLEST)) opts.thumb_width = self.THUMB_SMALLEST if float(opts.thumb_width) > float(self.THUMB_LARGEST): - log.warning("coercing thumb_width from '%s' to '%s'" % (opts.thumb_width,self.THUMB_LARGEST)) + log.warning("coercing thumb_width from '%s' to '%s'" % (opts.thumb_width, self.THUMB_LARGEST)) opts.thumb_width = self.THUMB_LARGEST opts.thumb_width = "%.2f" % float(opts.thumb_width) except: - log.error("coercing thumb_width from '%s' to '%s'" % (opts.thumb_width,self.THUMB_SMALLEST)) + log.error("coercing thumb_width from '%s' to '%s'" % (opts.thumb_width, self.THUMB_SMALLEST)) opts.thumb_width = "1.0" # eval prefix_rules if passed from command line @@ -331,13 +332,13 @@ class EPUB_MOBI(CatalogPlugin): keys.sort() build_log.append(" opts:") for key in keys: - if key in ['catalog_title','author_clip','connected_kindle','creator', - 'cross_reference_authors','description_clip','exclude_book_marker', - 'exclude_genre','exclude_tags','exclusion_rules', 'fmt', - 'genre_source_field', 'header_note_source_field','merge_comments_rule', - 'output_profile','prefix_rules','read_book_marker', - 'search_text','sort_by','sort_descriptions_by_author','sync', - 'thumb_width','use_existing_cover','wishlist_tag']: + if key in ['catalog_title', 'author_clip', 'connected_kindle', 'creator', + 'cross_reference_authors', 'description_clip', 'exclude_book_marker', + 'exclude_genre', 'exclude_tags', 'exclusion_rules', 'fmt', + 'genre_source_field', 'header_note_source_field', 'merge_comments_rule', + 'output_profile', 'prefix_rules', 'read_book_marker', + 'search_text', 'sort_by', 'sort_descriptions_by_author', 'sync', + 'thumb_width', 'use_existing_cover', 'wishlist_tag']: build_log.append(" %s: %s" % (key, repr(opts_dict[key]))) if opts.verbose: log('\n'.join(line for line in build_log)) @@ -370,8 +371,8 @@ class EPUB_MOBI(CatalogPlugin): """ GENERATE_DEBUG_EPUB = False if GENERATE_DEBUG_EPUB: - catalog_debug_path = os.path.join(os.path.expanduser('~'),'Desktop','Catalog debug') - setattr(opts,'debug_pipeline',os.path.expanduser(catalog_debug_path)) + catalog_debug_path = os.path.join(os.path.expanduser('~'), 'Desktop', 'Catalog debug') + setattr(opts, 'debug_pipeline', os.path.expanduser(catalog_debug_path)) dp = getattr(opts, 'debug_pipeline', None) if dp is not None: @@ -381,11 +382,13 @@ class EPUB_MOBI(CatalogPlugin): if opts.output_profile and opts.output_profile.startswith("kindle"): recommendations.append(('output_profile', opts.output_profile, OptionRecommendation.HIGH)) - recommendations.append(('book_producer',opts.output_profile, + recommendations.append(('book_producer', opts.output_profile, OptionRecommendation.HIGH)) if opts.fmt == 'mobi': recommendations.append(('no_inline_toc', True, OptionRecommendation.HIGH)) + recommendations.append(('verbose', 2, + OptionRecommendation.HIGH)) # Use existing cover or generate new cover cpath = None @@ -432,14 +435,13 @@ class EPUB_MOBI(CatalogPlugin): from calibre.ebooks.epub import initialize_container from calibre.ebooks.tweak import zip_rebuilder from calibre.utils.zipfile import ZipFile - input_path = os.path.join(catalog_debug_path,'input') - epub_shell = os.path.join(catalog_debug_path,'epub_shell.zip') + input_path = os.path.join(catalog_debug_path, 'input') + epub_shell = os.path.join(catalog_debug_path, 'epub_shell.zip') initialize_container(epub_shell, opf_name='content.opf') with ZipFile(epub_shell, 'r') as zf: zf.extractall(path=input_path) os.remove(epub_shell) - zip_rebuilder(input_path, os.path.join(catalog_debug_path,'input.epub')) + zip_rebuilder(input_path, os.path.join(catalog_debug_path, 'input.epub')) # returns to gui2.actions.catalog:catalog_generated() return catalog.error - diff --git a/src/calibre/library/catalogs/epub_mobi_builder.py b/src/calibre/library/catalogs/epub_mobi_builder.py index 24049e83f0..65ed89da5c 100644 --- a/src/calibre/library/catalogs/epub_mobi_builder.py +++ b/src/calibre/library/catalogs/epub_mobi_builder.py @@ -126,7 +126,7 @@ class CatalogBuilder(object): self.bookmarked_books_by_date_read = None self.books_by_author = None self.books_by_date_range = None - self.books_by_description = None + self.books_by_description = [] self.books_by_month = None self.books_by_series = None self.books_by_title = None @@ -748,8 +748,9 @@ class CatalogBuilder(object): # Assumes books_by_title already populated # init books_by_description before relisting multiple authors - books_by_description = list(books_by_author) if self.opts.sort_descriptions_by_author \ - else list(self.books_by_title) + if self.opts.generate_descriptions: + books_by_description = list(books_by_author) if self.opts.sort_descriptions_by_author \ + else list(self.books_by_title) if self.opts.cross_reference_authors: books_by_author = self.relist_multiple_authors(books_by_author) @@ -760,8 +761,9 @@ class CatalogBuilder(object): asl = [i['author_sort'] for i in books_by_author] las = max(asl, key=len) - self.books_by_description = sorted(books_by_description, - key=lambda x: sort_key(self._kf_books_by_author_sorter_author_sort(x, len(las)))) + if self.opts.generate_descriptions: + self.books_by_description = sorted(books_by_description, + key=lambda x: sort_key(self._kf_books_by_author_sorter_author_sort(x, len(las)))) books_by_author = sorted(books_by_author, key=lambda x: sort_key(self._kf_books_by_author_sorter_author_sort(x, len(las)))) @@ -2973,7 +2975,7 @@ class CatalogBuilder(object): contentTag = Tag(soup, 'content') contentTag['src'] = "content/ByDateAdded.html" navPointTag.insert(1, contentTag) - else: + elif self.opts.generate_descriptions: # Descriptions only contentTag = Tag(soup, 'content') contentTag['src'] = "content/book_%d.html" % int(self.books_by_description[0]['id']) @@ -4103,21 +4105,20 @@ class CatalogBuilder(object): spine.insert(stc, itemrefTag) stc += 1 - if self.opts.generate_descriptions: - for book in self.books_by_description: - # manifest - itemTag = Tag(soup, "item") - itemTag['href'] = "content/book_%d.html" % int(book['id']) - itemTag['id'] = "book%d" % int(book['id']) - itemTag['media-type'] = "application/xhtml+xml" - manifest.insert(mtc, itemTag) - mtc += 1 + for book in self.books_by_description: + # manifest + itemTag = Tag(soup, "item") + itemTag['href'] = "content/book_%d.html" % int(book['id']) + itemTag['id'] = "book%d" % int(book['id']) + itemTag['media-type'] = "application/xhtml+xml" + manifest.insert(mtc, itemTag) + mtc += 1 - # spine - itemrefTag = Tag(soup, "itemref") - itemrefTag['idref'] = "book%d" % int(book['id']) - spine.insert(stc, itemrefTag) - stc += 1 + # spine + itemrefTag = Tag(soup, "itemref") + itemrefTag['idref'] = "book%d" % int(book['id']) + spine.insert(stc, itemrefTag) + stc += 1 # Guide if self.generate_for_kindle_mobi: From 7b22c622ed4dc91b7fa4ff8b7371c56f53037935 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 5 Dec 2012 12:23:44 +0530 Subject: [PATCH 09/12] PDF Output: Fix custom size field not accepting fractional numbers as sizes --- src/calibre/ebooks/pdf/writer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/calibre/ebooks/pdf/writer.py b/src/calibre/ebooks/pdf/writer.py index a9cb951e35..76ab6b9096 100644 --- a/src/calibre/ebooks/pdf/writer.py +++ b/src/calibre/ebooks/pdf/writer.py @@ -27,10 +27,10 @@ def get_custom_size(opts): custom_size = None if opts.custom_size != None: width, sep, height = opts.custom_size.partition('x') - if height != '': + if height: try: - width = int(width) - height = int(height) + width = float(width) + height = float(height) custom_size = (width, height) except: custom_size = None From 30f57006292acfe80817348ffc3ad49362459619 Mon Sep 17 00:00:00 2001 From: davidfor Date: Wed, 5 Dec 2012 22:25:35 +1100 Subject: [PATCH 10/12] User reported an error which appears to mean the series_index is None. This works around this. --- src/calibre/devices/kobo/driver.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/calibre/devices/kobo/driver.py b/src/calibre/devices/kobo/driver.py index 60672d7167..0aa946c848 100644 --- a/src/calibre/devices/kobo/driver.py +++ b/src/calibre/devices/kobo/driver.py @@ -2357,6 +2357,8 @@ class KOBOTOUCH(KOBO): update_query = 'UPDATE content SET Series=?, SeriesNumber==? where BookID is Null and ContentID = ?' if book.series is None: update_values = (None, None, book.contentID, ) + elif book.series_index is None: # This should never happen, but... + update_values = (book.series, None, book.contentID, ) else: update_values = (book.series, "%g"%book.series_index, book.contentID, ) From f8de042bf16149caa0c6623dbd33f5915eef09f5 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 5 Dec 2012 17:09:21 +0530 Subject: [PATCH 11/12] KF8 Input: Fix handling of links in files that link to the obsolete tags instead of tags with an id attribute. Fixes #1086705 (Private bug) --- src/calibre/ebooks/mobi/reader/mobi8.py | 46 ++++++++++++------------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/src/calibre/ebooks/mobi/reader/mobi8.py b/src/calibre/ebooks/mobi/reader/mobi8.py index cdbea325fa..b6fe7d9aa0 100644 --- a/src/calibre/ebooks/mobi/reader/mobi8.py +++ b/src/calibre/ebooks/mobi/reader/mobi8.py @@ -44,6 +44,18 @@ def locate_beg_end_of_tag(ml, aid): return plt, pgt return 0, 0 +def reverse_tag_iter(block): + ''' Iterate over all tags in block in reverse order, i.e. last tag + to first tag. ''' + end = len(block) + while True: + pgt = block.rfind(b'>', 0, end) + if pgt == -1: break + plt = block.rfind(b'<', 0, pgt) + if plt == -1: break + yield block[plt:pgt+1] + end = plt + class Mobi8Reader(object): def __init__(self, mobi6_reader, log): @@ -275,13 +287,12 @@ class Mobi8Reader(object): return '%s/%s'%(fi.type, fi.filename), idtext def get_id_tag(self, pos): - # find the correct tag by actually searching in the destination - # textblock at position + # Find the first tag with a named anchor (name or id attribute) before + # pos fi = self.get_file_info(pos) if fi.num is None and fi.start is None: raise ValueError('No file contains pos: %d'%pos) textblock = self.parts[fi.num] - id_map = [] npos = pos - fi.start pgt = textblock.find(b'>', npos) plt = textblock.find(b'<', npos) @@ -290,28 +301,15 @@ class Mobi8Reader(object): if plt == npos or pgt < plt: npos = pgt + 1 textblock = textblock[0:npos] - # find id links only inside of tags - # inside any < > pair find all "id=' and return whatever is inside - # the quotes - id_pattern = re.compile(br'''<[^>]*\sid\s*=\s*['"]([^'"]*)['"][^>]*>''', - re.IGNORECASE) - for m in re.finditer(id_pattern, textblock): - id_map.append((m.start(), m.group(1))) + id_re = re.compile(br'''<[^>]+\sid\s*=\s*['"]([^'"]+)['"]''') + name_re = re.compile(br'''<\s*a\s*\sname\s*=\s*['"]([^'"]+)['"]''') + for tag in reverse_tag_iter(textblock): + m = id_re.match(tag) or name_re.match(tag) + if m is not None: + return m.group(1) - if not id_map: - # Found no id in the textblock, link must be to top of file - return b'' - # if npos is before first id= inside a tag, return the first - if npos < id_map[0][0]: - return id_map[0][1] - # if npos is after the last id= inside a tag, return the last - if npos > id_map[-1][0]: - return id_map[-1][1] - # otherwise find last id before npos - for i, item in enumerate(id_map): - if npos < item[0]: - return id_map[i-1][1] - return id_map[0][1] + # No tag found, link to start of file + return b'' def create_guide(self): guide = Guide() From edd6d773ffd666278f1e3b1171d5ead6605a68ef Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 5 Dec 2012 17:38:53 +0530 Subject: [PATCH 12/12] KF8 Input: Fix page breaks specified using the data-AmznPageBreak attribute being ignored by calibre. --- src/calibre/ebooks/mobi/reader/markup.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/calibre/ebooks/mobi/reader/markup.py b/src/calibre/ebooks/mobi/reader/markup.py index 9940af4fa4..3330c65a0a 100644 --- a/src/calibre/ebooks/mobi/reader/markup.py +++ b/src/calibre/ebooks/mobi/reader/markup.py @@ -74,11 +74,12 @@ def remove_kindlegen_markup(parts): part = "".join(srcpieces) parts[i] = part - # we can safely remove all of the Kindlegen generated data-AmznPageBreak tags + # we can safely remove all of the Kindlegen generated data-AmznPageBreak + # attributes find_tag_with_AmznPageBreak_pattern = re.compile( r'''(<[^>]*\sdata-AmznPageBreak=[^>]*>)''', re.IGNORECASE) within_tag_AmznPageBreak_position_pattern = re.compile( - r'''\sdata-AmznPageBreak=['"][^'"]*['"]''') + r'''\sdata-AmznPageBreak=['"]([^'"]*)['"]''') for i in xrange(len(parts)): part = parts[i] @@ -86,10 +87,8 @@ def remove_kindlegen_markup(parts): for j in range(len(srcpieces)): tag = srcpieces[j] if tag.startswith('<'): - for m in within_tag_AmznPageBreak_position_pattern.finditer(tag): - replacement = '' - tag = within_tag_AmznPageBreak_position_pattern.sub(replacement, tag, 1) - srcpieces[j] = tag + srcpieces[j] = within_tag_AmznPageBreak_position_pattern.sub( + lambda m:' style="page-break-after:%s"'%m.group(1), tag) part = "".join(srcpieces) parts[i] = part