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.
This commit is contained in:
David 2022-04-15 11:52:22 +10:00
parent 02368d2888
commit 2982a46696
3 changed files with 303 additions and 56 deletions

View File

@ -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_number = None # Kobo stores the series number as string. And it can have a leading "#".
self.kobo_series_id = None self.kobo_series_id = None
self.kobo_subtitle = None self.kobo_subtitle = None
self.kobo_bookstats = {}
if thumbnail_name is not None: if thumbnail_name is not None:
self.thumbnail = ImageWrapper(thumbnail_name) self.thumbnail = ImageWrapper(thumbnail_name)
@ -151,14 +152,14 @@ class KTCollectionsBookList(CollectionsBookList):
if show_debug: # or len(book.device_collections) > 0: 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 - 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('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 # Make sure we can identify this book via the lpath
lpath = getattr(book, 'lpath', None) lpath = getattr(book, 'lpath', None)
if lpath is None: if lpath is None:
continue continue
# If the book is not in the current library, we don't want to use the metadtaa for the collections # 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: 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 continue
# Decide how we will build the collections. The default: leave the # Decide how we will build the collections. The default: leave the
# book in all existing collections. Do not add any new ones. # 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) 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 # 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: 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 continue
else: else:
doing_dc = False doing_dc = False
@ -221,7 +222,7 @@ class KTCollectionsBookList(CollectionsBookList):
val = val.decode(preferred_encoding, 'replace') val = val.decode(preferred_encoding, 'replace')
if isinstance(val, (list, tuple)): if isinstance(val, (list, tuple)):
val = list(val) 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': elif fm is not None and fm['datatype'] == 'series':
val = [orig_val] val = [orig_val]
elif fm is not None and fm['datatype'] == 'rating': elif fm is not None and fm['datatype'] == 'rating':
@ -244,7 +245,7 @@ class KTCollectionsBookList(CollectionsBookList):
debug_print("KTCollectionsBookList:get_collections - val=", val) debug_print("KTCollectionsBookList:get_collections - val=", val)
for category in val: for category in val:
# debug_print("KTCollectionsBookList:get_collections - category=", category) # debug_print("KTCollectionsBookList:get_collections - category=", category)
is_series = False is_series = False
if doing_dc: if doing_dc:
# Attempt to determine if this value is a series by # Attempt to determine if this value is a series by

View File

