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)