This commit is contained in:
Kovid Goyal 2022-04-15 08:21:39 +05:30
commit 2b40d1dbfb
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
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,7 +152,7 @@ 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:
@ -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':

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)