@ -36,6 +36,7 @@ from polyglot.builtins import iteritems, itervalues, string_or_bytes
EPUB_EXT = '.epub' EPUB_EXT = '.epub'
KEPUB_EXT = '.kepub' KEPUB_EXT = '.kepub'
KOBO_ROOT_DIR_NAME = ".kobo"
DEFAULT_COVER_LETTERBOX_COLOR = '#000000' DEFAULT_COVER_LETTERBOX_COLOR = '#000000'
@ -84,10 +85,11 @@ class KOBO(USBMS):
dbversion = 0 dbversion = 0
fwversion = (0,0,0) fwversion = (0,0,0)
_device_version_info = None
# The firmware for these devices is not being updated. But the Kobo desktop application # 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 # will update the database if the device is connected. The database structure is completely
# backwardly compatible. # backwardly compatible.
supported_dbversion = 162 supported_dbversion = 169
has_kepubs = False has_kepubs = False
supported_platforms = ['windows', 'osx', 'linux'] supported_platforms = ['windows', 'osx', 'linux']
@ -170,9 +172,14 @@ class KOBO(USBMS):
def initialize(self): def initialize(self):
USBMS.initialize(self) USBMS.initialize(self)
self.dbversion = 7 self.dbversion = 7
self._device_version_info = None
def eject(self):
self._device_version_info = None
super().eject()
def device_database_path(self): 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): def device_database_connection(self, use_row_factory=False):
import apsw import apsw
@ -197,14 +204,29 @@ class KOBO(USBMS):
return dbversion 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): def get_firmware_version(self):
# Determine the firmware version # Determine the firmware version
try: try:
with lopen(self.normalize_path(self._main_prefix + '.kobo/version'), 'rb') as f: fwversion = self.device_version_info()[2]
fwversion = f.readline().split(b',')[2] fwversion = tuple(int(x) for x in fwversion.split('.'))
fwversion = tuple(int(x) for x in fwversion.split(b'.')) except Exception as e:
except Exception: debug_print(f"Kobo::get_firmware_version - didn't get firmware version from file' - Exception: {e}")
debug_print("Kobo::get_firmware_version - didn't get firmware version from file'")
fwversion = (0,0,0) fwversion = (0,0,0)
return fwversion return fwversion
@ -292,10 +314,10 @@ class KOBO(USBMS):
if idx is not None: if idx is not None:
bl_cache[lpath] = None bl_cache[lpath] = None
if ImageID is not 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): if not os.path.exists(imagename):
# Try the Touch version if the image does not exist # 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 # print "Image name Normalized: " + imagename
if not os.path.exists(imagename): if not os.path.exists(imagename):
@ -505,7 +527,7 @@ class KOBO(USBMS):
def delete_images(self, ImageID, book_path): def delete_images(self, ImageID, book_path):
if ImageID is not None: if ImageID is not None:
path_prefix = '.kobo/images/' path_prefix = KOBO_ROOT_DIR_NAME + '/images/'
path = self._main_prefix + path_prefix + ImageID path = self._main_prefix + path_prefix + ImageID
file_endings = (' - iPhoneThumbnail.parsed', ' - bbMediumGridList.parsed', ' - NickelBookCover.parsed', ' - N3_LIBRARY_FULL.parsed', 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, '') ContentID = ContentID.replace(self._main_prefix, '')
else: else:
ContentID = path 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: if self._card_a_prefix is not None:
ContentID = ContentID.replace(self._card_a_prefix, '') ContentID = ContentID.replace(self._card_a_prefix, '')
@ -684,7 +706,7 @@ class KOBO(USBMS):
if path.startswith("file:///mnt/onboard/"): if path.startswith("file:///mnt/onboard/"):
path = self._main_prefix + path.replace("file:///mnt/onboard/", '') path = self._main_prefix + path.replace("file:///mnt/onboard/", '')
else: else:
path = self._main_prefix + '.kobo/kepub/' + path path = self._main_prefix + KOBO_ROOT_DIR_NAME + '/kepub/' + path
# print "Internal: " + path # print "Internal: " + path
else: else:
# if path.startswith("file:///mnt/onboard/"): # if path.startswith("file:///mnt/onboard/"):
@ -1037,7 +1059,7 @@ class KOBO(USBMS):
cursor.close() cursor.close()
if ImageID is not None: if ImageID is not None:
path_prefix = '.kobo/images/' path_prefix = KOBO_ROOT_DIR_NAME + '/images/'
path = self._main_prefix + path_prefix + ImageID path = self._main_prefix + path_prefix + ImageID
file_endings = {' - iPhoneThumbnail.parsed':(103,150), file_endings = {' - iPhoneThumbnail.parsed':(103,150),
@ -1355,7 +1377,7 @@ class KOBOTOUCH(KOBO):
' Based on the existing Kobo driver by %s.') % KOBO.author ' Based on the existing Kobo driver by %s.') % KOBO.author
# icon = I('devices/kobotouch.jpg') # icon = I('devices/kobotouch.jpg')
supported_dbversion = 166 supported_dbversion = 169
min_supported_dbversion = 53 min_supported_dbversion = 53
min_dbversion_series = 65 min_dbversion_series = 65
min_dbversion_externalid = 65 min_dbversion_externalid = 65
@ -1364,11 +1386,12 @@ class KOBOTOUCH(KOBO):
min_dbversion_activity = 77 min_dbversion_activity = 77
min_dbversion_keywords = 82 min_dbversion_keywords = 82
min_dbversion_seriesid = 136 min_dbversion_seriesid = 136
min_dbversion_bookstats = 169
# Starting with firmware version 3.19.x, the last number appears to be is a # 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 # build number. A number will be recorded here but it can be safely ignored
# when testing the firmware version. # 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. # 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. # Not all are used, but this feels a good place to record it.
min_fwversion_shelves = (2, 0, 0) min_fwversion_shelves = (2, 0, 0)
@ -1390,6 +1413,7 @@ class KOBOTOUCH(KOBO):
min_libra2_fwversion = (4, 29, 18730) min_libra2_fwversion = (4, 29, 18730)
min_sage_fwversion = (4, 29, 18730) min_sage_fwversion = (4, 29, 18730)
min_fwversion_audiobooks = (4, 29, 18730) min_fwversion_audiobooks = (4, 29, 18730)
min_fwversion_bookstats = (4, 32, 19501)
has_kepubs = True has_kepubs = True
@ -1636,7 +1660,7 @@ class KOBOTOUCH(KOBO):
series, seriesnumber, SeriesID, SeriesNumberFloat, series, seriesnumber, SeriesID, SeriesNumberFloat,
ISBN, Language, Subtitle, ISBN, Language, Subtitle,
readstatus, expired, favouritesindex, accessibility, isdownloaded, readstatus, expired, favouritesindex, accessibility, isdownloaded,
userid, bookshelves userid, bookshelves, book_stats=None
): ):
show_debug = self.is_debugging_title(title) show_debug = self.is_debugging_title(title)
# show_debug = authors == 'L. Frank Baum' # show_debug = authors == 'L. Frank Baum'
@ -1781,6 +1805,7 @@ class KOBOTOUCH(KOBO):
bl[idx].kobo_series_id = SeriesID bl[idx].kobo_series_id = SeriesID
bl[idx].kobo_series_number_float = SeriesNumberFloat bl[idx].kobo_series_number_float = SeriesNumberFloat
bl[idx].kobo_subtitle = Subtitle bl[idx].kobo_subtitle = Subtitle
bl[idx].kobo_bookstats = book_stats
bl[idx].can_put_on_shelves = allow_shelves bl[idx].can_put_on_shelves = allow_shelves
bl[idx].mime = MimeType bl[idx].mime = MimeType
@ -1841,6 +1866,7 @@ class KOBOTOUCH(KOBO):
book.kobo_series_id = SeriesID book.kobo_series_id = SeriesID
book.kobo_series_number_float = SeriesNumberFloat book.kobo_series_number_float = SeriesNumberFloat
book.kobo_subtitle = Subtitle book.kobo_subtitle = Subtitle
book.kobo_bookstats = book_stats
book.can_put_on_shelves = allow_shelves book.can_put_on_shelves = allow_shelves
# debug_print('KoboTouch:update_booklist - title=', title, 'book.device_collections', book.device_collections) # debug_print('KoboTouch:update_booklist - title=', title, 'book.device_collections', book.device_collections)
@ -1912,6 +1938,10 @@ class KOBOTOUCH(KOBO):
columns += ", SeriesID, SeriesNumberFloat" columns += ", SeriesID, SeriesNumberFloat"
else: else:
columns += ', null as SeriesID, null as SeriesNumberFloat' 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 = '' where_clause = ''
if self.supports_kobo_archive() or self.supports_overdrive(): if self.supports_kobo_archive() or self.supports_overdrive():
@ -2010,7 +2040,13 @@ class KOBOTOUCH(KOBO):
row['ISBN'], row['Language'], row['Subtitle'], row['ISBN'], row['Language'], row['Subtitle'],
row['ReadStatus'], row['___ExpirationStatus'], row['ReadStatus'], row['___ExpirationStatus'],
int(row['FavouritesIndex']), row['Accessibility'], row['IsDownloaded'], 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: if changed:
@ -2082,7 +2118,7 @@ class KOBOTOUCH(KOBO):
else: else:
if (ContentType == "6" or ContentType == "10"): if (ContentType == "6" or ContentType == "10"):
if (MimeType == 'application/octet-stream'): # Audiobooks purchased from Kobo are in a different location. 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/"): elif path.startswith("file:///mnt/onboard/"):
path = self._main_prefix + path.replace("file:///mnt/onboard/", '') path = self._main_prefix + path.replace("file:///mnt/onboard/", '')
elif path.startswith("file:///mnt/sd/"): elif path.startswith("file:///mnt/sd/"):
@ -2090,7 +2126,7 @@ class KOBOTOUCH(KOBO):
elif externalId: elif externalId:
path = self._card_a_prefix + 'koboExtStorage/kepub/' + path path = self._card_a_prefix + 'koboExtStorage/kepub/' + path
else: 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... else: # Should never get here, but, just in case...
# if path.startswith("file:///mnt/onboard/"): # if path.startswith("file:///mnt/onboard/"):
path = path.replace("file:///mnt/onboard/", self._main_prefix) path = path.replace("file:///mnt/onboard/", self._main_prefix)
@ -2387,7 +2423,7 @@ class KOBOTOUCH(KOBO):
ContentID = ContentID.replace(self._main_prefix, '') ContentID = ContentID.replace(self._main_prefix, '')
elif not extension: elif not extension:
ContentID = path 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: else:
ContentID = path ContentID = path
ContentID = ContentID.replace(self._main_prefix, "file:///mnt/onboard/") 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_prefix = 'koboExtStorage/images-cache/' if self.supports_images_tree() else 'koboExtStorage/images/'
path = os.path.join(self._card_a_prefix, path_prefix) path = os.path.join(self._card_a_prefix, path_prefix)
else: 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) path = os.path.join(self._main_prefix, path_prefix)
if self.supports_images_tree() and imageId: if self.supports_images_tree() and imageId:
@ -3173,11 +3209,28 @@ class KOBOTOUCH(KOBO):
debug_print("KoboTouch:set_series - end") debug_print("KoboTouch:set_series - end")
def set_core_metadata(self, connection, book, series_only=False): 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) show_debug = self.is_debugging_title(book.title)
if show_debug: if show_debug:
debug_print(f'KoboTouch:set_core_metadata book="{book}", \nseries_only="{series_only}"') 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 plugboard = None
if self.plugboard_func and not series_only: if self.plugboard_func and not series_only:
if book.contentID.endswith('.kepub.epub') or not os.path.splitext(book.contentID)[1]: 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_query = 'UPDATE content SET '
update_values = [] update_values = []
set_clause = '' set_clause = []
changes_found = False changes_found = False
kobo_metadata = book.kobo_metadata kobo_metadata = book.kobo_metadata
@ -3229,9 +3282,9 @@ class KOBOTOUCH(KOBO):
if series_changed or series_number_changed: if series_changed or series_number_changed:
update_values.append(new_series) update_values.append(new_series)
set_clause += ', Series = ? ' set_clause.append('Series')
update_values.append(new_series_number) update_values.append(new_series_number)
set_clause += ', SeriesNumber = ? ' set_clause.append('SeriesNumber')
if self.supports_series_list and book.is_sideloaded: if self.supports_series_list and book.is_sideloaded:
series_id = self.kobo_series_dict.get(new_series, new_series) series_id = self.kobo_series_dict.get(new_series, new_series)
try: try:
@ -3245,50 +3298,63 @@ class KOBOTOUCH(KOBO):
or not kobo_series_id == series_id \ or not kobo_series_id == series_id \
or not kobo_series_number_float == newmi.series_index: or not kobo_series_number_float == newmi.series_index:
update_values.append(series_id) update_values.append(series_id)
set_clause += ', SeriesID = ? ' set_clause.append('SeriesID')
update_values.append(newmi.series_index) update_values.append(newmi.series_index)
set_clause += ', SeriesNumberFloat = ? ' set_clause.append('SeriesNumberFloat')
if show_debug: if show_debug:
debug_print(f"KoboTouch:set_core_metadata Setting SeriesID - new_series='{new_series}', series_id='{series_id}'") debug_print(f"KoboTouch:set_core_metadata Setting SeriesID - new_series='{new_series}', series_id='{series_id}'")
if not series_only: 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): if not (newmi.title == kobo_metadata.title):
update_values.append(newmi.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)): if not (authors_to_string(newmi.authors) == authors_to_string(kobo_metadata.authors)):
update_values.append(authors_to_string(newmi.authors)) update_values.append(authors_to_string(newmi.authors))
set_clause += ', Attribution = ? ' set_clause.append('Attribution')
if not (newmi.publisher == kobo_metadata.publisher): if not (newmi.publisher == kobo_metadata.publisher):
update_values.append(newmi.publisher) update_values.append(newmi.publisher)
set_clause += ', Publisher = ? ' set_clause.append('Publisher')
if not (newmi.pubdate == kobo_metadata.pubdate): if not (newmi.pubdate == kobo_metadata.pubdate):
pubdate_string = strftime(self.TIMESTAMP_STRING, newmi.pubdate) if newmi.pubdate else None pubdate_string = strftime(self.TIMESTAMP_STRING, newmi.pubdate) if newmi.pubdate else None
update_values.append(pubdate_string) update_values.append(pubdate_string)
set_clause += ', DateCreated = ? ' set_clause.append('DateCreated')
if not (newmi.comments == kobo_metadata.comments): if not (newmi.comments == kobo_metadata.comments):
update_values.append(newmi.comments) update_values.append(newmi.comments)
set_clause += ', Description = ? ' set_clause.append('Description')
if not (newmi.isbn == kobo_metadata.isbn): if not (newmi.isbn == kobo_metadata.isbn):
update_values.append(newmi.isbn) update_values.append(newmi.isbn)
set_clause += ', ISBN = ? ' set_clause.append('ISBN')
library_language = normalize_languages(kobo_metadata.languages, newmi.languages) 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 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): if not (library_language == kobo_metadata.language):
update_values.append(library_language) update_values.append(library_language)
set_clause += ', Language = ? ' set_clause.append('Language')
if self.update_subtitle: if self.update_subtitle:
if self.subtitle_template is None or self.subtitle_template == '': if self.subtitle_template is None or self.subtitle_template == '':
new_subtitle = None new_subtitle = None
else: else:
pb = [(self.subtitle_template, 'subtitle')]
book.template_to_attribute(book, pb)
new_subtitle = book.subtitle if len(book.subtitle.strip()) else None new_subtitle = book.subtitle if len(book.subtitle.strip()) else None
if new_subtitle is not None and new_subtitle.startswith("PLUGBOARD TEMPLATE ERROR"): 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) 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 \ 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): (new_subtitle is None and book.kobo_subtitle is not None):
update_values.append(new_subtitle) 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: if len(set_clause) > 0:
update_query += set_clause[1:] update_query += ', '.join([col_name + ' = ?' for col_name in set_clause])
changes_found = True changes_found = True
if show_debug: if show_debug:
debug_print('KoboTouch:set_core_metadata set_clause="%s"' % set_clause) 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_values)
debug_print('KoboTouch:set_core_metadata update_values="%s"' % update_query)
if changes_found: if changes_found:
update_query += 'WHERE ContentID = ? AND BookID IS NULL' update_query += ' WHERE ContentID = ? AND BookID IS NULL'
update_values.append(book.contentID) update_values.append(book.contentID)
cursor = connection.cursor() cursor = connection.cursor()
try: try:
@ -3317,6 +3418,8 @@ class KOBOTOUCH(KOBO):
self.core_metadata_set += 1 self.core_metadata_set += 1
except: except:
debug_print(' Database Exception: Unable to set the core metadata') 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 raise
finally: finally:
cursor.close() cursor.close()
@ -3324,6 +3427,7 @@ class KOBOTOUCH(KOBO):
if show_debug: if show_debug:
debug_print("KoboTouch:set_core_metadata - end") debug_print("KoboTouch:set_core_metadata - end")
@classmethod @classmethod
def config_widget(cls): def config_widget(cls):
# TODO: Cleanup the following # TODO: Cleanup the following
@ -3387,6 +3491,11 @@ class KOBOTOUCH(KOBO):
c.add_opt('update_device_metadata', default=True) c.add_opt('update_device_metadata', default=True)
c.add_opt('update_subtitle', default=False) c.add_opt('update_subtitle', default=False)
c.add_opt('subtitle_template', default=None) 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('modify_css', default=False)
c.add_opt('override_kobo_replace_existing', default=True) # Overriding the replace behaviour is how the driver has always worked. 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 subtitle_template = subtitle_template.strip() if subtitle_template is not None else None
return subtitle_template 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 @property
def update_core_metadata(self): def update_core_metadata(self):
return self.update_device_metadata and self.get_pref('update_core_metadata') return self.update_device_metadata and self.get_pref('update_core_metadata')
@ -3682,6 +3828,10 @@ class KOBOTOUCH(KOBO):
def supports_series(self): def supports_series(self):
return self.dbversion >= self.min_dbversion_series 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 @property
def supports_series_list(self): def supports_series_list(self):
return self.dbversion >= self.min_dbversion_seriesid and self.fwversion >= self.min_fwversion_serieslist return self.dbversion >= self.min_dbversion_seriesid and self.fwversion >= self.min_fwversion_serieslist

