From 2982a4669682ae1444ceaf6b445cc135b0eb4822 Mon Sep 17 00:00:00 2001 From: David Date: Fri, 15 Apr 2022 11:52:22 +1000 Subject: [PATCH] Updates for new firmware and book stats Updates to support firmware 4.32. This adds some books stats such as word and page count and expected reading time. Plus did some code cleanup. --- src/calibre/devices/kobo/books.py | 11 +- src/calibre/devices/kobo/driver.py | 228 +++++++++++++++---- src/calibre/devices/kobo/kobotouch_config.py | 120 +++++++++- 3 files changed, 303 insertions(+), 56 deletions(-) diff --git a/src/calibre/devices/kobo/books.py b/src/calibre/devices/kobo/books.py index 9a564d173f..9421a31bd2 100644 --- a/src/calibre/devices/kobo/books.py +++ b/src/calibre/devices/kobo/books.py @@ -70,6 +70,7 @@ class Book(Book_): self.kobo_series_number = None # Kobo stores the series number as string. And it can have a leading "#". self.kobo_series_id = None self.kobo_subtitle = None + self.kobo_bookstats = {} if thumbnail_name is not None: self.thumbnail = ImageWrapper(thumbnail_name) @@ -151,14 +152,14 @@ class KTCollectionsBookList(CollectionsBookList): if show_debug: # or len(book.device_collections) > 0: debug_print('KTCollectionsBookList:get_collections - tsval=', tsval, "book.title=", book.title, "book.title_sort=", book.title_sort) debug_print('KTCollectionsBookList:get_collections - book.device_collections=', book.device_collections) -# debug_print(book) + # debug_print(book) # Make sure we can identify this book via the lpath lpath = getattr(book, 'lpath', None) if lpath is None: continue # If the book is not in the current library, we don't want to use the metadtaa for the collections if book.application_id is None: - # debug_print("KTCollectionsBookList:get_collections - Book not in current library") + # debug_print("KTCollectionsBookList:get_collections - Book not in current library") continue # Decide how we will build the collections. The default: leave the # book in all existing collections. Do not add any new ones. @@ -205,7 +206,7 @@ class KTCollectionsBookList(CollectionsBookList): debug_print("KTCollectionsBookList:get_collections - adding book.device_collections", book.device_collections) # If the book is not in the current library, we don't want to use the metadtaa for the collections elif book.application_id is None or not book.can_put_on_shelves: - # debug_print("KTCollectionsBookList:get_collections - Book not in current library") + # debug_print("KTCollectionsBookList:get_collections - Book not in current library") continue else: doing_dc = False @@ -221,7 +222,7 @@ class KTCollectionsBookList(CollectionsBookList): val = val.decode(preferred_encoding, 'replace') if isinstance(val, (list, tuple)): val = list(val) -# debug_print("KTCollectionsBookList:get_collections - val is list=", val) + # debug_print("KTCollectionsBookList:get_collections - val is list=", val) elif fm is not None and fm['datatype'] == 'series': val = [orig_val] elif fm is not None and fm['datatype'] == 'rating': @@ -244,7 +245,7 @@ class KTCollectionsBookList(CollectionsBookList): debug_print("KTCollectionsBookList:get_collections - val=", val) for category in val: - # debug_print("KTCollectionsBookList:get_collections - category=", category) + # debug_print("KTCollectionsBookList:get_collections - category=", category) is_series = False if doing_dc: # Attempt to determine if this value is a series by diff --git a/src/calibre/devices/kobo/driver.py b/src/calibre/devices/kobo/driver.py index 37d9a4d71e..f7b2ad7cfd 100644 --- a/src/calibre/devices/kobo/driver.py +++ b/src/calibre/devices/kobo/driver.py @@ -36,6 +36,7 @@ from polyglot.builtins import iteritems, itervalues, string_or_bytes EPUB_EXT = '.epub' KEPUB_EXT = '.kepub' +KOBO_ROOT_DIR_NAME = ".kobo" DEFAULT_COVER_LETTERBOX_COLOR = '#000000' @@ -84,10 +85,11 @@ class KOBO(USBMS): dbversion = 0 fwversion = (0,0,0) + _device_version_info = None # The firmware for these devices is not being updated. But the Kobo desktop application # will update the database if the device is connected. The database structure is completely # backwardly compatible. - supported_dbversion = 162 + supported_dbversion = 169 has_kepubs = False supported_platforms = ['windows', 'osx', 'linux'] @@ -170,9 +172,14 @@ class KOBO(USBMS): def initialize(self): USBMS.initialize(self) self.dbversion = 7 + self._device_version_info = None + + def eject(self): + self._device_version_info = None + super().eject() def device_database_path(self): - return self.normalize_path(self._main_prefix + '.kobo/KoboReader.sqlite') + return os.path.join(self._main_prefix, KOBO_ROOT_DIR_NAME, 'KoboReader.sqlite') def device_database_connection(self, use_row_factory=False): import apsw @@ -197,14 +204,29 @@ class KOBO(USBMS): return dbversion + def device_version_info(self): + debug_print("device_version_info - start") + if not self._device_version_info: + version_file = os.path.join(self._main_prefix, KOBO_ROOT_DIR_NAME, "version") + debug_print(f"device_version_info - version_file={version_file}") + if os.path.isfile(version_file): + debug_print("device_version_info - have opened version_file") + vf = open(version_file, "r") + self._device_version_info = vf.read().strip().split(",") + vf.close() + debug_print("device_version_info - self._device_version_info=", self._device_version_info) + return self._device_version_info + + def device_serial_no(self): + return self.device_version_info()[0] + def get_firmware_version(self): # Determine the firmware version try: - with lopen(self.normalize_path(self._main_prefix + '.kobo/version'), 'rb') as f: - fwversion = f.readline().split(b',')[2] - fwversion = tuple(int(x) for x in fwversion.split(b'.')) - except Exception: - debug_print("Kobo::get_firmware_version - didn't get firmware version from file'") + fwversion = self.device_version_info()[2] + fwversion = tuple(int(x) for x in fwversion.split('.')) + except Exception as e: + debug_print(f"Kobo::get_firmware_version - didn't get firmware version from file' - Exception: {e}") fwversion = (0,0,0) return fwversion @@ -292,10 +314,10 @@ class KOBO(USBMS): if idx is not None: bl_cache[lpath] = None if ImageID is not None: - imagename = self.normalize_path(self._main_prefix + '.kobo/images/' + ImageID + ' - NickelBookCover.parsed') + imagename = self.normalize_path(self._main_prefix + KOBO_ROOT_DIR_NAME + '/images/' + ImageID + ' - NickelBookCover.parsed') if not os.path.exists(imagename): # Try the Touch version if the image does not exist - imagename = self.normalize_path(self._main_prefix + '.kobo/images/' + ImageID + ' - N3_LIBRARY_FULL.parsed') + imagename = self.normalize_path(self._main_prefix + KOBO_ROOT_DIR_NAME + '/images/' + ImageID + ' - N3_LIBRARY_FULL.parsed') # print "Image name Normalized: " + imagename if not os.path.exists(imagename): @@ -505,7 +527,7 @@ class KOBO(USBMS): def delete_images(self, ImageID, book_path): if ImageID is not None: - path_prefix = '.kobo/images/' + path_prefix = KOBO_ROOT_DIR_NAME + '/images/' path = self._main_prefix + path_prefix + ImageID file_endings = (' - iPhoneThumbnail.parsed', ' - bbMediumGridList.parsed', ' - NickelBookCover.parsed', ' - N3_LIBRARY_FULL.parsed', @@ -622,7 +644,7 @@ class KOBO(USBMS): ContentID = ContentID.replace(self._main_prefix, '') else: ContentID = path - ContentID = ContentID.replace(self._main_prefix + self.normalize_path('.kobo/kepub/'), '') + ContentID = ContentID.replace(self._main_prefix + self.normalize_path(KOBO_ROOT_DIR_NAME + '/kepub/'), '') if self._card_a_prefix is not None: ContentID = ContentID.replace(self._card_a_prefix, '') @@ -684,7 +706,7 @@ class KOBO(USBMS): if path.startswith("file:///mnt/onboard/"): path = self._main_prefix + path.replace("file:///mnt/onboard/", '') else: - path = self._main_prefix + '.kobo/kepub/' + path + path = self._main_prefix + KOBO_ROOT_DIR_NAME + '/kepub/' + path # print "Internal: " + path else: # if path.startswith("file:///mnt/onboard/"): @@ -1037,7 +1059,7 @@ class KOBO(USBMS): cursor.close() if ImageID is not None: - path_prefix = '.kobo/images/' + path_prefix = KOBO_ROOT_DIR_NAME + '/images/' path = self._main_prefix + path_prefix + ImageID file_endings = {' - iPhoneThumbnail.parsed':(103,150), @@ -1355,7 +1377,7 @@ class KOBOTOUCH(KOBO): ' Based on the existing Kobo driver by %s.') % KOBO.author # icon = I('devices/kobotouch.jpg') - supported_dbversion = 166 + supported_dbversion = 169 min_supported_dbversion = 53 min_dbversion_series = 65 min_dbversion_externalid = 65 @@ -1364,11 +1386,12 @@ class KOBOTOUCH(KOBO): min_dbversion_activity = 77 min_dbversion_keywords = 82 min_dbversion_seriesid = 136 + min_dbversion_bookstats = 169 # Starting with firmware version 3.19.x, the last number appears to be is a # build number. A number will be recorded here but it can be safely ignored # when testing the firmware version. - max_supported_fwversion = (4, 31, 19086) + max_supported_fwversion = (4, 32, 19501) # The following document firmware versions where new function or devices were added. # Not all are used, but this feels a good place to record it. min_fwversion_shelves = (2, 0, 0) @@ -1390,6 +1413,7 @@ class KOBOTOUCH(KOBO): min_libra2_fwversion = (4, 29, 18730) min_sage_fwversion = (4, 29, 18730) min_fwversion_audiobooks = (4, 29, 18730) + min_fwversion_bookstats = (4, 32, 19501) has_kepubs = True @@ -1636,7 +1660,7 @@ class KOBOTOUCH(KOBO): series, seriesnumber, SeriesID, SeriesNumberFloat, ISBN, Language, Subtitle, readstatus, expired, favouritesindex, accessibility, isdownloaded, - userid, bookshelves + userid, bookshelves, book_stats=None ): show_debug = self.is_debugging_title(title) # show_debug = authors == 'L. Frank Baum' @@ -1781,6 +1805,7 @@ class KOBOTOUCH(KOBO): bl[idx].kobo_series_id = SeriesID bl[idx].kobo_series_number_float = SeriesNumberFloat bl[idx].kobo_subtitle = Subtitle + bl[idx].kobo_bookstats = book_stats bl[idx].can_put_on_shelves = allow_shelves bl[idx].mime = MimeType @@ -1841,6 +1866,7 @@ class KOBOTOUCH(KOBO): book.kobo_series_id = SeriesID book.kobo_series_number_float = SeriesNumberFloat book.kobo_subtitle = Subtitle + book.kobo_bookstats = book_stats book.can_put_on_shelves = allow_shelves # debug_print('KoboTouch:update_booklist - title=', title, 'book.device_collections', book.device_collections) @@ -1912,6 +1938,10 @@ class KOBOTOUCH(KOBO): columns += ", SeriesID, SeriesNumberFloat" else: columns += ', null as SeriesID, null as SeriesNumberFloat' + if self.supports_bookstats: + columns += ", StorePages, StoreWordCount, StoreTimeToReadLowerEstimate, StoreTimeToReadUpperEstimate" + else: + columns += ', null as StorePages, null as StoreWordCount, null as StoreTimeToReadLowerEstimate, null as StoreTimeToReadUpperEstimate' where_clause = '' if self.supports_kobo_archive() or self.supports_overdrive(): @@ -2010,7 +2040,13 @@ class KOBOTOUCH(KOBO): row['ISBN'], row['Language'], row['Subtitle'], row['ReadStatus'], row['___ExpirationStatus'], int(row['FavouritesIndex']), row['Accessibility'], row['IsDownloaded'], - row['___UserID'], bookshelves + row['___UserID'], bookshelves, + book_stats={ + 'StorePages': row['StorePages'], + 'StoreWordCount': row['StoreWordCount'], + 'StoreTimeToReadLowerEstimate': row['StoreTimeToReadLowerEstimate'], + 'StoreTimeToReadUpperEstimate': row['StoreTimeToReadUpperEstimate'] + } ) if changed: @@ -2082,7 +2118,7 @@ class KOBOTOUCH(KOBO): else: if (ContentType == "6" or ContentType == "10"): if (MimeType == 'application/octet-stream'): # Audiobooks purchased from Kobo are in a different location. - path = self._main_prefix + '.kobo/audiobook/' + path + path = self._main_prefix + KOBO_ROOT_DIR_NAME + '/audiobook/' + path elif path.startswith("file:///mnt/onboard/"): path = self._main_prefix + path.replace("file:///mnt/onboard/", '') elif path.startswith("file:///mnt/sd/"): @@ -2090,7 +2126,7 @@ class KOBOTOUCH(KOBO): elif externalId: path = self._card_a_prefix + 'koboExtStorage/kepub/' + path else: - path = self._main_prefix + '.kobo/kepub/' + path + path = self._main_prefix + KOBO_ROOT_DIR_NAME + '/kepub/' + path else: # Should never get here, but, just in case... # if path.startswith("file:///mnt/onboard/"): path = path.replace("file:///mnt/onboard/", self._main_prefix) @@ -2387,7 +2423,7 @@ class KOBOTOUCH(KOBO): ContentID = ContentID.replace(self._main_prefix, '') elif not extension: ContentID = path - ContentID = ContentID.replace(self._main_prefix + self.normalize_path('.kobo/kepub/'), '') + ContentID = ContentID.replace(self._main_prefix + self.normalize_path(KOBO_ROOT_DIR_NAME + '/kepub/'), '') else: ContentID = path ContentID = ContentID.replace(self._main_prefix, "file:///mnt/onboard/") @@ -2663,7 +2699,7 @@ class KOBOTOUCH(KOBO): path_prefix = 'koboExtStorage/images-cache/' if self.supports_images_tree() else 'koboExtStorage/images/' path = os.path.join(self._card_a_prefix, path_prefix) else: - path_prefix = '.kobo-images/' if self.supports_images_tree() else '.kobo/images/' + path_prefix = '.kobo-images/' if self.supports_images_tree() else KOBO_ROOT_DIR_NAME + '/images/' path = os.path.join(self._main_prefix, path_prefix) if self.supports_images_tree() and imageId: @@ -3173,11 +3209,28 @@ class KOBOTOUCH(KOBO): debug_print("KoboTouch:set_series - end") def set_core_metadata(self, connection, book, series_only=False): - # debug_print('KoboTouch:set_core_metadata book="%s"' % book.title) + debug_print('KoboTouch:set_core_metadata book="%s"' % book.title) show_debug = self.is_debugging_title(book.title) if show_debug: debug_print(f'KoboTouch:set_core_metadata book="{book}", \nseries_only="{series_only}"') + def generate_update_from_template(book, update_values, set_clause, column_name, new_value=None, template=None, current_value=None): + if template is None or template == '': + new_value = None + else: + new_value = new_value if len(new_value.strip()) else None + if new_value is not None and new_value.startswith("PLUGBOARD TEMPLATE ERROR"): + debug_print("KoboTouch:generate_update_from_template template error - template='%s'" % template) + debug_print("KoboTouch:generate_update_from_template - new_value=", new_value) + + debug_print(f"KoboTouch:generate_update_from_template - {book.title} - column_name='{column_name}', current_value='{current_value}', new_value='{new_value}'") + if (new_value is not None and \ + (current_value is None or new_value != current_value) ) or \ + (new_value is None and current_value is not None): + update_values.append(new_value) + set_clause.append(column_name) + + plugboard = None if self.plugboard_func and not series_only: if book.contentID.endswith('.kepub.epub') or not os.path.splitext(book.contentID)[1]: @@ -3198,7 +3251,7 @@ class KOBOTOUCH(KOBO): update_query = 'UPDATE content SET ' update_values = [] - set_clause = '' + set_clause = [] changes_found = False kobo_metadata = book.kobo_metadata @@ -3229,9 +3282,9 @@ class KOBOTOUCH(KOBO): if series_changed or series_number_changed: update_values.append(new_series) - set_clause += ', Series = ? ' + set_clause.append('Series') update_values.append(new_series_number) - set_clause += ', SeriesNumber = ? ' + set_clause.append('SeriesNumber') if self.supports_series_list and book.is_sideloaded: series_id = self.kobo_series_dict.get(new_series, new_series) try: @@ -3245,50 +3298,63 @@ class KOBOTOUCH(KOBO): or not kobo_series_id == series_id \ or not kobo_series_number_float == newmi.series_index: update_values.append(series_id) - set_clause += ', SeriesID = ? ' + set_clause.append('SeriesID') update_values.append(newmi.series_index) - set_clause += ', SeriesNumberFloat = ? ' + set_clause.append('SeriesNumberFloat') if show_debug: debug_print(f"KoboTouch:set_core_metadata Setting SeriesID - new_series='{new_series}', series_id='{series_id}'") if not series_only: + pb = [] + if self.subtitle_template is not None: + pb.append((self.subtitle_template, 'subtitle')) + if self.bookstats_pagecount_template is not None: + pb.append((self.bookstats_pagecount_template, 'bookstats_pagecount')) + if self.bookstats_wordcount_template is not None: + pb.append((self.bookstats_wordcount_template, 'bookstats_wordcount')) + if self.bookstats_timetoread_upper_template is not None: + pb.append((self.bookstats_timetoread_upper_template, 'bookstats_timetoread_upper')) + if self.bookstats_timetoread_lower_template is not None: + pb.append((self.bookstats_timetoread_lower_template, 'bookstats_timetoread_lower')) + if show_debug: + debug_print(f"KoboTouch:set_core_metadata templates being used - pb='{pb}'") + book.template_to_attribute(book, pb) + if not (newmi.title == kobo_metadata.title): update_values.append(newmi.title) - set_clause += ', Title = ? ' + set_clause.append('Title') if not (authors_to_string(newmi.authors) == authors_to_string(kobo_metadata.authors)): update_values.append(authors_to_string(newmi.authors)) - set_clause += ', Attribution = ? ' + set_clause.append('Attribution') if not (newmi.publisher == kobo_metadata.publisher): update_values.append(newmi.publisher) - set_clause += ', Publisher = ? ' + set_clause.append('Publisher') if not (newmi.pubdate == kobo_metadata.pubdate): pubdate_string = strftime(self.TIMESTAMP_STRING, newmi.pubdate) if newmi.pubdate else None update_values.append(pubdate_string) - set_clause += ', DateCreated = ? ' + set_clause.append('DateCreated') if not (newmi.comments == kobo_metadata.comments): update_values.append(newmi.comments) - set_clause += ', Description = ? ' + set_clause.append('Description') if not (newmi.isbn == kobo_metadata.isbn): update_values.append(newmi.isbn) - set_clause += ', ISBN = ? ' + set_clause.append('ISBN') library_language = normalize_languages(kobo_metadata.languages, newmi.languages) library_language = library_language[0] if library_language is not None and len(library_language) > 0 else None if not (library_language == kobo_metadata.language): update_values.append(library_language) - set_clause += ', Language = ? ' + set_clause.append('Language') if self.update_subtitle: if self.subtitle_template is None or self.subtitle_template == '': new_subtitle = None else: - pb = [(self.subtitle_template, 'subtitle')] - book.template_to_attribute(book, pb) new_subtitle = book.subtitle if len(book.subtitle.strip()) else None if new_subtitle is not None and new_subtitle.startswith("PLUGBOARD TEMPLATE ERROR"): debug_print("KoboTouch:set_core_metadata subtitle template error - self.subtitle_template='%s'" % self.subtitle_template) @@ -3297,16 +3363,51 @@ class KOBOTOUCH(KOBO): if (new_subtitle is not None and (book.kobo_subtitle is None or book.subtitle != book.kobo_subtitle)) or \ (new_subtitle is None and book.kobo_subtitle is not None): update_values.append(new_subtitle) - set_clause += ', Subtitle = ? ' + set_clause.append('Subtitle') + + if self.update_bookstats: + if self.bookstats_pagecount_template is not None: + current_bookstats_pagecount = book.kobo_bookstats.get('StorePages', None) + generate_update_from_template(book, update_values, set_clause, + column_name='StorePages', + template=self.bookstats_pagecount_template, + new_value=book.bookstats_pagecount, + current_value=current_bookstats_pagecount + ) + if self.bookstats_wordcount_template is not None: + current_bookstats_wordcount = book.kobo_bookstats.get('StoreWordCount', None) + generate_update_from_template(book, update_values, set_clause, + column_name='StoreWordCount', + template=self.bookstats_wordcount_template, + new_value=book.bookstats_wordcount, + current_value=current_bookstats_wordcount + ) + if self.bookstats_timetoread_upper_template is not None: + current_bookstats_timetoread_upper = book.kobo_bookstats.get('StoreTimeToReadUpperEstimate', None) + generate_update_from_template(book, update_values, set_clause, + column_name='StoreTimeToReadUpperEstimate', + template=self.bookstats_timetoread_upper_template, + new_value=book.bookstats_timetoread_upper, + current_value=current_bookstats_timetoread_upper + ) + if self.bookstats_timetoread_lower_template is not None: + current_bookstats_timetoread_lower = book.kobo_bookstats.get('StoreTimeToReadLowerEstimate', None) + generate_update_from_template(book, update_values, set_clause, + column_name='StoreTimeToReadLowerEstimate', + template=self.bookstats_timetoread_lower_template, + new_value=book.bookstats_timetoread_lower, + current_value=current_bookstats_timetoread_lower + ) if len(set_clause) > 0: - update_query += set_clause[1:] + update_query += ', '.join([col_name + ' = ?' for col_name in set_clause]) changes_found = True if show_debug: debug_print('KoboTouch:set_core_metadata set_clause="%s"' % set_clause) debug_print('KoboTouch:set_core_metadata update_values="%s"' % update_values) + debug_print('KoboTouch:set_core_metadata update_values="%s"' % update_query) if changes_found: - update_query += 'WHERE ContentID = ? AND BookID IS NULL' + update_query += ' WHERE ContentID = ? AND BookID IS NULL' update_values.append(book.contentID) cursor = connection.cursor() try: @@ -3317,6 +3418,8 @@ class KOBOTOUCH(KOBO): self.core_metadata_set += 1 except: debug_print(' Database Exception: Unable to set the core metadata') + debug_print(f' Query was: {update_query}') + debug_print(f' Values were: {update_values}') raise finally: cursor.close() @@ -3324,6 +3427,7 @@ class KOBOTOUCH(KOBO): if show_debug: debug_print("KoboTouch:set_core_metadata - end") + @classmethod def config_widget(cls): # TODO: Cleanup the following @@ -3387,6 +3491,11 @@ class KOBOTOUCH(KOBO): c.add_opt('update_device_metadata', default=True) c.add_opt('update_subtitle', default=False) c.add_opt('subtitle_template', default=None) + c.add_opt('update_bookstats', default=False) + c.add_opt('bookstats_wordcount_template', default=None) + c.add_opt('bookstats_pagecount_template', default=None) + c.add_opt('bookstats_timetoread_upper_template', default=None) + c.add_opt('bookstats_timetoread_lower_template', default=None) c.add_opt('modify_css', default=False) c.add_opt('override_kobo_replace_existing', default=True) # Overriding the replace behaviour is how the driver has always worked. @@ -3644,6 +3753,43 @@ class KOBOTOUCH(KOBO): subtitle_template = subtitle_template.strip() if subtitle_template is not None else None return subtitle_template + @property + def update_bookstats(self): + # Subtitle was added to the database at the same time as the series support. + return self.update_device_metadata and self.supports_bookstats and self.get_pref('update_bookstats') + + @property + def bookstats_wordcount_template(self): + if not self.update_bookstats: + return None + bookstats_wordcount_template = self.get_pref('bookstats_wordcount_template') + bookstats_wordcount_template = bookstats_wordcount_template.strip() if bookstats_wordcount_template is not None else None + return bookstats_wordcount_template + + @property + def bookstats_pagecount_template(self): + if not self.update_bookstats: + return None + bookstats_pagecount_template = self.get_pref('bookstats_pagecount_template') + bookstats_pagecount_template = bookstats_pagecount_template.strip() if bookstats_pagecount_template is not None else None + return bookstats_pagecount_template + + @property + def bookstats_timetoread_lower_template(self): + if not self.update_bookstats: + return None + bookstats_timetoread_lower_template = self.get_pref('bookstats_timetoread_lower_template') + bookstats_timetoread_lower_template = bookstats_timetoread_lower_template.strip() if bookstats_timetoread_lower_template is not None else None + return bookstats_timetoread_lower_template + + @property + def bookstats_timetoread_upper_template(self): + if not self.update_bookstats: + return None + bookstats_timetoread_upper_template = self.get_pref('bookstats_timetoread_upper_template') + bookstats_timetoread_upper_template = bookstats_timetoread_upper_template.strip() if bookstats_timetoread_upper_template is not None else None + return bookstats_timetoread_upper_template + @property def update_core_metadata(self): return self.update_device_metadata and self.get_pref('update_core_metadata') @@ -3682,6 +3828,10 @@ class KOBOTOUCH(KOBO): def supports_series(self): return self.dbversion >= self.min_dbversion_series + @property + def supports_bookstats(self): + return self.fwversion >= self.min_fwversion_bookstats and self.dbversion >= self.min_dbversion_bookstats + @property def supports_series_list(self): return self.dbversion >= self.min_dbversion_seriesid and self.fwversion >= self.min_fwversion_serieslist diff --git a/src/calibre/devices/kobo/kobotouch_config.py b/src/calibre/devices/kobo/kobotouch_config.py index a195c73a1e..c5d36a9a14 100644 --- a/src/calibre/devices/kobo/kobotouch_config.py +++ b/src/calibre/devices/kobo/kobotouch_config.py @@ -120,6 +120,11 @@ class KOBOTOUCHConfig(TabbedDeviceConfig): p['update_purchased_kepubs'] = self.update_purchased_kepubs p['subtitle_template'] = self.subtitle_template p['update_subtitle'] = self.update_subtitle + p['update_bookstats'] = self.update_bookstats + p['bookstats_wordcount_template'] = self.bookstats_wordcount_template + p['bookstats_pagecount_template'] = self.bookstats_pagecount_template + p['bookstats_timetoread_upper_template'] = self.bookstats_timetoread_upper_template + p['bookstats_timetoread_lower_template'] = self.bookstats_timetoread_lower_template p['modify_css'] = self.modify_css p['override_kobo_replace_existing'] = self.override_kobo_replace_existing @@ -560,28 +565,88 @@ class MetadataGroupBox(DeviceOptionsGroupBox): "If the template is empty, the subtitle will be cleared." ) ) + self.update_bookstats_checkbox = create_checkbox( + _("Book stats"), + _('Update the book stats '), + device.get_pref('update_bookstats') + ) + self.bookstats_wordcount_template_edit = TemplateConfig( + device.get_pref('bookstats_wordcount_template'), + label=_("Words:"), + tooltip=_("Enter a template to use to set the word count for the book. " + "If the template is empty, the word count will be cleared." + ) + ) + self.bookstats_pagecount_template_edit = TemplateConfig( + device.get_pref('bookstats_pagecount_template'), + label=_("Pages:"), + tooltip=_("Enter a template to use to set the page count for the book. " + "If the template is empty, the page count will be cleared." + ) + ) - self.options_layout.addWidget(self.update_series_checkbox, 0, 0, 1, 2) - self.options_layout.addWidget(self.update_core_metadata_checkbox, 1, 0, 1, 2) - self.options_layout.addWidget(self.update_subtitle_checkbox, 2, 0, 1, 1) - self.options_layout.addWidget(self.subtitle_template_edit, 2, 1, 1, 1) - self.options_layout.addWidget(self.update_purchased_kepubs_checkbox, 3, 0, 1, 2) + self.bookstats_timetoread_label = QLabel(_('Hours to read estimates:')) + self.bookstats_timetoread_upper_template_edit = TemplateConfig( + device.get_pref('bookstats_timetoread_upper_template'), + label=_("Upper:"), + tooltip=_("Enter a template to use to set the upper estimate of the time to read for the book. " + "The estimate is in hours. " + "If the template is empty, the time will be cleared." + ) + ) + self.bookstats_timetoread_lower_template_edit = TemplateConfig( + device.get_pref('bookstats_timetoread_lower_template'), + label=_("Lower:"), + tooltip=_("Enter a template to use to set the lower estimate of the time to read for the book. " + "The estimate is in hours. " + "If the template is empty, the time will be cleared." + ) + ) + + line = 0 + self.options_layout.addWidget(self.update_series_checkbox, line, 0, 1, 4) + line += 1 + self.options_layout.addWidget(self.update_core_metadata_checkbox, line, 0, 1, 4) + line += 1 + self.options_layout.addWidget(self.update_subtitle_checkbox, line, 0, 1, 2) + self.options_layout.addWidget(self.subtitle_template_edit, line, 2, 1, 2) + line += 1 + self.options_layout.addWidget(self.update_bookstats_checkbox, line, 0, 1, 2) + self.options_layout.addWidget(self.bookstats_wordcount_template_edit, line, 2, 1, 1) + self.options_layout.addWidget(self.bookstats_pagecount_template_edit, line, 3, 1, 1) + line += 1 + self.options_layout.addWidget(self.bookstats_timetoread_label, line, 1, 1, 1) + self.options_layout.addWidget(self.bookstats_timetoread_lower_template_edit, line, 2, 1, 1) + self.options_layout.addWidget(self.bookstats_timetoread_upper_template_edit, line, 3, 1, 1) + line += 1 + self.options_layout.addWidget(self.update_purchased_kepubs_checkbox, line, 0, 1, 4) self.update_core_metadata_checkbox.clicked.connect(self.update_core_metadata_checkbox_clicked) self.update_subtitle_checkbox.clicked.connect(self.update_subtitle_checkbox_clicked) + self.update_bookstats_checkbox.clicked.connect(self.update_bookstats_checkbox_clicked) self.update_core_metadata_checkbox_clicked(device.get_pref('update_core_metadata')) self.update_subtitle_checkbox_clicked(device.get_pref('update_subtitle')) + self.update_bookstats_checkbox_clicked(device.get_pref('update_bookstats')) def update_core_metadata_checkbox_clicked(self, checked): self.update_series_checkbox.setEnabled(not checked) self.subtitle_template_edit.setEnabled(checked) self.update_subtitle_checkbox.setEnabled(checked) + self.update_bookstats_checkbox.setEnabled(checked) self.update_subtitle_checkbox_clicked(self.update_subtitle) + self.update_bookstats_checkbox_clicked(self.update_bookstats) self.update_purchased_kepubs_checkbox.setEnabled(checked) def update_subtitle_checkbox_clicked(self, checked): self.subtitle_template_edit.setEnabled(checked and self.update_core_metadata) + def update_bookstats_checkbox_clicked(self, checked): + self.bookstats_timetoread_label.setEnabled(checked and self.update_bookstats and self.update_core_metadata) + self.bookstats_wordcount_template_edit.setEnabled(checked and self.update_bookstats and self.update_core_metadata) + self.bookstats_pagecount_template_edit.setEnabled(checked and self.update_bookstats and self.update_core_metadata) + self.bookstats_timetoread_upper_template_edit.setEnabled(checked and self.update_bookstats and self.update_core_metadata) + self.bookstats_timetoread_lower_template_edit.setEnabled(checked and self.update_bookstats and self.update_core_metadata) + def edit_template(self): t = TemplateDialog(self, self.template) t.setWindowTitle(_('Edit template')) @@ -591,6 +656,14 @@ class MetadataGroupBox(DeviceOptionsGroupBox): def validate(self): if self.update_subtitle and not self.subtitle_template_edit.validate(): return False + if self.update_bookstats and not self.bookstats_pagecount_template_edit.validate(): + return False + if self.update_bookstats and not self.bookstats_wordcount_template_edit.validate(): + return False + if self.update_bookstats and not self.bookstats_timetoread_upper_template_edit.validate(): + return False + if self.update_bookstats and not self.bookstats_timetoread_lower_template_edit.validate(): + return False return True @property @@ -617,20 +690,43 @@ class MetadataGroupBox(DeviceOptionsGroupBox): def update_subtitle(self): return self.update_subtitle_checkbox.isChecked() + @property + def update_bookstats(self): + return self.update_bookstats_checkbox.isChecked() + @property + def bookstats_pagecount_template(self): + return self.bookstats_pagecount_template_edit.template + @property + def bookstats_wordcount_template(self): + return self.bookstats_wordcount_template_edit.template + @property + def bookstats_timetoread_lower_template(self): + return self.bookstats_timetoread_lower_template_edit.template + @property + def bookstats_timetoread_upper_template(self): + return self.bookstats_timetoread_upper_template_edit.template + + +from calibre.gui2.dialogs.template_line_editor import TemplateLineEditor class TemplateConfig(QWidget): # {{{ - def __init__(self, val, tooltip=None): - QWidget.__init__(self) - self.t = t = QLineEdit(self) + def __init__(self, val, label=None, tooltip=None): + super().__init__() + self.l = l = QGridLayout(self) + self.setLayout(l) + col = 0 + if label is not None: + l.addWidget(QLabel(label), 0, col, 1, 1) + col += 1 + self.t = t = TemplateLineEditor(self) t.setText(val or '') t.setCursorPosition(0) self.setMinimumWidth(300) - self.l = l = QGridLayout(self) - self.setLayout(l) - l.addWidget(t, 1, 0, 1, 1) + l.addWidget(t, 0, col, 1, 1) + col += 1 b = self.b = QPushButton(_('&Template editor')) - l.addWidget(b, 1, 1, 1, 1) + l.addWidget(b, 0, col, 1, 1) b.clicked.connect(self.edit_template) self.setToolTip(tooltip)