View File

@ -120,6 +120,11 @@ class KOBOTOUCHConfig(TabbedDeviceConfig):
p['update_purchased_kepubs'] = self.update_purchased_kepubs p['update_purchased_kepubs'] = self.update_purchased_kepubs
p['subtitle_template'] = self.subtitle_template p['subtitle_template'] = self.subtitle_template
p['update_subtitle'] = self.update_subtitle 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['modify_css'] = self.modify_css
p['override_kobo_replace_existing'] = self.override_kobo_replace_existing 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." "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.bookstats_timetoread_label = QLabel(_('Hours to read estimates:'))
self.options_layout.addWidget(self.update_core_metadata_checkbox, 1, 0, 1, 2) self.bookstats_timetoread_upper_template_edit = TemplateConfig(
self.options_layout.addWidget(self.update_subtitle_checkbox, 2, 0, 1, 1) device.get_pref('bookstats_timetoread_upper_template'),
self.options_layout.addWidget(self.subtitle_template_edit, 2, 1, 1, 1) label=_("Upper:"),
self.options_layout.addWidget(self.update_purchased_kepubs_checkbox, 3, 0, 1, 2) 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_core_metadata_checkbox.clicked.connect(self.update_core_metadata_checkbox_clicked)
self.update_subtitle_checkbox.clicked.connect(self.update_subtitle_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_core_metadata_checkbox_clicked(device.get_pref('update_core_metadata'))
self.update_subtitle_checkbox_clicked(device.get_pref('update_subtitle')) 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): def update_core_metadata_checkbox_clicked(self, checked):
self.update_series_checkbox.setEnabled(not checked) self.update_series_checkbox.setEnabled(not checked)
self.subtitle_template_edit.setEnabled(checked) self.subtitle_template_edit.setEnabled(checked)
self.update_subtitle_checkbox.setEnabled(checked) self.update_subtitle_checkbox.setEnabled(checked)
self.update_bookstats_checkbox.setEnabled(checked)
self.update_subtitle_checkbox_clicked(self.update_subtitle) self.update_subtitle_checkbox_clicked(self.update_subtitle)
self.update_bookstats_checkbox_clicked(self.update_bookstats)
self.update_purchased_kepubs_checkbox.setEnabled(checked) self.update_purchased_kepubs_checkbox.setEnabled(checked)
def update_subtitle_checkbox_clicked(self, checked): def update_subtitle_checkbox_clicked(self, checked):
self.subtitle_template_edit.setEnabled(checked and self.update_core_metadata) 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): def edit_template(self):
t = TemplateDialog(self, self.template) t = TemplateDialog(self, self.template)
t.setWindowTitle(_('Edit template')) t.setWindowTitle(_('Edit template'))
@ -591,6 +656,14 @@ class MetadataGroupBox(DeviceOptionsGroupBox):
def validate(self): def validate(self):
if self.update_subtitle and not self.subtitle_template_edit.validate(): if self.update_subtitle and not self.subtitle_template_edit.validate():
return False 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 return True
@property @property
@ -617,20 +690,43 @@ class MetadataGroupBox(DeviceOptionsGroupBox):
def update_subtitle(self): def update_subtitle(self):
return self.update_subtitle_checkbox.isChecked() 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): # {{{ class TemplateConfig(QWidget): # {{{
def __init__(self, val, tooltip=None): def __init__(self, val, label=None, tooltip=None):
QWidget.__init__(self) super().__init__()
self.t = t = QLineEdit(self) 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.setText(val or '')
t.setCursorPosition(0) t.setCursorPosition(0)
self.setMinimumWidth(300) self.setMinimumWidth(300)
self.l = l = QGridLayout(self) l.addWidget(t, 0, col, 1, 1)
self.setLayout(l) col += 1
l.addWidget(t, 1, 0, 1, 1)
b = self.b = QPushButton(_('&Template editor')) 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) b.clicked.connect(self.edit_template)
self.setToolTip(tooltip) self.setToolTip(tooltip)