%PDF- %PDF-
| Direktori : /usr/lib/calibre/calibre/devices/kobo/ |
| Current File : //usr/lib/calibre/calibre/devices/kobo/driver.py |
#!/usr/bin/env python3
__license__ = 'GPL v3'
__copyright__ = '2010-2019, Timothy Legge <timlegge@gmail.com>, Kovid Goyal <kovid@kovidgoyal.net> and David Forrester <davidfor@internode.on.net>'
__docformat__ = 'restructuredtext en'
'''
Driver for Kobo eReaders. Supports all e-ink devices.
Originally developed by Timothy Legge <timlegge@gmail.com>.
Extended to support Touch firmware 2.0.0 and later and newer devices by David Forrester <davidfor@internode.on.net>
'''
import os, time, shutil, re
from contextlib import closing
from datetime import datetime
from calibre import strftime
from calibre.utils.date import parse_date
from calibre.devices.usbms.books import BookList
from calibre.devices.usbms.books import CollectionsBookList
from calibre.devices.kobo.books import KTCollectionsBookList
from calibre.ebooks.metadata import authors_to_string
from calibre.ebooks.metadata.book.base import Metadata
from calibre.ebooks.metadata.utils import normalize_languages
from calibre.devices.kobo.books import Book
from calibre.devices.kobo.books import ImageWrapper
from calibre.devices.mime import mime_type_ext
from calibre.devices.usbms.driver import USBMS, debug_print
from calibre import prints, fsync
from calibre.ptempfile import PersistentTemporaryFile, better_mktemp
from calibre.constants import DEBUG
from calibre.utils.config_base import prefs
from polyglot.builtins import iteritems, itervalues, string_or_bytes
EPUB_EXT = '.epub'
KEPUB_EXT = '.kepub'
DEFAULT_COVER_LETTERBOX_COLOR = '#000000'
# Implementation of QtQHash for strings. This doesn't seem to be in the Python implementation.
def qhash(inputstr):
instr = b""
if isinstance(inputstr, bytes):
instr = inputstr
elif isinstance(inputstr, str):
instr = inputstr.encode("utf8")
else:
return -1
h = 0x00000000
for x in bytearray(instr):
h = (h << 4) + x
h ^= (h & 0xf0000000) >> 23
h &= 0x0fffffff
return h
def any_in(haystack, *needles):
for n in needles:
if n in haystack:
return True
return False
class DummyCSSPreProcessor:
def __call__(self, data, add_namespace=False):
return data
class KOBO(USBMS):
name = 'Kobo Reader Device Interface'
gui_name = 'Kobo Reader'
description = _('Communicate with the original Kobo Reader and the Kobo WiFi.')
author = 'Timothy Legge and David Forrester'
version = (2, 5, 1)
dbversion = 0
fwversion = (0,0,0)
# 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
has_kepubs = False
supported_platforms = ['windows', 'osx', 'linux']
booklist_class = CollectionsBookList
book_class = Book
# Ordered list of supported formats
FORMATS = ['kepub', 'epub', 'pdf', 'txt', 'cbz', 'cbr']
CAN_SET_METADATA = ['collections']
VENDOR_ID = [0x2237]
BCD = [0x0110, 0x0323, 0x0326]
ORIGINAL_PRODUCT_ID = [0x4165]
WIFI_PRODUCT_ID = [0x4161, 0x4162]
PRODUCT_ID = ORIGINAL_PRODUCT_ID + WIFI_PRODUCT_ID
VENDOR_NAME = ['KOBO_INC', 'KOBO']
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = ['.KOBOEREADER', 'EREADER']
EBOOK_DIR_MAIN = ''
SUPPORTS_SUB_DIRS = True
SUPPORTS_ANNOTATIONS = True
# "kepubs" do not have an extension. The name looks like a GUID. Using an empty string seems to work.
VIRTUAL_BOOK_EXTENSIONS = frozenset(('kobo', ''))
EXTRA_CUSTOMIZATION_MESSAGE = [
_('The Kobo supports several collections including ')+ 'Read, Closed, Im_Reading. ' + _(
'Create tags for automatic management'),
_('Upload covers for books (newer readers)') + ':::'+_(
'Normally, the Kobo readers get the cover image from the'
' e-book file itself. With this option, calibre will send a '
'separate cover image to the reader, useful if you '
'have modified the cover.'),
_('Upload black and white covers'),
_('Show expired books') + ':::'+_(
'A bug in an earlier version left non kepubs book records'
' in the database. With this option calibre will show the '
'expired records and allow you to delete them with '
'the new delete logic.'),
_('Show previews') + ':::'+_(
'Kobo previews are included on the Touch and some other versions.'
' By default, they are no longer displayed as there is no good reason to '
'see them. Enable if you wish to see/delete them.'),
_('Show recommendations') + ':::'+_(
'Kobo now shows recommendations on the device. In some cases these have '
'files but in other cases they are just pointers to the web site to buy. '
'Enable if you wish to see/delete them.'),
_('Attempt to support newer firmware') + ':::'+_(
'Kobo routinely updates the firmware and the '
'database version. With this option calibre will attempt '
'to perform full read-write functionality - Here be Dragons!! '
'Enable only if you are comfortable with restoring your kobo '
'to Factory defaults and testing software'),
]
EXTRA_CUSTOMIZATION_DEFAULT = [
', '.join(['tags']),
True,
True,
True,
False,
False,
False
]
OPT_COLLECTIONS = 0
OPT_UPLOAD_COVERS = 1
OPT_UPLOAD_GRAYSCALE_COVERS = 2
OPT_SHOW_EXPIRED_BOOK_RECORDS = 3
OPT_SHOW_PREVIEWS = 4
OPT_SHOW_RECOMMENDATIONS = 5
OPT_SUPPORT_NEWER_FIRMWARE = 6
def __init__(self, *args, **kwargs):
USBMS.__init__(self, *args, **kwargs)
self.plugboards = self.plugboard_func = None
def initialize(self):
USBMS.initialize(self)
self.dbversion = 7
def device_database_path(self):
return self.normalize_path(self._main_prefix + '.kobo/KoboReader.sqlite')
def device_database_connection(self, use_row_factory=False):
import apsw
db_connection = apsw.Connection(self.device_database_path())
if use_row_factory:
db_connection.setrowtrace(self.row_factory)
return db_connection
def row_factory(self, cursor, row):
return {k[0]: row[i] for i, k in enumerate(cursor.getdescription())}
def get_database_version(self, connection):
cursor = connection.cursor()
cursor.execute('SELECT version FROM dbversion')
try:
result = next(cursor)
dbversion = result['version']
except StopIteration:
dbversion = 0
return dbversion
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 = (0,0,0)
return fwversion
def sanitize_path_components(self, components):
invalid_filename_chars_re = re.compile(r'[\/\\\?%\*:;\|\"\'><\$!]', re.IGNORECASE | re.UNICODE)
return [invalid_filename_chars_re.sub('_', x) for x in components]
def books(self, oncard=None, end_session=True):
from calibre.ebooks.metadata.meta import path_to_ext
dummy_bl = BookList(None, None, None)
if oncard == 'carda' and not self._card_a_prefix:
self.report_progress(1.0, _('Getting list of books on device...'))
return dummy_bl
elif oncard == 'cardb' and not self._card_b_prefix:
self.report_progress(1.0, _('Getting list of books on device...'))
return dummy_bl
elif oncard and oncard != 'carda' and oncard != 'cardb':
self.report_progress(1.0, _('Getting list of books on device...'))
return dummy_bl
prefix = self._card_a_prefix if oncard == 'carda' else \
self._card_b_prefix if oncard == 'cardb' \
else self._main_prefix
self.fwversion = self.get_firmware_version()
if not (self.fwversion == (1,0) or self.fwversion == (1,4)):
self.has_kepubs = True
debug_print('Version of driver: ', self.version, 'Has kepubs:', self.has_kepubs)
debug_print('Version of firmware: ', self.fwversion, 'Has kepubs:', self.has_kepubs)
self.booklist_class.rebuild_collections = self.rebuild_collections
# get the metadata cache
bl = self.booklist_class(oncard, prefix, self.settings)
need_sync = self.parse_metadata_cache(bl, prefix, self.METADATA_CACHE)
# make a dict cache of paths so the lookup in the loop below is faster.
bl_cache = {}
for idx,b in enumerate(bl):
bl_cache[b.lpath] = idx
def update_booklist(prefix, path, title, authors, mime, date, ContentType, ImageID, readstatus, MimeType, expired, favouritesindex, accessibility):
changed = False
try:
lpath = path.partition(self.normalize_path(prefix))[2]
if lpath.startswith(os.sep):
lpath = lpath[len(os.sep):]
lpath = lpath.replace('\\', '/')
# debug_print("LPATH: ", lpath, " - Title: " , title)
playlist_map = {}
if lpath not in playlist_map:
playlist_map[lpath] = []
if readstatus == 1:
playlist_map[lpath].append('Im_Reading')
elif readstatus == 2:
playlist_map[lpath].append('Read')
elif readstatus == 3:
playlist_map[lpath].append('Closed')
# Related to a bug in the Kobo firmware that leaves an expired row for deleted books
# this shows an expired Collection so the user can decide to delete the book
if expired == 3:
playlist_map[lpath].append('Expired')
# A SHORTLIST is supported on the touch but the data field is there on most earlier models
if favouritesindex == 1:
playlist_map[lpath].append('Shortlist')
# Label Previews
if accessibility == 6:
playlist_map[lpath].append('Preview')
elif accessibility == 4:
playlist_map[lpath].append('Recommendation')
path = self.normalize_path(path)
# print "Normalized FileName: " + path
idx = bl_cache.get(lpath, None)
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')
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')
# print "Image name Normalized: " + imagename
if not os.path.exists(imagename):
debug_print("Strange - The image name does not exist - title: ", title)
if imagename is not None:
bl[idx].thumbnail = ImageWrapper(imagename)
if (ContentType != '6' and MimeType != 'Shortcover'):
if os.path.exists(self.normalize_path(os.path.join(prefix, lpath))):
if self.update_metadata_item(bl[idx]):
# print 'update_metadata_item returned true'
changed = True
else:
debug_print(" Strange: The file: ", prefix, lpath, " does not exist!")
if lpath in playlist_map and \
playlist_map[lpath] not in bl[idx].device_collections:
bl[idx].device_collections = playlist_map.get(lpath,[])
else:
if ContentType == '6' and MimeType == 'Shortcover':
book = self.book_class(prefix, lpath, title, authors, mime, date, ContentType, ImageID, size=1048576)
else:
try:
if os.path.exists(self.normalize_path(os.path.join(prefix, lpath))):
book = self.book_from_path(prefix, lpath, title, authors, mime, date, ContentType, ImageID)
else:
debug_print(" Strange: The file: ", prefix, lpath, " does not exist!")
title = "FILE MISSING: " + title
book = self.book_class(prefix, lpath, title, authors, mime, date, ContentType, ImageID, size=1048576)
except:
debug_print("prefix: ", prefix, "lpath: ", lpath, "title: ", title, "authors: ", authors,
"mime: ", mime, "date: ", date, "ContentType: ", ContentType, "ImageID: ", ImageID)
raise
# print 'Update booklist'
book.device_collections = playlist_map.get(lpath,[]) # if lpath in playlist_map else []
if bl.add_book(book, replace_metadata=False):
changed = True
except: # Probably a path encoding error
import traceback
traceback.print_exc()
return changed
with closing(self.device_database_connection(use_row_factory=True)) as connection:
self.dbversion = self.get_database_version(connection)
debug_print("Database Version: ", self.dbversion)
cursor = connection.cursor()
opts = self.settings()
if self.dbversion >= 33:
query= ('select Title, Attribution, DateCreated, ContentID, MimeType, ContentType, '
'ImageID, ReadStatus, ___ExpirationStatus, FavouritesIndex, Accessibility, IsDownloaded from content where '
'BookID is Null %(previews)s %(recommendations)s and not ((___ExpirationStatus=3 or ___ExpirationStatus is Null) %(expiry)s') % dict(
expiry=' and ContentType = 6)' if opts.extra_customization[self.OPT_SHOW_EXPIRED_BOOK_RECORDS] else ')',
previews=' and Accessibility <> 6' if not self.show_previews else '',
recommendations=' and IsDownloaded in (\'true\', 1)' if opts.extra_customization[self.OPT_SHOW_RECOMMENDATIONS] is False else '')
elif self.dbversion >= 16 and self.dbversion < 33:
query= ('select Title, Attribution, DateCreated, ContentID, MimeType, ContentType, '
'ImageID, ReadStatus, ___ExpirationStatus, FavouritesIndex, Accessibility, "1" as IsDownloaded from content where '
'BookID is Null and not ((___ExpirationStatus=3 or ___ExpirationStatus is Null) %(expiry)s') % dict(expiry=' and ContentType = 6)'
if opts.extra_customization[self.OPT_SHOW_EXPIRED_BOOK_RECORDS] else ')')
elif self.dbversion < 16 and self.dbversion >= 14:
query= ('select Title, Attribution, DateCreated, ContentID, MimeType, ContentType, '
'ImageID, ReadStatus, ___ExpirationStatus, FavouritesIndex, "-1" as Accessibility, "1" as IsDownloaded from content where '
'BookID is Null and not ((___ExpirationStatus=3 or ___ExpirationStatus is Null) %(expiry)s') % dict(expiry=' and ContentType = 6)'
if opts.extra_customization[self.OPT_SHOW_EXPIRED_BOOK_RECORDS] else ')')
elif self.dbversion < 14 and self.dbversion >= 8:
query= ('select Title, Attribution, DateCreated, ContentID, MimeType, ContentType, '
'ImageID, ReadStatus, ___ExpirationStatus, "-1" as FavouritesIndex, "-1" as Accessibility, "1" as IsDownloaded from content where '
'BookID is Null and not ((___ExpirationStatus=3 or ___ExpirationStatus is Null) %(expiry)s') % dict(expiry=' and ContentType = 6)'
if opts.extra_customization[self.OPT_SHOW_EXPIRED_BOOK_RECORDS] else ')')
else:
query = ('select Title, Attribution, DateCreated, ContentID, MimeType, ContentType, '
'ImageID, ReadStatus, "-1" as ___ExpirationStatus, "-1" as FavouritesIndex, '
'"-1" as Accessibility, "1" as IsDownloaded from content where BookID is Null')
try:
cursor.execute(query)
except Exception as e:
err = str(e)
if not (any_in(err, '___ExpirationStatus', 'FavouritesIndex', 'Accessibility', 'IsDownloaded')):
raise
query= ('select Title, Attribution, DateCreated, ContentID, MimeType, ContentType, '
'ImageID, ReadStatus, "-1" as ___ExpirationStatus, "-1" as '
'FavouritesIndex, "-1" as Accessibility from content where '
'BookID is Null')
cursor.execute(query)
changed = False
for row in cursor:
# self.report_progress((i+1) / float(numrows), _('Getting list of books on device...'))
if not hasattr(row['ContentID'], 'startswith') or row['ContentID'].startswith("file:///usr/local/Kobo/help/"):
# These are internal to the Kobo device and do not exist
continue
path = self.path_from_contentid(row['ContentID'], row['ContentType'], row['MimeType'], oncard)
mime = mime_type_ext(path_to_ext(path)) if path.find('kepub') == -1 else 'application/epub+zip'
# debug_print("mime:", mime)
if oncard != 'carda' and oncard != 'cardb' and not row['ContentID'].startswith("file:///mnt/sd/"):
prefix = self._main_prefix
elif oncard == 'carda' and row['ContentID'].startswith("file:///mnt/sd/"):
prefix = self._card_a_prefix
changed = update_booklist(self._main_prefix, path,
row['Title'], row['Attribution'], mime, row['DateCreated'], row['ContentType'],
row['ImageId'], row['ReadStatus'], row['MimeType'], row['___ExpirationStatus'],
row['FavouritesIndex'], row['Accessibility']
)
if changed:
need_sync = True
cursor.close()
# Remove books that are no longer in the filesystem. Cache contains
# indices into the booklist if book not in filesystem, None otherwise
# Do the operation in reverse order so indices remain valid
for idx in sorted(itervalues(bl_cache), reverse=True, key=lambda x: x or -1):
if idx is not None:
need_sync = True
del bl[idx]
# print "count found in cache: %d, count of files in metadata: %d, need_sync: %s" % \
# (len(bl_cache), len(bl), need_sync)
if need_sync: # self.count_found_in_bl != len(bl) or need_sync:
if oncard == 'cardb':
self.sync_booklists((None, None, bl))
elif oncard == 'carda':
self.sync_booklists((None, bl, None))
else:
self.sync_booklists((bl, None, None))
self.report_progress(1.0, _('Getting list of books on device...'))
return bl
def filename_callback(self, path, mi):
# debug_print("Kobo:filename_callback:Path - {0}".format(path))
idx = path.rfind('.')
ext = path[idx:]
if ext == KEPUB_EXT:
path = path + EPUB_EXT
# debug_print("Kobo:filename_callback:New path - {0}".format(path))
return path
def delete_via_sql(self, ContentID, ContentType):
# Delete Order:
# 1) shortcover_page
# 2) volume_shorcover
# 2) content
debug_print('delete_via_sql: ContentID: ', ContentID, 'ContentType: ', ContentType)
with closing(self.device_database_connection()) as connection:
cursor = connection.cursor()
t = (ContentID,)
cursor.execute('select ImageID from content where ContentID = ?', t)
ImageID = None
for row in cursor:
# First get the ImageID to delete the images
ImageID = row[0]
cursor.close()
cursor = connection.cursor()
if ContentType == 6 and self.dbversion < 8:
# Delete the shortcover_pages first
cursor.execute('delete from shortcover_page where shortcoverid in (select ContentID from content where BookID = ?)', t)
# Delete the volume_shortcovers second
cursor.execute('delete from volume_shortcovers where volumeid = ?', t)
# Delete the rows from content_keys
if self.dbversion >= 8:
cursor.execute('delete from content_keys where volumeid = ?', t)
# Delete the chapters associated with the book next
t = (ContentID,)
# Kobo does not delete the Book row (ie the row where the BookID is Null)
# The next server sync should remove the row
cursor.execute('delete from content where BookID = ?', t)
if ContentType == 6:
try:
cursor.execute('update content set ReadStatus=0, FirstTimeReading = \'true\', ___PercentRead=0, ___ExpirationStatus=3 '
'where BookID is Null and ContentID =?',t)
except Exception as e:
if 'no such column' not in str(e):
raise
try:
cursor.execute('update content set ReadStatus=0, FirstTimeReading = \'true\', ___PercentRead=0 '
'where BookID is Null and ContentID =?',t)
except Exception as e:
if 'no such column' not in str(e):
raise
cursor.execute('update content set ReadStatus=0, FirstTimeReading = \'true\' '
'where BookID is Null and ContentID =?',t)
else:
cursor.execute('delete from content where BookID is Null and ContentID =?',t)
cursor.close()
if ImageID is None:
print("Error condition ImageID was not found")
print("You likely tried to delete a book that the kobo has not yet added to the database")
# If all this succeeds we need to delete the images files via the ImageID
return ImageID
def delete_images(self, ImageID, book_path):
if ImageID is not None:
path_prefix = '.kobo/images/'
path = self._main_prefix + path_prefix + ImageID
file_endings = (' - iPhoneThumbnail.parsed', ' - bbMediumGridList.parsed', ' - NickelBookCover.parsed', ' - N3_LIBRARY_FULL.parsed',
' - N3_LIBRARY_GRID.parsed', ' - N3_LIBRARY_LIST.parsed', ' - N3_SOCIAL_CURRENTREAD.parsed', ' - N3_FULL.parsed',)
for ending in file_endings:
fpath = path + ending
fpath = self.normalize_path(fpath)
if os.path.exists(fpath):
# print 'Image File Exists: ' + fpath
os.unlink(fpath)
def delete_books(self, paths, end_session=True):
if self.modify_database_check("delete_books") is False:
return
for i, path in enumerate(paths):
self.report_progress((i+1) / float(len(paths)), _('Removing books from device...'))
path = self.normalize_path(path)
# print "Delete file normalized path: " + path
extension = os.path.splitext(path)[1]
ContentType = self.get_content_type_from_extension(extension) if extension else self.get_content_type_from_path(path)
ContentID = self.contentid_from_path(path, ContentType)
ImageID = self.delete_via_sql(ContentID, ContentType)
# print " We would now delete the Images for" + ImageID
self.delete_images(ImageID, path)
if os.path.exists(path):
# Delete the ebook
# print "Delete the ebook: " + path
os.unlink(path)
filepath = os.path.splitext(path)[0]
for ext in self.DELETE_EXTS:
if os.path.exists(filepath + ext):
# print "Filename: " + filename
os.unlink(filepath + ext)
if os.path.exists(path + ext):
# print "Filename: " + filename
os.unlink(path + ext)
if self.SUPPORTS_SUB_DIRS:
try:
# print "removed"
os.removedirs(os.path.dirname(path))
except Exception:
pass
self.report_progress(1.0, _('Removing books from device...'))
def remove_books_from_metadata(self, paths, booklists):
if self.modify_database_check("remove_books_from_metatata") is False:
return
for i, path in enumerate(paths):
self.report_progress((i+1) / float(len(paths)), _('Removing books from device metadata listing...'))
for bl in booklists:
for book in bl:
# print "Book Path: " + book.path
if path.endswith(book.path):
# print " Remove: " + book.path
bl.remove_book(book)
self.report_progress(1.0, _('Removing books from device metadata listing...'))
def add_books_to_metadata(self, locations, metadata, booklists):
debug_print("KoboTouch::add_books_to_metadata - start. metadata=%s" % metadata[0])
metadata = iter(metadata)
for i, location in enumerate(locations):
self.report_progress((i+1) / float(len(locations)), _('Adding books to device metadata listing...'))
info = next(metadata)
debug_print("KoboTouch::add_books_to_metadata - info=%s" % info)
blist = 2 if location[1] == 'cardb' else 1 if location[1] == 'carda' else 0
# Extract the correct prefix from the pathname. To do this correctly,
# we must ensure that both the prefix and the path are normalized
# so that the comparison will work. Book's __init__ will fix up
# lpath, so we don't need to worry about that here.
path = self.normalize_path(location[0])
if self._main_prefix:
prefix = self._main_prefix if \
path.startswith(self.normalize_path(self._main_prefix)) else None
if not prefix and self._card_a_prefix:
prefix = self._card_a_prefix if \
path.startswith(self.normalize_path(self._card_a_prefix)) else None
if not prefix and self._card_b_prefix:
prefix = self._card_b_prefix if \
path.startswith(self.normalize_path(self._card_b_prefix)) else None
if prefix is None:
prints('in add_books_to_metadata. Prefix is None!', path,
self._main_prefix)
continue
# print "Add book to metadata: "
# print "prefix: " + prefix
lpath = path.partition(prefix)[2]
if lpath.startswith('/') or lpath.startswith('\\'):
lpath = lpath[1:]
# print "path: " + lpath
book = self.book_class(prefix, lpath, info.title, other=info)
if book.size is None or book.size == 0:
book.size = os.stat(self.normalize_path(path)).st_size
b = booklists[blist].add_book(book, replace_metadata=True)
if b:
b._new_book = True
self.report_progress(1.0, _('Adding books to device metadata listing...'))
def contentid_from_path(self, path, ContentType):
if ContentType == 6:
extension = os.path.splitext(path)[1]
if extension == '.kobo':
ContentID = os.path.splitext(path)[0]
# Remove the prefix on the file. it could be either
ContentID = ContentID.replace(self._main_prefix, '')
else:
ContentID = path
ContentID = ContentID.replace(self._main_prefix + self.normalize_path('.kobo/kepub/'), '')
if self._card_a_prefix is not None:
ContentID = ContentID.replace(self._card_a_prefix, '')
elif ContentType == 999: # HTML Files
ContentID = path
ContentID = ContentID.replace(self._main_prefix, "/mnt/onboard/")
if self._card_a_prefix is not None:
ContentID = ContentID.replace(self._card_a_prefix, "/mnt/sd/")
else: # ContentType = 16
ContentID = path
ContentID = ContentID.replace(self._main_prefix, "file:///mnt/onboard/")
if self._card_a_prefix is not None:
ContentID = ContentID.replace(self._card_a_prefix, "file:///mnt/sd/")
ContentID = ContentID.replace("\\", '/')
return ContentID
def get_content_type_from_path(self, path):
# Strictly speaking the ContentType could be 6 or 10
# however newspapers have the same storage format
ContentType = 901
if path.find('kepub') >= 0:
ContentType = 6
return ContentType
def get_content_type_from_extension(self, extension):
if extension == '.kobo':
# Kobo books do not have book files. They do have some images though
# print "kobo book"
ContentType = 6
elif extension == '.pdf' or extension == '.epub':
# print "ePub or pdf"
ContentType = 16
elif extension == '.rtf' or extension == '.txt' or extension == '.htm' or extension == '.html':
# print "txt"
if self.fwversion == (1,0) or self.fwversion == (1,4) or self.fwversion == (1,7,4):
ContentType = 999
else:
ContentType = 901
else: # if extension == '.html' or extension == '.txt':
ContentType = 901 # Yet another hack: to get around Kobo changing how ContentID is stored
return ContentType
def path_from_contentid(self, ContentID, ContentType, MimeType, oncard):
path = ContentID
if oncard == 'cardb':
print('path from_contentid cardb')
elif oncard == 'carda':
path = path.replace("file:///mnt/sd/", self._card_a_prefix)
# print "SD Card: " + path
else:
if ContentType == "6" and MimeType == 'Shortcover':
# This is a hack as the kobo files do not exist
# but the path is required to make a unique id
# for calibre's reference
path = self._main_prefix + path + '.kobo'
# print "Path: " + path
elif (ContentType == "6" or ContentType == "10") and MimeType == 'application/x-kobo-epub+zip':
if path.startswith("file:///mnt/onboard/"):
path = self._main_prefix + path.replace("file:///mnt/onboard/", '')
else:
path = self._main_prefix + '.kobo/kepub/' + path
# print "Internal: " + path
else:
# if path.startswith("file:///mnt/onboard/"):
path = path.replace("file:///mnt/onboard/", self._main_prefix)
path = path.replace("/mnt/onboard/", self._main_prefix)
# print "Internal: " + path
return path
def modify_database_check(self, function):
# Checks to see whether the database version is supported
# and whether the user has chosen to support the firmware version
if self.dbversion > self.supported_dbversion:
# Unsupported database
opts = self.settings()
if not opts.extra_customization[self.OPT_SUPPORT_NEWER_FIRMWARE]:
debug_print('The database has been upgraded past supported version')
self.report_progress(1.0, _('Removing books from device...'))
from calibre.devices.errors import UserFeedback
raise UserFeedback(_("Kobo database version unsupported - See details"),
_('Your Kobo is running an updated firmware/database version.'
' As calibre does not know about this updated firmware,'
' database editing is disabled, to prevent corruption.'
' You can still send books to your Kobo with calibre, '
' but deleting books and managing collections is disabled.'
' If you are willing to experiment and know how to reset'
' your Kobo to Factory defaults, you can override this'
' check by right clicking the device icon in calibre and'
' selecting "Configure this device" and then the '
' "Attempt to support newer firmware" option.'
' Doing so may require you to perform a Factory reset of'
' your Kobo.') + ((
'\nDevice database version: %s.'
'\nDevice firmware version: %s') % (self.dbversion, self.fwversion))
, UserFeedback.WARN)
return False
else:
# The user chose to edit the database anyway
return True
else:
# Supported database version
return True
def get_file(self, path, *args, **kwargs):
tpath = self.munge_path(path)
extension = os.path.splitext(tpath)[1]
if extension == '.kobo':
from calibre.devices.errors import UserFeedback
raise UserFeedback(_("Not Implemented"),
_('".kobo" files do not exist on the device as books; '
'instead they are rows in the sqlite database. '
'Currently they cannot be exported or viewed.'),
UserFeedback.WARN)
return USBMS.get_file(self, path, *args, **kwargs)
@classmethod
def book_from_path(cls, prefix, lpath, title, authors, mime, date, ContentType, ImageID):
# debug_print("KOBO:book_from_path - title=%s"%title)
from calibre.ebooks.metadata import MetaInformation
if cls.read_metadata or cls.MUST_READ_METADATA:
mi = cls.metadata_from_path(cls.normalize_path(os.path.join(prefix, lpath)))
else:
from calibre.ebooks.metadata.meta import metadata_from_filename
mi = metadata_from_filename(cls.normalize_path(os.path.basename(lpath)),
cls.build_template_regexp())
if mi is None:
mi = MetaInformation(os.path.splitext(os.path.basename(lpath))[0],
[_('Unknown')])
size = os.stat(cls.normalize_path(os.path.join(prefix, lpath))).st_size
book = cls.book_class(prefix, lpath, title, authors, mime, date, ContentType, ImageID, size=size, other=mi)
return book
def get_device_paths(self):
paths = {}
for prefix, path, source_id in [
('main', 'metadata.calibre', 0),
('card_a', 'metadata.calibre', 1),
('card_b', 'metadata.calibre', 2)
]:
prefix = getattr(self, '_%s_prefix'%prefix)
if prefix is not None and os.path.exists(prefix):
paths[source_id] = os.path.join(prefix, *(path.split('/')))
return paths
def reset_readstatus(self, connection, oncard):
cursor = connection.cursor()
# Reset Im_Reading list in the database
if oncard == 'carda':
query= 'update content set ReadStatus=0, FirstTimeReading = \'true\' where BookID is Null and ContentID like \'file:///mnt/sd/%\''
elif oncard != 'carda' and oncard != 'cardb':
query= 'update content set ReadStatus=0, FirstTimeReading = \'true\' where BookID is Null and ContentID not like \'file:///mnt/sd/%\''
try:
cursor.execute(query)
except:
debug_print(' Database Exception: Unable to reset ReadStatus list')
raise
finally:
cursor.close()
def set_readstatus(self, connection, ContentID, ReadStatus):
debug_print("Kobo::set_readstatus - ContentID=%s, ReadStatus=%d" % (ContentID, ReadStatus))
cursor = connection.cursor()
t = (ContentID,)
cursor.execute('select DateLastRead, ReadStatus from Content where BookID is Null and ContentID = ?', t)
try:
result = next(cursor)
datelastread = result['DateLastRead']
current_ReadStatus = result['ReadStatus']
except StopIteration:
datelastread = None
current_ReadStatus = 0
if not ReadStatus == current_ReadStatus:
if ReadStatus == 0:
datelastread = None
else:
datelastread = 'CURRENT_TIMESTAMP' if datelastread is None else datelastread
t = (ReadStatus, datelastread, ContentID,)
try:
debug_print("Kobo::set_readstatus - Making change - ContentID=%s, ReadStatus=%d, DateLastRead=%s" % (ContentID, ReadStatus, datelastread))
cursor.execute('update content set ReadStatus=?,FirstTimeReading=\'false\',DateLastRead=? where BookID is Null and ContentID = ?', t)
except:
debug_print(' Database Exception: Unable to update ReadStatus')
raise
cursor.close()
def reset_favouritesindex(self, connection, oncard):
# Reset FavouritesIndex list in the database
if oncard == 'carda':
query= 'update content set FavouritesIndex=-1 where BookID is Null and ContentID like \'file:///mnt/sd/%\''
elif oncard != 'carda' and oncard != 'cardb':
query= 'update content set FavouritesIndex=-1 where BookID is Null and ContentID not like \'file:///mnt/sd/%\''
cursor = connection.cursor()
try:
cursor.execute(query)
except Exception as e:
debug_print(' Database Exception: Unable to reset Shortlist list')
if 'no such column' not in str(e):
raise
finally:
cursor.close()
def set_favouritesindex(self, connection, ContentID):
cursor = connection.cursor()
t = (ContentID,)
try:
cursor.execute('update content set FavouritesIndex=1 where BookID is Null and ContentID = ?', t)
except Exception as e:
debug_print(' Database Exception: Unable set book as Shortlist')
if 'no such column' not in str(e):
raise
finally:
cursor.close()
def update_device_database_collections(self, booklists, collections_attributes, oncard):
debug_print("Kobo:update_device_database_collections - oncard='%s'"%oncard)
if self.modify_database_check("update_device_database_collections") is False:
return
# Only process categories in this list
supportedcategories = {
"Im_Reading":1,
"Read":2,
"Closed":3,
"Shortlist":4,
# "Preview":99, # Unsupported as we don't want to change it
}
# Define lists for the ReadStatus
readstatuslist = {
"Im_Reading":1,
"Read":2,
"Closed":3,
}
accessibilitylist = {
"Preview":6,
"Recommendation":4,
}
# debug_print('Starting update_device_database_collections', collections_attributes)
# Force collections_attributes to be 'tags' as no other is currently supported
# debug_print('KOBO: overriding the provided collections_attributes:', collections_attributes)
collections_attributes = ['tags']
collections = booklists.get_collections(collections_attributes)
# debug_print('Kobo:update_device_database_collections - Collections:', collections)
# Create a connection to the sqlite database
# Needs to be outside books collection as in the case of removing
# the last book from the collection the list of books is empty
# and the removal of the last book would not occur
with closing(self.device_database_connection()) as connection:
if collections:
# Need to reset the collections outside the particular loops
# otherwise the last item will not be removed
self.reset_readstatus(connection, oncard)
if self.dbversion >= 14:
self.reset_favouritesindex(connection, oncard)
# Process any collections that exist
for category, books in collections.items():
if category in supportedcategories:
# debug_print("Category: ", category, " id = ", readstatuslist.get(category))
for book in books:
# debug_print(' Title:', book.title, 'category: ', category)
if category not in book.device_collections:
book.device_collections.append(category)
extension = os.path.splitext(book.path)[1]
ContentType = self.get_content_type_from_extension(extension) if extension else self.get_content_type_from_path(book.path)
ContentID = self.contentid_from_path(book.path, ContentType)
if category in tuple(readstatuslist):
# Manage ReadStatus
self.set_readstatus(connection, ContentID, readstatuslist.get(category))
elif category == 'Shortlist' and self.dbversion >= 14:
# Manage FavouritesIndex/Shortlist
self.set_favouritesindex(connection, ContentID)
elif category in tuple(accessibilitylist):
# Do not manage the Accessibility List
pass
else: # No collections
# Since no collections exist the ReadStatus needs to be reset to 0 (Unread)
debug_print("No Collections - resetting ReadStatus")
self.reset_readstatus(connection, oncard)
if self.dbversion >= 14:
debug_print("No Collections - resetting FavouritesIndex")
self.reset_favouritesindex(connection, oncard)
# debug_print('Finished update_device_database_collections', collections_attributes)
def get_collections_attributes(self):
collections = [x.lower().strip() for x in self.collections_columns.split(',')]
return collections
@property
def collections_columns(self):
opts = self.settings()
return opts.extra_customization[self.OPT_COLLECTIONS]
@property
def read_metadata(self):
return self.settings().read_metadata
@property
def show_previews(self):
opts = self.settings()
return opts.extra_customization[self.OPT_SHOW_PREVIEWS] is False
def sync_booklists(self, booklists, end_session=True):
debug_print('KOBO:sync_booklists - start')
paths = self.get_device_paths()
# debug_print('KOBO:sync_booklists - booklists:', booklists)
blists = {}
for i in paths:
try:
if booklists[i] is not None:
# debug_print('Booklist: ', i)
blists[i] = booklists[i]
except IndexError:
pass
collections = self.get_collections_attributes()
# debug_print('KOBO: collection fields:', collections)
for i, blist in blists.items():
if i == 0:
oncard = 'main'
else:
oncard = 'carda'
self.update_device_database_collections(blist, collections, oncard)
USBMS.sync_booklists(self, booklists, end_session=end_session)
debug_print('KOBO:sync_booklists - end')
def rebuild_collections(self, booklist, oncard):
collections_attributes = []
self.update_device_database_collections(booklist, collections_attributes, oncard)
def upload_cover(self, path, filename, metadata, filepath):
'''
Upload book cover to the device. Default implementation does nothing.
:param path: The full path to the folder where the associated book is located.
:param filename: The name of the book file without the extension.
:param metadata: metadata belonging to the book. Use metadata.thumbnail
for cover
:param filepath: The full path to the ebook file
'''
opts = self.settings()
if not opts.extra_customization[self.OPT_UPLOAD_COVERS]:
# Building thumbnails disabled
debug_print('KOBO: not uploading cover')
return
if not opts.extra_customization[self.OPT_UPLOAD_GRAYSCALE_COVERS]:
uploadgrayscale = False
else:
uploadgrayscale = True
debug_print('KOBO: uploading cover')
try:
self._upload_cover(path, filename, metadata, filepath, uploadgrayscale)
except:
debug_print('FAILED to upload cover', filepath)
def _upload_cover(self, path, filename, metadata, filepath, uploadgrayscale):
from calibre.utils.img import save_cover_data_to
if metadata.cover:
cover = self.normalize_path(metadata.cover.replace('/', os.sep))
if os.path.exists(cover):
# Get ContentID for Selected Book
extension = os.path.splitext(filepath)[1]
ContentType = self.get_content_type_from_extension(extension) if extension != '' else self.get_content_type_from_path(filepath)
ContentID = self.contentid_from_path(filepath, ContentType)
with closing(self.device_database_connection()) as connection:
cursor = connection.cursor()
t = (ContentID,)
cursor.execute('select ImageId from Content where BookID is Null and ContentID = ?', t)
try:
result = next(cursor)
# debug_print("ImageId: ", result[0])
ImageID = result[0]
except StopIteration:
debug_print("No rows exist in the database - cannot upload")
return
finally:
cursor.close()
if ImageID is not None:
path_prefix = '.kobo/images/'
path = self._main_prefix + path_prefix + ImageID
file_endings = {' - iPhoneThumbnail.parsed':(103,150),
' - bbMediumGridList.parsed':(93,135),
' - NickelBookCover.parsed':(500,725),
' - N3_LIBRARY_FULL.parsed':(355,530),
' - N3_LIBRARY_GRID.parsed':(149,233),
' - N3_LIBRARY_LIST.parsed':(60,90),
' - N3_FULL.parsed':(600,800),
' - N3_SOCIAL_CURRENTREAD.parsed':(120,186)}
for ending, resize in file_endings.items():
fpath = path + ending
fpath = self.normalize_path(fpath.replace('/', os.sep))
if os.path.exists(fpath):
with lopen(cover, 'rb') as f:
data = f.read()
# Return the data resized and grayscaled if
# required
data = save_cover_data_to(data, grayscale=uploadgrayscale, resize_to=resize, minify_to=resize)
with lopen(fpath, 'wb') as f:
f.write(data)
fsync(f)
else:
debug_print("ImageID could not be retrieved from the database")
def prepare_addable_books(self, paths):
'''
The Kobo supports an encrypted epub referred to as a kepub
Unfortunately Kobo decided to put the files on the device
with no file extension. I just hope that decision causes
them as much grief as it does me :-)
This has to make a temporary copy of the book files with a
epub extension to allow calibre's normal processing to
deal with the file appropriately
'''
for idx, path in enumerate(paths):
if path.find('kepub') >= 0:
with closing(lopen(path, 'rb')) as r:
tf = PersistentTemporaryFile(suffix='.epub')
shutil.copyfileobj(r, tf)
# tf.write(r.read())
paths[idx] = tf.name
return paths
@classmethod
def config_widget(self):
# TODO: Cleanup the following
self.current_friendly_name = self.gui_name
from calibre.gui2.device_drivers.tabbed_device_config import TabbedDeviceConfig
return TabbedDeviceConfig(self.settings(), self.FORMATS, self.SUPPORTS_SUB_DIRS,
self.MUST_READ_METADATA, self.SUPPORTS_USE_AUTHOR_SORT,
self.EXTRA_CUSTOMIZATION_MESSAGE, self,
extra_customization_choices=self.EXTRA_CUSTOMIZATION_CHOICES)
def migrate_old_settings(self, old_settings):
OPT_COLLECTIONS = 0
OPT_UPLOAD_COVERS = 1
OPT_UPLOAD_GRAYSCALE_COVERS = 2
OPT_SHOW_EXPIRED_BOOK_RECORDS = 3
OPT_SHOW_PREVIEWS = 4
OPT_SHOW_RECOMMENDATIONS = 5
OPT_SUPPORT_NEWER_FIRMWARE = 6
p = {}
p['format_map'] = old_settings.format_map
p['save_template'] = old_settings.save_template
p['use_subdirs'] = old_settings.use_subdirs
p['read_metadata'] = old_settings.read_metadata
p['use_author_sort'] = old_settings.use_author_sort
p['extra_customization'] = old_settings.extra_customization
p['collections_columns'] = old_settings.extra_customization[OPT_COLLECTIONS]
p['upload_covers'] = old_settings.extra_customization[OPT_UPLOAD_COVERS]
p['upload_grayscale'] = old_settings.extra_customization[OPT_UPLOAD_GRAYSCALE_COVERS]
p['show_expired_books'] = old_settings.extra_customization[OPT_SHOW_EXPIRED_BOOK_RECORDS]
p['show_previews'] = old_settings.extra_customization[OPT_SHOW_PREVIEWS]
p['show_recommendations'] = old_settings.extra_customization[OPT_SHOW_RECOMMENDATIONS]
p['support_newer_firmware'] = old_settings.extra_customization[OPT_SUPPORT_NEWER_FIRMWARE]
return p
def create_annotations_path(self, mdata, device_path=None):
if device_path:
return device_path
return USBMS.create_annotations_path(self, mdata)
def get_annotations(self, path_map):
from calibre.devices.kobo.bookmark import Bookmark
EPUB_FORMATS = ['epub']
epub_formats = set(EPUB_FORMATS)
def get_storage():
storage = []
if self._main_prefix:
storage.append(os.path.join(self._main_prefix, self.EBOOK_DIR_MAIN))
if self._card_a_prefix:
storage.append(os.path.join(self._card_a_prefix, self.EBOOK_DIR_CARD_A))
if self._card_b_prefix:
storage.append(os.path.join(self._card_b_prefix, self.EBOOK_DIR_CARD_B))
return storage
def resolve_bookmark_paths(storage, path_map):
pop_list = []
book_ext = {}
for book_id in path_map:
file_fmts = set()
for fmt in path_map[book_id]['fmts']:
file_fmts.add(fmt)
bookmark_extension = None
if file_fmts.intersection(epub_formats):
book_extension = list(file_fmts.intersection(epub_formats))[0]
bookmark_extension = 'epub'
if bookmark_extension:
for vol in storage:
bkmk_path = path_map[book_id]['path']
bkmk_path = bkmk_path
if os.path.exists(bkmk_path):
path_map[book_id] = bkmk_path
book_ext[book_id] = book_extension
break
else:
pop_list.append(book_id)
else:
pop_list.append(book_id)
# Remove non-existent bookmark templates
for book_id in pop_list:
path_map.pop(book_id)
return path_map, book_ext
storage = get_storage()
path_map, book_ext = resolve_bookmark_paths(storage, path_map)
bookmarked_books = {}
with closing(self.device_database_connection(use_row_factory=True)) as connection:
for book_id in path_map:
extension = os.path.splitext(path_map[book_id])[1]
ContentType = self.get_content_type_from_extension(extension) if extension else self.get_content_type_from_path(path_map[book_id])
ContentID = self.contentid_from_path(path_map[book_id], ContentType)
debug_print("get_annotations - ContentID: ", ContentID, "ContentType: ", ContentType)
bookmark_ext = extension
myBookmark = Bookmark(connection, ContentID, path_map[book_id], book_id, book_ext[book_id], bookmark_ext)
bookmarked_books[book_id] = self.UserAnnotation(type='kobo_bookmark', value=myBookmark)
# This returns as job.result in gui2.ui.annotations_fetched(self,job)
return bookmarked_books
def generate_annotation_html(self, bookmark):
import calendar
from calibre.ebooks.BeautifulSoup import BeautifulSoup
# Returns <div class="user_annotations"> ... </div>
# last_read_location = bookmark.last_read_location
# timestamp = bookmark.timestamp
percent_read = bookmark.percent_read
debug_print("Kobo::generate_annotation_html - last_read: ", bookmark.last_read)
if bookmark.last_read is not None:
try:
last_read = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(calendar.timegm(time.strptime(bookmark.last_read, "%Y-%m-%dT%H:%M:%S"))))
except:
try:
last_read = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(calendar.timegm(time.strptime(bookmark.last_read, "%Y-%m-%dT%H:%M:%S.%f"))))
except:
try:
last_read = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(calendar.timegm(time.strptime(bookmark.last_read, "%Y-%m-%dT%H:%M:%SZ"))))
except:
last_read = time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime())
else:
# self.datetime = time.gmtime()
last_read = time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime())
# debug_print("Percent read: ", percent_read)
ka_soup = BeautifulSoup()
dtc = 0
divTag = ka_soup.new_tag('div')
divTag['class'] = 'user_annotations'
# Add the last-read location
if bookmark.book_format == 'epub':
markup = _("<hr /><b>Book last read:</b> %(time)s<br /><b>Percentage read:</b> %(pr)d%%<hr />") % dict(
time=last_read,
# loc=last_read_location,
pr=percent_read)
else:
markup = _("<hr /><b>Book last read:</b> %(time)s<br /><b>Percentage read:</b> %(pr)d%%<hr />") % dict(
time=last_read,
# loc=last_read_location,
pr=percent_read)
spanTag = BeautifulSoup('<span style="font-weight:normal">' + markup + '</span>').find('span')
divTag.insert(dtc, spanTag)
dtc += 1
divTag.insert(dtc, ka_soup.new_tag('br'))
dtc += 1
if bookmark.user_notes:
user_notes = bookmark.user_notes
annotations = []
# Add the annotations sorted by location
for location in sorted(user_notes):
if user_notes[location]['type'] == 'Bookmark':
annotations.append(
_('<b>Chapter %(chapter)d:</b> %(chapter_title)s<br /><b>%(typ)s</b>'
'<br /><b>Chapter Progress:</b> %(chapter_progress)s%%<br />%(annotation)s<br /><hr />') % dict(
chapter=user_notes[location]['chapter'],
dl=user_notes[location]['displayed_location'],
typ=user_notes[location]['type'],
chapter_title=user_notes[location]['chapter_title'],
chapter_progress=user_notes[location]['chapter_progress'],
annotation=user_notes[location]['annotation'] if user_notes[location]['annotation'] is not None else ""))
elif user_notes[location]['type'] == 'Highlight':
annotations.append(
_('<b>Chapter %(chapter)d:</b> %(chapter_title)s<br /><b>%(typ)s</b><br />'
'<b>Chapter progress:</b> %(chapter_progress)s%%<br /><b>Highlight:</b> %(text)s<br /><hr />') % dict(
chapter=user_notes[location]['chapter'],
dl=user_notes[location]['displayed_location'],
typ=user_notes[location]['type'],
chapter_title=user_notes[location]['chapter_title'],
chapter_progress=user_notes[location]['chapter_progress'],
text=user_notes[location]['text']))
elif user_notes[location]['type'] == 'Annotation':
annotations.append(
_('<b>Chapter %(chapter)d:</b> %(chapter_title)s<br />'
'<b>%(typ)s</b><br /><b>Chapter progress:</b> %(chapter_progress)s%%<br /><b>Highlight:</b> %(text)s<br />'
'<b>Notes:</b> %(annotation)s<br /><hr />') % dict(
chapter=user_notes[location]['chapter'],
dl=user_notes[location]['displayed_location'],
typ=user_notes[location]['type'],
chapter_title=user_notes[location]['chapter_title'],
chapter_progress=user_notes[location]['chapter_progress'],
text=user_notes[location]['text'],
annotation=user_notes[location]['annotation']))
else:
annotations.append(
_('<b>Chapter %(chapter)d:</b> %(chapter_title)s<br />'
'<b>%(typ)s</b><br /><b>Chapter progress:</b> %(chapter_progress)s%%<br /><b>Highlight:</b> %(text)s<br />'
'<b>Notes:</b> %(annotation)s<br /><hr />') % dict(
chapter=user_notes[location]['chapter'],
dl=user_notes[location]['displayed_location'],
typ=user_notes[location]['type'],
chapter_title=user_notes[location]['chapter_title'],
chapter_progress=user_notes[location]['chapter_progress'],
text=user_notes[location]['text'],
annotation=user_notes[location]['annotation']))
for annotation in annotations:
annot = BeautifulSoup('<span>' + annotation + '</span>').find('span')
divTag.insert(dtc, annot)
dtc += 1
ka_soup.insert(0,divTag)
return ka_soup
def add_annotation_to_library(self, db, db_id, annotation):
from calibre.ebooks.BeautifulSoup import prettify
bm = annotation
ignore_tags = {'Catalog', 'Clippings'}
if bm.type == 'kobo_bookmark' and bm.value.last_read:
mi = db.get_metadata(db_id, index_is_id=True)
debug_print("KOBO:add_annotation_to_library - Title: ", mi.title)
user_notes_soup = self.generate_annotation_html(bm.value)
if mi.comments:
a_offset = mi.comments.find('<div class="user_annotations">')
ad_offset = mi.comments.find('<hr class="annotations_divider" />')
if a_offset >= 0:
mi.comments = mi.comments[:a_offset]
if ad_offset >= 0:
mi.comments = mi.comments[:ad_offset]
if set(mi.tags).intersection(ignore_tags):
return
if mi.comments:
hrTag = user_notes_soup.new_tag('hr')
hrTag['class'] = 'annotations_divider'
user_notes_soup.insert(0, hrTag)
mi.comments += prettify(user_notes_soup)
else:
mi.comments = prettify(user_notes_soup)
# Update library comments
db.set_comment(db_id, mi.comments)
# Add bookmark file to db_id
# NOTE: As it is, this copied the book from the device back to the library. That meant it replaced the
# existing file. Taking this out for that reason, but some books have a ANNOT file that could be
# copied.
# db.add_format_with_hooks(db_id, bm.value.bookmark_extension,
# bm.value.path, index_is_id=True)
class KOBOTOUCH(KOBO):
name = 'KoboTouch'
gui_name = 'Kobo eReader'
author = 'David Forrester'
description = _(
'Communicate with the Kobo Touch, Glo, Mini, Aura HD,'
' Aura H2O, Glo HD, Touch 2, Aura ONE, Aura Edition 2,'
' Aura H2O Edition 2, Clara HD, Forma, Libra H2O, Elipsa,'
' Sage and Libra 2 eReaders.'
' Based on the existing Kobo driver by %s.') % KOBO.author
# icon = I('devices/kobotouch.jpg')
supported_dbversion = 166
min_supported_dbversion = 53
min_dbversion_series = 65
min_dbversion_externalid = 65
min_dbversion_archive = 71
min_dbversion_images_on_sdcard = 77
min_dbversion_activity = 77
min_dbversion_keywords = 82
min_dbversion_seriesid = 136
# 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)
# 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)
min_fwversion_images_on_sdcard = (2, 4, 1)
min_fwversion_images_tree = (2, 9, 0) # Cover images stored in tree under .kobo-images
min_aurah2o_fwversion = (3, 7, 0)
min_reviews_fwversion = (3, 12, 0)
min_glohd_fwversion = (3, 14, 0)
min_auraone_fwversion = (3, 20, 7280)
min_fwversion_overdrive = (4, 0, 7523)
min_clarahd_fwversion = (4, 8, 11090)
min_forma_fwversion = (4, 11, 11879)
min_librah20_fwversion = (4, 16, 13337) # "Reviewers" release.
min_fwversion_epub_location = (4, 17, 13651) # ePub reading location without full contentid.
min_fwversion_dropbox = (4, 18, 13737) # The Forma only at this point.
min_fwversion_serieslist = (4, 20, 14601) # Series list needs the SeriesID to be set.
min_nia_fwversion = (4, 22, 15202)
min_elipsa_fwversion = (4, 28, 17820)
min_libra2_fwversion = (4, 29, 18730)
min_sage_fwversion = (4, 29, 18730)
min_fwversion_audiobooks = (4, 29, 18730)
has_kepubs = True
booklist_class = KTCollectionsBookList
book_class = Book
kobo_series_dict = {}
MAX_PATH_LEN = 185 # 250 - (len(" - N3_LIBRARY_SHELF.parsed") + len("F:\.kobo\images\"))
KOBO_EXTRA_CSSFILE = 'kobo_extra.css'
EXTRA_CUSTOMIZATION_MESSAGE = []
EXTRA_CUSTOMIZATION_DEFAULT = []
OSX_MAIN_MEM_VOL_PAT = re.compile(r'/KOBOeReader')
opts = None
TIMESTAMP_STRING = "%Y-%m-%dT%H:%M:%SZ"
AURA_PRODUCT_ID = [0x4203]
AURA_EDITION2_PRODUCT_ID = [0x4226]
AURA_HD_PRODUCT_ID = [0x4193]
AURA_H2O_PRODUCT_ID = [0x4213]
AURA_H2O_EDITION2_PRODUCT_ID = [0x4227]
AURA_ONE_PRODUCT_ID = [0x4225]
CLARA_HD_PRODUCT_ID = [0x4228]
ELIPSA_PRODUCT_ID = [0x4233]
FORMA_PRODUCT_ID = [0x4229]
GLO_PRODUCT_ID = [0x4173]
GLO_HD_PRODUCT_ID = [0x4223]
LIBRA_H2O_PRODUCT_ID = [0x4232]
LIBRA2_PRODUCT_ID = [0x4234]
MINI_PRODUCT_ID = [0x4183]
NIA_PRODUCT_ID = [0x4230]
SAGE_PRODUCT_ID = [0x4231]
TOUCH_PRODUCT_ID = [0x4163]
TOUCH2_PRODUCT_ID = [0x4224]
PRODUCT_ID = AURA_PRODUCT_ID + AURA_EDITION2_PRODUCT_ID + \
AURA_HD_PRODUCT_ID + AURA_H2O_PRODUCT_ID + AURA_H2O_EDITION2_PRODUCT_ID + \
GLO_PRODUCT_ID + GLO_HD_PRODUCT_ID + \
MINI_PRODUCT_ID + TOUCH_PRODUCT_ID + TOUCH2_PRODUCT_ID + \
AURA_ONE_PRODUCT_ID + CLARA_HD_PRODUCT_ID + FORMA_PRODUCT_ID + LIBRA_H2O_PRODUCT_ID + \
NIA_PRODUCT_ID + ELIPSA_PRODUCT_ID + \
SAGE_PRODUCT_ID + LIBRA2_PRODUCT_ID
BCD = [0x0110, 0x0326, 0x401, 0x409]
KOBO_AUDIOBOOKS_MIMETYPES = ['application/octet-stream', 'application/x-kobo-mp3z']
# Image file name endings. Made up of: image size, min_dbversion, max_dbversion, isFullSize,
# Note: "200" has been used just as a much larger number than the current versions. It is just a lazy
# way of making it open ended.
# NOTE: Values pulled from Nickel by @geek1011,
# c.f., this handy recap: https://github.com/shermp/Kobo-UNCaGED/issues/16#issuecomment-494229994
# Only the N3_FULL values differ, as they should match the screen's effective resolution.
# Note that all Kobo devices share a common AR at roughly 0.75,
# so results should be similar, no matter the exact device.
# Common to all Kobo models
COMMON_COVER_FILE_ENDINGS = {
# Used for Details screen before FW2.8.1, then for current book tile on home screen
' - N3_LIBRARY_FULL.parsed': [(355,530),0, 200,False,],
# Used for library lists
' - N3_LIBRARY_GRID.parsed': [(149,223),0, 200,False,],
# Used for library lists
' - N3_LIBRARY_LIST.parsed': [(60,90),0, 53,False,],
# Used for Details screen from FW2.8.1
' - AndroidBookLoadTablet_Aspect.parsed': [(355,530), 82, 100,False,],
}
# Legacy 6" devices
LEGACY_COVER_FILE_ENDINGS = {
# Used for screensaver, home screen
' - N3_FULL.parsed': [(600,800),0, 200,True,],
}
# Glo
GLO_COVER_FILE_ENDINGS = {
# Used for screensaver, home screen
' - N3_FULL.parsed': [(758,1024),0, 200,True,],
}
# Aura
AURA_COVER_FILE_ENDINGS = {
# Used for screensaver, home screen
# NOTE: The Aura's bezel covers 10 pixels at the bottom.
# Kobo officially advertised the screen resolution with those chopped off.
' - N3_FULL.parsed': [(758,1014),0, 200,True,],
}
# Glo HD and Clara HD share resolution, so the image sizes should be the same.
GLO_HD_COVER_FILE_ENDINGS = {
# Used for screensaver, home screen
' - N3_FULL.parsed': [(1072,1448), 0, 200,True,],
}
AURA_HD_COVER_FILE_ENDINGS = {
# Used for screensaver, home screen
' - N3_FULL.parsed': [(1080,1440), 0, 200,True,],
}
AURA_H2O_COVER_FILE_ENDINGS = {
# Used for screensaver, home screen
# NOTE: The H2O's bezel covers 11 pixels at the top.
# Unlike on the Aura, Nickel fails to account for this when generating covers.
# c.f., https://github.com/shermp/Kobo-UNCaGED/pull/17#discussion_r286209827
' - N3_FULL.parsed': [(1080,1429), 0, 200,True,],
}
# Aura ONE and Elipsa have the same resolution.
AURA_ONE_COVER_FILE_ENDINGS = {
# Used for screensaver, home screen
' - N3_FULL.parsed': [(1404,1872), 0, 200,True,],
}
FORMA_COVER_FILE_ENDINGS = {
# Used for screensaver, home screen
# NOTE: Nickel currently fails to honor the real screen resolution when generating covers,
# choosing instead to follow the Aura One codepath.
' - N3_FULL.parsed': [(1440,1920), 0, 200,True,],
}
LIBRA_H2O_COVER_FILE_ENDINGS = {
# Used for screensaver, home screen
' - N3_FULL.parsed': [(1264,1680), 0, 200,True,],
}
# Following are the sizes used with pre2.1.4 firmware
# COVER_FILE_ENDINGS = {
# ' - N3_LIBRARY_FULL.parsed':[(355,530),0, 99,], # Used for Details screen
# ' - N3_LIBRARY_FULL.parsed':[(600,800),0, 99,],
# ' - N3_LIBRARY_GRID.parsed':[(149,223),0, 99,], # Used for library lists
# ' - N3_LIBRARY_LIST.parsed':[(60,90),0, 53,],
# ' - N3_LIBRARY_SHELF.parsed': [(40,60),0, 52,],
# ' - N3_FULL.parsed':[(600,800),0, 99,], # Used for screensaver if "Full screen" is checked.
# }
def __init__(self, *args, **kwargs):
KOBO.__init__(self, *args, **kwargs)
self.plugboards = self.plugboard_func = None
def initialize(self):
super().initialize()
self.bookshelvelist = []
def get_device_information(self, end_session=True):
self.set_device_name()
return super().get_device_information(end_session)
def open_linux(self):
super().open_linux()
self.swap_drives_if_needed()
def open_osx(self):
# Just dump some info to the logs.
super().open_osx()
# Wrap some debugging output in a try/except so that it is unlikely to break things completely.
try:
if DEBUG:
from calibre_extensions.usbobserver import get_mounted_filesystems
mount_map = get_mounted_filesystems()
debug_print('KoboTouch::open_osx - mount_map=', mount_map)
debug_print('KoboTouch::open_osx - self._main_prefix=', self._main_prefix)
debug_print('KoboTouch::open_osx - self._card_a_prefix=', self._card_a_prefix)
debug_print('KoboTouch::open_osx - self._card_b_prefix=', self._card_b_prefix)
except:
pass
self.swap_drives_if_needed()
def swap_drives_if_needed(self):
# Check the drives have been mounted as expected and swap if needed.
if self._card_a_prefix is None:
return
if not self.is_main_drive(self._main_prefix):
temp_prefix = self._main_prefix
self._main_prefix = self._card_a_prefix
self._card_a_prefix = temp_prefix
def windows_sort_drives(self, drives):
return self.sort_drives(drives)
def sort_drives(self, drives):
if len(drives) < 2:
return drives
main = drives.get('main', None)
carda = drives.get('carda', None)
if main and carda and not self.is_main_drive(main):
drives['main'] = carda
drives['carda'] = main
debug_print('KoboTouch::sort_drives - swapped drives - main={}, carda={}'.format(drives['main'], drives['carda']))
return drives
def is_main_drive(self, drive):
debug_print('KoboTouch::is_main_drive - drive={}, path={}'.format(drive, os.path.join(drive, '.kobo')))
return os.path.exists(self.normalize_path(os.path.join(drive, '.kobo')))
def books(self, oncard=None, end_session=True):
debug_print("KoboTouch:books - oncard='%s'"%oncard)
self.debugging_title = self.get_debugging_title()
dummy_bl = self.booklist_class(None, None, None)
if oncard == 'carda' and not self._card_a_prefix:
self.report_progress(1.0, _('Getting list of books on device...'))
debug_print("KoboTouch:books - Asked to process 'carda', but do not have one!")
return dummy_bl
elif oncard == 'cardb' and not self._card_b_prefix:
self.report_progress(1.0, _('Getting list of books on device...'))
debug_print("KoboTouch:books - Asked to process 'cardb', but do not have one!")
return dummy_bl
elif oncard and oncard != 'carda' and oncard != 'cardb':
self.report_progress(1.0, _('Getting list of books on device...'))
debug_print("KoboTouch:books - unknown card")
return dummy_bl
prefix = self._card_a_prefix if oncard == 'carda' else \
self._card_b_prefix if oncard == 'cardb' \
else self._main_prefix
debug_print("KoboTouch:books - oncard='%s', prefix='%s'"%(oncard, prefix))
self.fwversion = self.get_firmware_version()
debug_print('Kobo device: %s' % self.gui_name)
debug_print('Version of driver:', self.version, 'Has kepubs:', self.has_kepubs)
debug_print('Version of firmware:', self.fwversion, 'Has kepubs:', self.has_kepubs)
debug_print('Firmware supports cover image tree:', self.fwversion >= self.min_fwversion_images_tree)
self.booklist_class.rebuild_collections = self.rebuild_collections
# get the metadata cache
bl = self.booklist_class(oncard, prefix, self.settings)
opts = self.settings()
debug_print("KoboTouch:books - opts.extra_customization=", opts.extra_customization)
debug_print("KoboTouch:books - driver options=", self)
debug_print("KoboTouch:books - prefs['manage_device_metadata']=", prefs['manage_device_metadata'])
debugging_title = self.debugging_title
debug_print("KoboTouch:books - set_debugging_title to '%s'" % debugging_title)
bl.set_debugging_title(debugging_title)
debug_print("KoboTouch:books - length bl=%d"%len(bl))
need_sync = self.parse_metadata_cache(bl, prefix, self.METADATA_CACHE)
debug_print("KoboTouch:books - length bl after sync=%d"%len(bl))
# make a dict cache of paths so the lookup in the loop below is faster.
bl_cache = {}
for idx,b in enumerate(bl):
bl_cache[b.lpath] = idx
def update_booklist(prefix, path, ContentID, ContentType, MimeType, ImageID,
title, authors, DateCreated, Description, Publisher,
series, seriesnumber, SeriesID, SeriesNumberFloat,
ISBN, Language, Subtitle,
readstatus, expired, favouritesindex, accessibility, isdownloaded,
userid, bookshelves
):
show_debug = self.is_debugging_title(title)
# show_debug = authors == 'L. Frank Baum'
if show_debug:
debug_print("KoboTouch:update_booklist - title='%s'"%title, "ContentType=%s"%ContentType, "isdownloaded=", isdownloaded)
debug_print(
" prefix=%s, DateCreated=%s, readstatus=%d, MimeType=%s, expired=%d, favouritesindex=%d, accessibility=%d, isdownloaded=%s"%
(prefix, DateCreated, readstatus, MimeType, expired, favouritesindex, accessibility, isdownloaded,))
changed = False
try:
lpath = path.partition(self.normalize_path(prefix))[2]
if lpath.startswith(os.sep):
lpath = lpath[len(os.sep):]
lpath = lpath.replace('\\', '/')
# debug_print("KoboTouch:update_booklist - LPATH: ", lpath, " - Title: " , title)
playlist_map = {}
if lpath not in playlist_map:
playlist_map[lpath] = []
allow_shelves = True
if readstatus == 1:
playlist_map[lpath].append('Im_Reading')
elif readstatus == 2:
playlist_map[lpath].append('Read')
elif readstatus == 3:
playlist_map[lpath].append('Closed')
# Related to a bug in the Kobo firmware that leaves an expired row for deleted books
# this shows an expired Collection so the user can decide to delete the book
if expired == 3:
playlist_map[lpath].append('Expired')
allow_shelves = False
# A SHORTLIST is supported on the touch but the data field is there on most earlier models
if favouritesindex == 1:
playlist_map[lpath].append('Shortlist')
# Audiobooks are identified by their MimeType
if MimeType in self.KOBO_AUDIOBOOKS_MIMETYPES:
playlist_map[lpath].append('Audiobook')
# The following is in flux:
# - FW2.0.0, DBVersion 53,55 accessibility == 1
# - FW2.1.2 beta, DBVersion == 56, accessibility == -1:
# So, the following should be OK
if isdownloaded == 'false':
if self.dbversion < 56 and accessibility <= 1 or self.dbversion >= 56 and accessibility == -1:
playlist_map[lpath].append('Deleted')
allow_shelves = False
if show_debug:
debug_print("KoboTouch:update_booklist - have a deleted book")
elif self.supports_kobo_archive() and (accessibility == 1 or accessibility == 2):
playlist_map[lpath].append('Archived')
allow_shelves = True
# Label Previews and Recommendations
if accessibility == 6:
if userid == '':
playlist_map[lpath].append('Recommendation')
allow_shelves = False
else:
playlist_map[lpath].append('Preview')
allow_shelves = False
elif accessibility == 4: # Pre 2.x.x firmware
playlist_map[lpath].append('Recommendation')
allow_shelves = False
elif accessibility == 8: # From 4.22 but waa probably there earlier.
playlist_map[lpath].append('Kobo Plus')
allow_shelves = True
elif accessibility == 9: # From 4.0 on Aura One
playlist_map[lpath].append('OverDrive')
allow_shelves = True
kobo_collections = playlist_map[lpath][:]
if allow_shelves:
# debug_print('KoboTouch:update_booklist - allowing shelves - title=%s' % title)
if len(bookshelves) > 0:
playlist_map[lpath].extend(bookshelves)
if show_debug:
debug_print('KoboTouch:update_booklist - playlist_map=', playlist_map)
path = self.normalize_path(path)
# print "Normalized FileName: " + path
# Collect the Kobo metadata
authors_list = [a.strip() for a in authors.split("&")] if authors is not None else [_('Unknown')]
kobo_metadata = Metadata(title, authors_list)
kobo_metadata.series = series
kobo_metadata.series_index = seriesnumber
kobo_metadata.comments = Description
kobo_metadata.publisher = Publisher
kobo_metadata.language = Language
kobo_metadata.isbn = ISBN
if DateCreated is not None:
try:
kobo_metadata.pubdate = parse_date(DateCreated, assume_utc=True)
except:
try:
kobo_metadata.pubdate = datetime.strptime(DateCreated, "%Y-%m-%dT%H:%M:%S.%fZ")
except:
debug_print("KoboTouch:update_booklist - Cannot convert date - DateCreated='%s'"%DateCreated)
idx = bl_cache.get(lpath, None)
if idx is not None: # and not (accessibility == 1 and isdownloaded == 'false'):
if show_debug:
self.debug_index = idx
debug_print("KoboTouch:update_booklist - idx=%d"%idx)
debug_print("KoboTouch:update_booklist - lpath=%s"%lpath)
debug_print('KoboTouch:update_booklist - bl[idx].device_collections=', bl[idx].device_collections)
debug_print('KoboTouch:update_booklist - playlist_map=', playlist_map)
debug_print('KoboTouch:update_booklist - bookshelves=', bookshelves)
debug_print('KoboTouch:update_booklist - kobo_collections=', kobo_collections)
debug_print('KoboTouch:update_booklist - series="%s"' % bl[idx].series)
debug_print('KoboTouch:update_booklist - the book=', bl[idx])
debug_print('KoboTouch:update_booklist - the authors=', bl[idx].authors)
debug_print('KoboTouch:update_booklist - application_id=', bl[idx].application_id)
debug_print('KoboTouch:update_booklist - size=', bl[idx].size)
bl_cache[lpath] = None
if ImageID is not None:
imagename = self.imagefilename_from_imageID(prefix, ImageID)
if imagename is not None:
bl[idx].thumbnail = ImageWrapper(imagename)
if (ContentType == '6' and MimeType != 'application/x-kobo-epub+zip'):
if os.path.exists(self.normalize_path(os.path.join(prefix, lpath))):
if self.update_metadata_item(bl[idx]):
# debug_print("KoboTouch:update_booklist - update_metadata_item returned true")
changed = True
else:
debug_print(" Strange: The file: ", prefix, lpath, " does not exist!")
debug_print("KoboTouch:update_booklist - book size=", bl[idx].size)
if show_debug:
debug_print("KoboTouch:update_booklist - ContentID='%s'"%ContentID)
bl[idx].contentID = ContentID
bl[idx].kobo_metadata = kobo_metadata
bl[idx].kobo_series = series
bl[idx].kobo_series_number = seriesnumber
bl[idx].kobo_series_id = SeriesID
bl[idx].kobo_series_number_float = SeriesNumberFloat
bl[idx].kobo_subtitle = Subtitle
bl[idx].can_put_on_shelves = allow_shelves
bl[idx].mime = MimeType
if not bl[idx].is_sideloaded and bl[idx].has_kobo_series and SeriesID is not None:
if show_debug:
debug_print('KoboTouch:update_booklist - Have purchased kepub with series, saving SeriesID=', SeriesID)
self.kobo_series_dict[series] = SeriesID
if lpath in playlist_map:
bl[idx].device_collections = playlist_map.get(lpath,[])
bl[idx].current_shelves = bookshelves
bl[idx].kobo_collections = kobo_collections
if show_debug:
debug_print('KoboTouch:update_booklist - updated bl[idx].device_collections=', bl[idx].device_collections)
debug_print('KoboTouch:update_booklist - playlist_map=', playlist_map, 'changed=', changed)
# debug_print('KoboTouch:update_booklist - book=', bl[idx])
debug_print("KoboTouch:update_booklist - book class=%s"%bl[idx].__class__)
debug_print("KoboTouch:update_booklist - book title=%s"%bl[idx].title)
else:
if show_debug:
debug_print('KoboTouch:update_booklist - idx is none')
try:
if os.path.exists(self.normalize_path(os.path.join(prefix, lpath))):
book = self.book_from_path(prefix, lpath, title, authors, MimeType, DateCreated, ContentType, ImageID)
else:
if isdownloaded == 'true': # A recommendation or preview is OK to not have a file
debug_print(" Strange: The file: ", prefix, lpath, " does not exist!")
title = "FILE MISSING: " + title
book = self.book_class(prefix, lpath, title, authors, MimeType, DateCreated, ContentType, ImageID, size=0)
if show_debug:
debug_print('KoboTouch:update_booklist - book file does not exist. ContentID="%s"'%ContentID)
except Exception as e:
debug_print("KoboTouch:update_booklist - exception creating book: '%s'"%str(e))
debug_print(" prefix: ", prefix, "lpath: ", lpath, "title: ", title, "authors: ", authors,
"MimeType: ", MimeType, "DateCreated: ", DateCreated, "ContentType: ", ContentType, "ImageID: ", ImageID)
raise
if show_debug:
debug_print('KoboTouch:update_booklist - class:', book.__class__)
# debug_print(' resolution:', book.__class__.__mro__)
debug_print(" contentid: '%s'"%book.contentID)
debug_print(" title:'%s'"%book.title)
debug_print(" the book:", book)
debug_print(" author_sort:'%s'"%book.author_sort)
debug_print(" bookshelves:", bookshelves)
debug_print(" kobo_collections:", kobo_collections)
# print 'Update booklist'
book.device_collections = playlist_map.get(lpath,[]) # if lpath in playlist_map else []
book.current_shelves = bookshelves
book.kobo_collections = kobo_collections
book.contentID = ContentID
book.kobo_metadata = kobo_metadata
book.kobo_series = series
book.kobo_series_number = seriesnumber
book.kobo_series_id = SeriesID
book.kobo_series_number_float = SeriesNumberFloat
book.kobo_subtitle = Subtitle
book.can_put_on_shelves = allow_shelves
# debug_print('KoboTouch:update_booklist - title=', title, 'book.device_collections', book.device_collections)
if not book.is_sideloaded and book.has_kobo_series and SeriesID is not None:
if show_debug:
debug_print('KoboTouch:update_booklist - Have purchased kepub with series, saving SeriesID=', SeriesID)
self.kobo_series_dict[series] = SeriesID
if bl.add_book(book, replace_metadata=False):
changed = True
if show_debug:
debug_print(' book.device_collections', book.device_collections)
debug_print(' book.title', book.title)
except: # Probably a path encoding error
import traceback
traceback.print_exc()
return changed
def get_bookshelvesforbook(connection, ContentID):
# debug_print("KoboTouch:get_bookshelvesforbook - " + ContentID)
bookshelves = []
if not self.supports_bookshelves:
return bookshelves
cursor = connection.cursor()
query = "select ShelfName " \
"from ShelfContent " \
"where ContentId = ? " \
"and _IsDeleted = 'false' " \
"and ShelfName is not null" # This should never be null, but it is protection against an error cause by a sync to the Kobo server
values = (ContentID, )
cursor.execute(query, values)
for i, row in enumerate(cursor):
bookshelves.append(row['ShelfName'])
cursor.close()
# debug_print("KoboTouch:get_bookshelvesforbook - count bookshelves=" + str(count_bookshelves))
return bookshelves
self.debug_index = 0
with closing(self.device_database_connection(use_row_factory=True)) as connection:
debug_print("KoboTouch:books - reading device database")
self.dbversion = self.get_database_version(connection)
debug_print("Database Version: ", self.dbversion)
self.bookshelvelist = self.get_bookshelflist(connection)
debug_print("KoboTouch:books - shelf list:", self.bookshelvelist)
columns = 'Title, Attribution, DateCreated, ContentID, MimeType, ContentType, ImageId, ReadStatus, Description, Publisher '
if self.dbversion >= 16:
columns += ', ___ExpirationStatus, FavouritesIndex, Accessibility'
else:
columns += ', -1 as ___ExpirationStatus, -1 as FavouritesIndex, -1 as Accessibility'
if self.dbversion >= 33:
columns += ', Language, IsDownloaded'
else:
columns += ', NULL AS Language, "1" AS IsDownloaded'
if self.dbversion >= 46:
columns += ', ISBN'
else:
columns += ', NULL AS ISBN'
if self.supports_series():
columns += ", Series, SeriesNumber, ___UserID, ExternalId, Subtitle"
else:
columns += ', null as Series, null as SeriesNumber, ___UserID, null as ExternalId, null as Subtitle'
if self.supports_series_list:
columns += ", SeriesID, SeriesNumberFloat"
else:
columns += ', null as SeriesID, null as SeriesNumberFloat'
where_clause = ''
if self.supports_kobo_archive() or self.supports_overdrive():
where_clause = (" WHERE BookID IS NULL "
" AND ((Accessibility = -1 AND IsDownloaded in ('true', 1 )) " # Sideloaded books
" OR (Accessibility IN (%(downloaded_accessibility)s) %(expiry)s) " # Purchased books
" %(previews)s %(recommendations)s ) " # Previews or Recommendations
) % \
dict(
expiry="" if self.show_archived_books else "and IsDownloaded in ('true', 1)",
previews=" OR (Accessibility in (6) AND ___UserID <> '')" if self.show_previews else "",
recommendations=" OR (Accessibility IN (-1, 4, 6) AND ___UserId = '')" if self.show_recommendations else "",
downloaded_accessibility="1,2,8,9" if self.supports_overdrive() else "1,2"
)
elif self.supports_series():
where_clause = (" WHERE BookID IS NULL "
" AND ((Accessibility = -1 AND IsDownloaded IN ('true', 1)) or (Accessibility IN (1,2)) %(previews)s %(recommendations)s )"
" AND NOT ((___ExpirationStatus=3 OR ___ExpirationStatus is Null) %(expiry)s)"
) % \
dict(
expiry=" AND ContentType = 6" if self.show_archived_books else "",
previews=" or (Accessibility IN (6) AND ___UserID <> '')" if self.show_previews else "",
recommendations=" or (Accessibility in (-1, 4, 6) AND ___UserId = '')" if self.show_recommendations else ""
)
elif self.dbversion >= 33:
where_clause = (' WHERE BookID IS NULL %(previews)s %(recommendations)s AND NOT'
' ((___ExpirationStatus=3 or ___ExpirationStatus IS NULL) %(expiry)s)'
) % \
dict(
expiry=' AND ContentType = 6' if self.show_archived_books else '',
previews=' AND Accessibility <> 6' if not self.show_previews else '',
recommendations=' AND IsDownloaded IN (\'true\', 1)' if not self.show_recommendations else ''
)
elif self.dbversion >= 16:
where_clause = (' WHERE BookID IS NULL '
'AND NOT ((___ExpirationStatus=3 OR ___ExpirationStatus IS Null) %(expiry)s)'
) % \
dict(expiry=' and ContentType = 6' if self.show_archived_books else '')
else:
where_clause = ' WHERE BookID IS NULL'
# Note: The card condition should not need the contentId test for the SD
# card. But the ExternalId does not get set for sideloaded kepubs on the
# SD card.
card_condition = ''
if self.has_externalid():
card_condition = " AND (externalId IS NOT NULL AND externalId <> '' OR contentId LIKE 'file:///mnt/sd/%')" if oncard == 'carda' else (
" AND (externalId IS NULL OR externalId = '') AND contentId NOT LIKE 'file:///mnt/sd/%'")
else:
card_condition = " AND contentId LIKE 'file:///mnt/sd/%'" if oncard == 'carda' else " AND contentId NOT LIKE'file:///mnt/sd/%'"
query = 'SELECT ' + columns + ' FROM content ' + where_clause + card_condition
debug_print("KoboTouch:books - query=", query)
cursor = connection.cursor()
try:
cursor.execute(query)
except Exception as e:
err = str(e)
if not (any_in(err, '___ExpirationStatus', 'FavouritesIndex', 'Accessibility', 'IsDownloaded', 'Series', 'ExternalId')):
raise
query= ('SELECT Title, Attribution, DateCreated, ContentID, MimeType, ContentType, '
'ImageId, ReadStatus, -1 AS ___ExpirationStatus, "-1" AS FavouritesIndex, '
'null AS ISBN, NULL AS Language '
'-1 AS Accessibility, 1 AS IsDownloaded, NULL AS Series, NULL AS SeriesNumber, null as Subtitle '
'FROM content '
'WHERE BookID IS NULL'
)
cursor.execute(query)
changed = False
i = 0
for row in cursor:
i += 1
# self.report_progress((i) / float(books_on_device), _('Getting list of books on device...'))
show_debug = self.is_debugging_title(row['Title'])
if show_debug:
debug_print("KoboTouch:books - looping on database - row=%d" % i)
debug_print("KoboTouch:books - title='%s'"%row['Title'], "authors=", row['Attribution'])
debug_print("KoboTouch:books - row=", row)
if not hasattr(row['ContentID'], 'startswith') or row['ContentID'].lower().startswith(
"file:///usr/local/kobo/help/") or row['ContentID'].lower().startswith("/usr/local/kobo/help/"):
# These are internal to the Kobo device and do not exist
continue
externalId = None if row['ExternalId'] and len(row['ExternalId']) == 0 else row['ExternalId']
path = self.path_from_contentid(row['ContentID'], row['ContentType'], row['MimeType'], oncard, externalId)
if show_debug:
debug_print("KoboTouch:books - path='%s'"%path, " ContentID='%s'"%row['ContentID'], " externalId=%s" % externalId)
bookshelves = get_bookshelvesforbook(connection, row['ContentID'])
prefix = self._card_a_prefix if oncard == 'carda' else self._main_prefix
changed = update_booklist(prefix, path, row['ContentID'], row['ContentType'], row['MimeType'], row['ImageId'],
row['Title'], row['Attribution'], row['DateCreated'], row['Description'], row['Publisher'],
row['Series'], row['SeriesNumber'], row['SeriesID'], row['SeriesNumberFloat'],
row['ISBN'], row['Language'], row['Subtitle'],
row['ReadStatus'], row['___ExpirationStatus'],
int(row['FavouritesIndex']), row['Accessibility'], row['IsDownloaded'],
row['___UserID'], bookshelves
)
if changed:
need_sync = True
cursor.close()
if not prefs['manage_device_metadata'] == 'on_connect':
self.dump_bookshelves(connection)
else:
debug_print("KoboTouch:books - automatically managing metadata")
debug_print("KoboTouch:books - self.kobo_series_dict=", self.kobo_series_dict)
# Remove books that are no longer in the filesystem. Cache contains
# indices into the booklist if book not in filesystem, None otherwise
# Do the operation in reverse order so indices remain valid
for idx in sorted(itervalues(bl_cache), reverse=True, key=lambda x: x or -1):
if idx is not None:
if not os.path.exists(self.normalize_path(os.path.join(prefix, bl[idx].lpath))) or not bl[idx].contentID:
need_sync = True
del bl[idx]
else:
debug_print("KoboTouch:books - Book in mtadata.calibre, on file system but not database - bl[idx].title:'%s'"%bl[idx].title)
# print "count found in cache: %d, count of files in metadata: %d, need_sync: %s" % \
# (len(bl_cache), len(bl), need_sync)
# Bypassing the KOBO sync_booklists as that does things we don't need to do
# Also forcing sync to see if this solves issues with updating shelves and matching books.
if need_sync or True: # self.count_found_in_bl != len(bl) or need_sync:
debug_print("KoboTouch:books - about to sync_booklists")
if oncard == 'cardb':
USBMS.sync_booklists(self, (None, None, bl))
elif oncard == 'carda':
USBMS.sync_booklists(self, (None, bl, None))
else:
USBMS.sync_booklists(self, (bl, None, None))
debug_print("KoboTouch:books - have done sync_booklists")
self.report_progress(1.0, _('Getting list of books on device...'))
debug_print("KoboTouch:books - end - oncard='%s'"%oncard)
return bl
@classmethod
def book_from_path(cls, prefix, lpath, title, authors, mime, date, ContentType, ImageID):
debug_print("KoboTouch:book_from_path - title=%s"%title)
book = super().book_from_path(prefix, lpath, title, authors, mime, date, ContentType, ImageID)
# Kobo Audiobooks are directories with files in them.
if mime in cls.KOBO_AUDIOBOOKS_MIMETYPES and book.size == 0:
audiobook_path = cls.normalize_path(os.path.join(prefix, lpath))
# debug_print("KoboTouch:book_from_path - audiobook=", audiobook_path)
for audiofile in os.scandir(audiobook_path):
# debug_print("KoboTouch:book_from_path - audiofile=", audiofile)
if audiofile.is_file():
size = audiofile.stat().st_size
# debug_print("KoboTouch:book_from_path - size=", size)
book.size += size
debug_print("KoboTouch:book_from_path - book.size=", book.size)
return book
def path_from_contentid(self, ContentID, ContentType, MimeType, oncard, externalId=None):
path = ContentID
if not (externalId or MimeType == 'application/octet-stream'):
return super().path_from_contentid(ContentID, ContentType, MimeType, oncard)
if oncard == 'cardb':
print('path from_contentid cardb')
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
elif path.startswith("file:///mnt/onboard/"):
path = self._main_prefix + path.replace("file:///mnt/onboard/", '')
elif path.startswith("file:///mnt/sd/"):
path = self._card_a_prefix + path.replace("file:///mnt/sd/", '')
elif externalId:
path = self._card_a_prefix + 'koboExtStorage/kepub/' + path
else:
path = self._main_prefix + '.kobo/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)
path = path.replace("file:///mnt/sd/", self._card_a_prefix)
path = path.replace("/mnt/onboard/", self._main_prefix)
# print "Internal: " + path
return path
def imagefilename_from_imageID(self, prefix, ImageID):
show_debug = self.is_debugging_title(ImageID)
if len(ImageID) > 0:
path = self.images_path(prefix, ImageID)
for ending in self.cover_file_endings():
fpath = path + ending
if os.path.exists(fpath):
if show_debug:
debug_print("KoboTouch:imagefilename_from_imageID - have cover image fpath=%s" % (fpath))
return fpath
if show_debug:
debug_print("KoboTouch:imagefilename_from_imageID - no cover image found - ImageID=%s" % (ImageID))
return None
def get_extra_css(self):
extra_sheet = None
from css_parser.css import CSSRule
if self.modifying_css():
extra_css_path = os.path.join(self._main_prefix, self.KOBO_EXTRA_CSSFILE)
if os.path.exists(extra_css_path):
from css_parser import parseFile as cssparseFile
try:
extra_sheet = cssparseFile(extra_css_path)
debug_print(f"KoboTouch:get_extra_css: Using extra CSS in {extra_css_path} ({len(extra_sheet.cssRules)} rules)")
if len(extra_sheet.cssRules) ==0:
debug_print("KoboTouch:get_extra_css: Extra CSS file has no valid rules. CSS will not be modified.")
extra_sheet = None
except Exception as e:
debug_print(f"KoboTouch:get_extra_css: Problem parsing extra CSS file {extra_css_path}")
debug_print(f"KoboTouch:get_extra_css: Exception {e}")
# create dictionary of features enabled in kobo extra css
self.extra_css_options = {}
if extra_sheet:
# search extra_css for @page rule
self.extra_css_options['has_atpage'] = len(self.get_extra_css_rules(extra_sheet, CSSRule.PAGE_RULE)) > 0
# search extra_css for style rule(s) containing widows or orphans
self.extra_css_options['has_widows_orphans'] = len(self.get_extra_css_rules_widow_orphan(extra_sheet)) > 0
debug_print('KoboTouch:get_extra_css - CSS options:', self.extra_css_options)
return extra_sheet
def get_extra_css_rules(self, sheet, css_rule):
return [r for r in sheet.cssRules.rulesOfType(css_rule)]
def get_extra_css_rules_widow_orphan(self, sheet):
from css_parser.css import CSSRule
return [r for r in self.get_extra_css_rules(sheet, CSSRule.STYLE_RULE)
if (r.style['widows'] or r.style['orphans'])]
def upload_books(self, files, names, on_card=None, end_session=True,
metadata=None):
debug_print('KoboTouch:upload_books - %d books'%(len(files)))
debug_print('KoboTouch:upload_books - files=', files)
if self.modifying_epub():
self.extra_sheet = self.get_extra_css()
i = 0
for file, n, mi in zip(files, names, metadata):
debug_print("KoboTouch:upload_books: Processing book: {} by {}".format(mi.title, " and ".join(mi.authors)))
debug_print(f"KoboTouch:upload_books: file={file}, name={n}")
self.report_progress(i / float(len(files)), "Processing book: {} by {}".format(mi.title, " and ".join(mi.authors)))
mi.kte_calibre_name = n
self._modify_epub(file, mi)
i += 1
self.report_progress(0, 'Working...')
result = super().upload_books(files, names, on_card, end_session, metadata)
# debug_print('KoboTouch:upload_books - result=', result)
if self.dbversion >= 53:
try:
with closing(self.device_database_connection()) as connection:
cursor = connection.cursor()
cleanup_query = "DELETE FROM content WHERE ContentID = ? AND Accessibility = 1 AND IsDownloaded = 'false'"
for fname, cycle in result:
show_debug = self.is_debugging_title(fname)
contentID = self.contentid_from_path(fname, 6)
if show_debug:
debug_print('KoboTouch:upload_books: fname=', fname)
debug_print('KoboTouch:upload_books: contentID=', contentID)
cleanup_values = (contentID,)
# debug_print('KoboTouch:upload_books: Delete record left if deleted on Touch')
cursor.execute(cleanup_query, cleanup_values)
if self.override_kobo_replace_existing:
self.set_filesize_in_device_database(connection, contentID, fname)
if not self.upload_covers:
imageID = self.imageid_from_contentid(contentID)
self.delete_images(imageID, fname)
cursor.close()
except Exception as e:
debug_print('KoboTouch:upload_books - Exception: %s'%str(e))
return result
def _modify_epub(self, book_file, metadata, container=None):
debug_print(f"KoboTouch:_modify_epub:Processing {metadata.author_sort} - {metadata.title}")
# Currently only modifying CSS, so if no stylesheet, don't do anything
if not self.extra_sheet:
debug_print("KoboTouch:_modify_epub: no CSS file")
return True
container, commit_container = self.create_container(book_file, metadata, container)
if not container:
return False
from calibre.ebooks.oeb.base import OEB_STYLES
is_dirty = False
for cssname, mt in iteritems(container.mime_map):
if mt in OEB_STYLES:
newsheet = container.parsed(cssname)
oldrules = len(newsheet.cssRules)
# future css mods may be epub/kepub specific, so pass file extension arg
fileext = os.path.splitext(book_file)[-1].lower()
debug_print(f"KoboTouch:_modify_epub: Modifying {cssname}")
if self._modify_stylesheet(newsheet, fileext):
debug_print(f"KoboTouch:_modify_epub:CSS rules {oldrules} -> {len(newsheet.cssRules)} ({cssname})")
container.dirty(cssname)
is_dirty = True
if commit_container:
debug_print("KoboTouch:_modify_epub: committing container.")
self.commit_container(container, is_dirty)
return True
def _modify_stylesheet(self, sheet, fileext, is_dirty=False):
from css_parser.css import CSSRule
# if fileext in (EPUB_EXT, KEPUB_EXT):
# if kobo extra css contains a @page rule
# remove any existing @page rules in epub css
if self.extra_css_options.get('has_atpage', False):
page_rules = self.get_extra_css_rules(sheet, CSSRule.PAGE_RULE)
if len(page_rules) > 0:
debug_print("KoboTouch:_modify_stylesheet: Removing existing @page rules")
for rule in page_rules:
rule.style = ''
is_dirty = True
# if kobo extra css contains any widow/orphan style rules
# remove any existing widow/orphan settings in epub css
if self.extra_css_options.get('has_widows_orphans', False):
widow_orphan_rules = self.get_extra_css_rules_widow_orphan(sheet)
if len(widow_orphan_rules) > 0:
debug_print("KoboTouch:_modify_stylesheet: Removing existing widows/orphans attribs")
for rule in widow_orphan_rules:
rule.style.removeProperty('widows')
rule.style.removeProperty('orphans')
is_dirty = True
# append all rules from kobo extra css
debug_print("KoboTouch:_modify_stylesheet: Append all kobo extra css rules")
for extra_rule in self.extra_sheet.cssRules:
sheet.insertRule(extra_rule)
is_dirty = True
return is_dirty
def create_container(self, book_file, metadata, container=None):
# create new container if not received, else pass through
if not container:
commit_container = True
try:
from calibre.ebooks.oeb.polish.container import get_container
debug_print("KoboTouch:create_container: try to create new container")
container = get_container(book_file)
container.css_preprocessor = DummyCSSPreProcessor()
except Exception as e:
debug_print(f"KoboTouch:create_container: exception from get_container {metadata.author_sort} - {metadata.title}")
debug_print(f"KoboTouch:create_container: exception is: {e}")
else:
commit_container = False
debug_print("KoboTouch:create_container: received container")
return container, commit_container
def commit_container(self, container, is_dirty=True):
# commit container if changes have been made
if is_dirty:
debug_print("KoboTouch:commit_container: commit container.")
container.commit()
# Clean-up-AYGO prevents build-up of TEMP exploded epub/kepub files
debug_print("KoboTouch:commit_container: removing container temp files.")
try:
shutil.rmtree(container.root)
except Exception:
pass
def delete_via_sql(self, ContentID, ContentType):
imageId = super().delete_via_sql(ContentID, ContentType)
if self.dbversion >= 53:
debug_print('KoboTouch:delete_via_sql: ContentID="%s"'%ContentID, 'ContentType="%s"'%ContentType)
try:
with closing(self.device_database_connection()) as connection:
debug_print('KoboTouch:delete_via_sql: have database connection')
cursor = connection.cursor()
debug_print('KoboTouch:delete_via_sql: have cursor')
t = (ContentID,)
# Delete the Bookmarks
debug_print('KoboTouch:delete_via_sql: Delete from Bookmark')
cursor.execute('DELETE FROM Bookmark WHERE VolumeID = ?', t)
# Delete from the Bookshelf
debug_print('KoboTouch:delete_via_sql: Delete from the Bookshelf')
cursor.execute('delete from ShelfContent where ContentID = ?', t)
# ContentType 6 is now for all books.
debug_print('KoboTouch:delete_via_sql: BookID is Null')
cursor.execute('delete from content where BookID is Null and ContentID =?',t)
# Remove the content_settings entry
debug_print('KoboTouch:delete_via_sql: delete from content_settings')
cursor.execute('delete from content_settings where ContentID =?',t)
# Remove the ratings entry
debug_print('KoboTouch:delete_via_sql: delete from ratings')
cursor.execute('delete from ratings where ContentID =?',t)
# Remove any entries for the Activity table - removes tile from new home page
if self.has_activity_table():
debug_print('KoboTouch:delete_via_sql: delete from Activity')
cursor.execute('delete from Activity where Id =?', t)
cursor.close()
debug_print('KoboTouch:delete_via_sql: finished SQL')
debug_print('KoboTouch:delete_via_sql: After SQL, no exception')
except Exception as e:
debug_print('KoboTouch:delete_via_sql - Database Exception: %s'%str(e))
debug_print('KoboTouch:delete_via_sql: imageId="%s"'%imageId)
if imageId is None:
imageId = self.imageid_from_contentid(ContentID)
return imageId
def delete_images(self, ImageID, book_path):
debug_print("KoboTouch:delete_images - ImageID=", ImageID)
if ImageID is not None:
path = self.images_path(book_path, ImageID)
debug_print("KoboTouch:delete_images - path=%s" % path)
for ending in self.cover_file_endings().keys():
fpath = path + ending
fpath = self.normalize_path(fpath)
debug_print("KoboTouch:delete_images - fpath=%s" % fpath)
if os.path.exists(fpath):
debug_print("KoboTouch:delete_images - Image File Exists")
os.unlink(fpath)
try:
os.removedirs(os.path.dirname(path))
except Exception:
pass
def contentid_from_path(self, path, ContentType):
show_debug = self.is_debugging_title(path) and True
if show_debug:
debug_print("KoboTouch:contentid_from_path - path='%s'"%path, "ContentType='%s'"%ContentType)
debug_print("KoboTouch:contentid_from_path - self._main_prefix='%s'"%self._main_prefix, "self._card_a_prefix='%s'"%self._card_a_prefix)
if ContentType == 6:
extension = os.path.splitext(path)[1]
if extension == '.kobo':
ContentID = os.path.splitext(path)[0]
# Remove the prefix on the file. it could be either
ContentID = ContentID.replace(self._main_prefix, '')
elif not extension:
ContentID = path
ContentID = ContentID.replace(self._main_prefix + self.normalize_path('.kobo/kepub/'), '')
else:
ContentID = path
ContentID = ContentID.replace(self._main_prefix, "file:///mnt/onboard/")
if show_debug:
debug_print("KoboTouch:contentid_from_path - 1 ContentID='%s'"%ContentID)
if self._card_a_prefix is not None:
ContentID = ContentID.replace(self._card_a_prefix, "file:///mnt/sd/")
else: # ContentType = 16
debug_print("KoboTouch:contentid_from_path ContentType other than 6 - ContentType='%d'"%ContentType, "path='%s'"%path)
ContentID = path
ContentID = ContentID.replace(self._main_prefix, "file:///mnt/onboard/")
if self._card_a_prefix is not None:
ContentID = ContentID.replace(self._card_a_prefix, "file:///mnt/sd/")
ContentID = ContentID.replace("\\", '/')
if show_debug:
debug_print("KoboTouch:contentid_from_path - end - ContentID='%s'"%ContentID)
return ContentID
def get_content_type_from_path(self, path):
ContentType = 6
if self.fwversion < (1, 9, 17):
ContentType = super().get_content_type_from_path(path)
return ContentType
def get_content_type_from_extension(self, extension):
debug_print("KoboTouch:get_content_type_from_extension - start")
# With new firmware, ContentType appears to be 6 for all types of sideloaded books.
ContentType = 6
if self.fwversion < (1,9,17):
ContentType = super().get_content_type_from_extension(extension)
return ContentType
def set_plugboards(self, plugboards, pb_func):
self.plugboards = plugboards
self.plugboard_func = pb_func
def update_device_database_collections(self, booklists, collections_attributes, oncard):
debug_print("KoboTouch:update_device_database_collections - oncard='%s'"%oncard)
if self.modify_database_check("update_device_database_collections") is False:
return
# Only process categories in this list
supportedcategories = {
"Im_Reading": 1,
"Read": 2,
"Closed": 3,
"Shortlist": 4,
"Archived": 5,
}
# Define lists for the ReadStatus
readstatuslist = {
"Im_Reading":1,
"Read":2,
"Closed":3,
}
accessibilitylist = {
"Deleted":1,
"OverDrive":9,
"Preview":6,
"Recommendation":4,
}
# debug_print('KoboTouch:update_device_database_collections - collections_attributes=', collections_attributes)
create_collections = self.create_collections
delete_empty_collections = self.delete_empty_collections
update_series_details = self.update_series_details
update_core_metadata = self.update_core_metadata
update_purchased_kepubs = self.update_purchased_kepubs
debugging_title = self.get_debugging_title()
debug_print("KoboTouch:update_device_database_collections - set_debugging_title to '%s'" % debugging_title)
booklists.set_debugging_title(debugging_title)
booklists.set_device_managed_collections(self.ignore_collections_names)
bookshelf_attribute = len(collections_attributes) > 0
collections = booklists.get_collections(collections_attributes) if bookshelf_attribute else None
# debug_print('KoboTouch:update_device_database_collections - Collections:', collections)
# Create a connection to the sqlite database
# Needs to be outside books collection as in the case of removing
# the last book from the collection the list of books is empty
# and the removal of the last book would not occur
with closing(self.device_database_connection(use_row_factory=True)) as connection:
if self.manage_collections:
if collections:
# debug_print("KoboTouch:update_device_database_collections - length collections=" + str(len(collections)))
# Need to reset the collections outside the particular loops
# otherwise the last item will not be removed
if self.dbversion < 53:
debug_print("KoboTouch:update_device_database_collections - calling reset_readstatus")
self.reset_readstatus(connection, oncard)
if self.dbversion >= 14 and self.fwversion < self.min_fwversion_shelves:
debug_print("KoboTouch:update_device_database_collections - calling reset_favouritesindex")
self.reset_favouritesindex(connection, oncard)
# debug_print("KoboTouch:update_device_database_collections - length collections=", len(collections))
# debug_print("KoboTouch:update_device_database_collections - self.bookshelvelist=", self.bookshelvelist)
# Process any collections that exist
for category, books in collections.items():
debug_print("KoboTouch:update_device_database_collections - category='%s' books=%d"%(category, len(books)))
if create_collections and not (category in supportedcategories or category in readstatuslist or category in accessibilitylist):
self.check_for_bookshelf(connection, category)
# if category in self.bookshelvelist:
# debug_print("Category: ", category, " id = ", readstatuslist.get(category))
for book in books:
# debug_print(' Title:', book.title, 'category: ', category)
show_debug = self.is_debugging_title(book.title)
if show_debug:
debug_print(' Title="%s"'%book.title, 'category="%s"'%category)
# debug_print(book)
debug_print(' class=%s'%book.__class__)
debug_print(' book.contentID="%s"'%book.contentID)
debug_print(' book.application_id="%s"'%book.application_id)
if book.application_id is None:
continue
category_added = False
if book.contentID is None:
debug_print(' Do not know ContentID - Title="%s", Authors="%s", path="%s"'%(book.title, book.author, book.path))
extension = os.path.splitext(book.path)[1]
ContentType = self.get_content_type_from_extension(extension) if extension else self.get_content_type_from_path(book.path)
book.contentID = self.contentid_from_path(book.path, ContentType)
if category in self.ignore_collections_names:
debug_print(' Ignoring collection=%s' % category)
category_added = True
elif category in self.bookshelvelist and self.supports_bookshelves:
if show_debug:
debug_print(' length book.device_collections=%d'%len(book.device_collections))
if category not in book.device_collections:
if show_debug:
debug_print(' Setting bookshelf on device')
self.set_bookshelf(connection, book, category)
category_added = True
elif category in readstatuslist:
debug_print("KoboTouch:update_device_database_collections - about to set_readstatus - category='%s'"%(category, ))
# Manage ReadStatus
self.set_readstatus(connection, book.contentID, readstatuslist.get(category))
category_added = True
elif category == 'Shortlist' and self.dbversion >= 14:
if show_debug:
debug_print(' Have an older version shortlist - %s'%book.title)
# Manage FavouritesIndex/Shortlist
if not self.supports_bookshelves:
if show_debug:
debug_print(' and about to set it - %s'%book.title)
self.set_favouritesindex(connection, book.contentID)
category_added = True
elif category in accessibilitylist:
# Do not manage the Accessibility List
pass
if category_added and category not in book.device_collections:
if show_debug:
debug_print(' adding category to book.device_collections', book.device_collections)
book.device_collections.append(category)
else:
if show_debug:
debug_print(' category not added to book.device_collections', book.device_collections)
debug_print("KoboTouch:update_device_database_collections - end for category='%s'"%category)
elif bookshelf_attribute: # No collections but have set the shelf option
# Since no collections exist the ReadStatus needs to be reset to 0 (Unread)
debug_print("No Collections - resetting ReadStatus")
if self.dbversion < 53:
self.reset_readstatus(connection, oncard)
if self.dbversion >= 14 and self.fwversion < self.min_fwversion_shelves:
debug_print("No Collections - resetting FavouritesIndex")
self.reset_favouritesindex(connection, oncard)
# Set the series info and cleanup the bookshelves only if the firmware supports them and the user has set the options.
if (self.supports_bookshelves and self.manage_collections or self.supports_series()) and (
bookshelf_attribute or update_series_details or update_core_metadata):
debug_print("KoboTouch:update_device_database_collections - managing bookshelves and series.")
self.series_set = 0
self.core_metadata_set = 0
books_in_library = 0
for book in booklists:
# debug_print("KoboTouch:update_device_database_collections - book.title=%s, book.contentID=%s" % (book.title, book.contentID))
if book.application_id is not None and book.contentID is not None:
books_in_library += 1
show_debug = self.is_debugging_title(book.title)
if show_debug:
debug_print("KoboTouch:update_device_database_collections - book.title=%s" % book.title)
debug_print(
"KoboTouch:update_device_database_collections - contentId=%s,"
"update_core_metadata=%s,update_purchased_kepubs=%s, book.is_sideloaded=%s" % (
book.contentID, update_core_metadata, update_purchased_kepubs, book.is_sideloaded))
if update_core_metadata and (update_purchased_kepubs or book.is_sideloaded):
if show_debug:
debug_print("KoboTouch:update_device_database_collections - calling set_core_metadata")
self.set_core_metadata(connection, book)
elif update_series_details:
if show_debug:
debug_print("KoboTouch:update_device_database_collections - calling set_core_metadata - series only")
self.set_core_metadata(connection, book, series_only=True)
if self.manage_collections and bookshelf_attribute:
if show_debug:
debug_print("KoboTouch:update_device_database_collections - about to remove a book from shelves book.title=%s" % book.title)
self.remove_book_from_device_bookshelves(connection, book)
book.device_collections.extend(book.kobo_collections)
if not prefs['manage_device_metadata'] == 'manual' and delete_empty_collections:
debug_print("KoboTouch:update_device_database_collections - about to clear empty bookshelves")
self.delete_empty_bookshelves(connection)
debug_print("KoboTouch:update_device_database_collections - Number of series set=%d Number of books=%d" % (self.series_set, books_in_library))
debug_print("KoboTouch:update_device_database_collections - Number of core metadata set=%d Number of books=%d" % (
self.core_metadata_set, books_in_library))
self.dump_bookshelves(connection)
debug_print('KoboTouch:update_device_database_collections - Finished ')
def rebuild_collections(self, booklist, oncard):
debug_print("KoboTouch:rebuild_collections")
collections_attributes = self.get_collections_attributes()
debug_print('KoboTouch:rebuild_collections: collection fields:', collections_attributes)
self.update_device_database_collections(booklist, collections_attributes, oncard)
def upload_cover(self, path, filename, metadata, filepath):
'''
Upload book cover to the device. Default implementation does nothing.
:param path: The full path to the folder where the associated book is located.
:param filename: The name of the book file without the extension.
:param metadata: metadata belonging to the book. Use metadata.thumbnail
for cover
:param filepath: The full path to the ebook file
'''
debug_print("KoboTouch:upload_cover - path='%s' filename='%s' "%(path, filename))
debug_print(" filepath='%s' "%(filepath))
if not self.upload_covers:
# Building thumbnails disabled
# debug_print('KoboTouch: not uploading cover')
return
# Only upload covers to SD card if that is supported
if self._card_a_prefix and os.path.abspath(path).startswith(os.path.abspath(self._card_a_prefix)) and not self.supports_covers_on_sdcard():
return
# debug_print('KoboTouch: uploading cover')
try:
self._upload_cover(
path, filename, metadata, filepath,
self.upload_grayscale, self.dithered_covers,
self.keep_cover_aspect, self.letterbox_fs_covers, self.png_covers,
letterbox_color=self.letterbox_fs_covers_color)
except Exception as e:
debug_print('KoboTouch: FAILED to upload cover=%s Exception=%s'%(filepath, str(e)))
def imageid_from_contentid(self, ContentID):
ImageID = ContentID.replace('/', '_')
ImageID = ImageID.replace(' ', '_')
ImageID = ImageID.replace(':', '_')
ImageID = ImageID.replace('.', '_')
return ImageID
def images_path(self, path, imageId=None):
if self._card_a_prefix and os.path.abspath(path).startswith(os.path.abspath(self._card_a_prefix)) and self.supports_covers_on_sdcard():
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 = os.path.join(self._main_prefix, path_prefix)
if self.supports_images_tree() and imageId:
hash1 = qhash(imageId)
dir1 = hash1 & (0xff * 1)
dir2 = (hash1 & (0xff00 * 1)) >> 8
path = os.path.join(path, "%s" % dir1, "%s" % dir2)
if imageId:
path = os.path.join(path, imageId)
return path
def _calculate_kobo_cover_size(self, library_size, kobo_size, expand, keep_cover_aspect, letterbox):
# Remember the canvas size
canvas_size = kobo_size
# NOTE: Loosely based on Qt's QSize::scaled implementation
if keep_cover_aspect:
# NOTE: Unlike Qt, we round to avoid accumulating errors,
# as ImageOps will then floor via fit_image
aspect_ratio = library_size[0] / library_size[1]
rescaled_width = int(round(kobo_size[1] * aspect_ratio))
if expand:
use_height = (rescaled_width >= kobo_size[0])
else:
use_height = (rescaled_width <= kobo_size[0])
if use_height:
kobo_size = (rescaled_width, kobo_size[1])
else:
kobo_size = (kobo_size[0], int(round(kobo_size[0] / aspect_ratio)))
# Did we actually want to letterbox?
if not letterbox:
canvas_size = kobo_size
return (kobo_size, canvas_size)
def _create_cover_data(
self, cover_data, resize_to, minify_to, kobo_size,
upload_grayscale=False, dithered_covers=False, keep_cover_aspect=False, is_full_size=False, letterbox=False, png_covers=False, quality=90,
letterbox_color=DEFAULT_COVER_LETTERBOX_COLOR
):
'''
This will generate the new cover image from the cover in the library. It is a wrapper
for save_cover_data_to to allow it to be overridden in a subclass. For this reason,
options are passed in that are not used by this implementation.
:param cover_data: original cover data
:param resize_to: Size to resize the cover to (width, height). None means do not resize.
:param minify_to: Maximum canvas size for the resized cover (width, height).
:param kobo_size: Size of the cover image on the device.
:param upload_grayscale: boolean True if driver configured to send grayscale thumbnails
:param dithered_covers: boolean True if driver configured to quantize to 16-col grayscale
:param keep_cover_aspect: boolean - True if the aspect ratio of the cover in the library is to be kept.
:param is_full_size: True if this is the kobo_size is for the full size cover image
Passed to allow ability to process screensaver differently
to smaller thumbnails
:param letterbox: True if we were asked to handle the letterboxing
:param png_covers: True if we were asked to encode those images in PNG instead of JPG
:param quality: 0-100 Output encoding quality (or compression level for PNG, àla IM)
:param letterbox_color: Colour used for letterboxing.
'''
from calibre.utils.img import save_cover_data_to
data = save_cover_data_to(
cover_data, resize_to=resize_to, compression_quality=quality, minify_to=minify_to, grayscale=upload_grayscale, eink=dithered_covers,
letterbox=letterbox, data_fmt="png" if png_covers else "jpeg", letterbox_color=letterbox_color)
return data
def _upload_cover(
self, path, filename, metadata, filepath, upload_grayscale,
dithered_covers=False, keep_cover_aspect=False, letterbox_fs_covers=False, png_covers=False,
letterbox_color=DEFAULT_COVER_LETTERBOX_COLOR
):
from calibre.utils.imghdr import identify
from calibre.utils.img import optimize_png
debug_print("KoboTouch:_upload_cover - filename='%s' upload_grayscale='%s' dithered_covers='%s' "%(filename, upload_grayscale, dithered_covers))
if not metadata.cover:
return
show_debug = self.is_debugging_title(filename)
if show_debug:
debug_print("KoboTouch:_upload_cover - path='%s'"%path, "filename='%s'"%filename)
debug_print(" filepath='%s'"%filepath)
cover = self.normalize_path(metadata.cover.replace('/', os.sep))
if not os.path.exists(cover):
debug_print("KoboTouch:_upload_cover - Cover file does not exist in library")
return
# Get ContentID for Selected Book
extension = os.path.splitext(filepath)[1]
ContentType = self.get_content_type_from_extension(extension) if extension else self.get_content_type_from_path(filepath)
ContentID = self.contentid_from_path(filepath, ContentType)
try:
with closing(self.device_database_connection()) as connection:
cursor = connection.cursor()
t = (ContentID,)
cursor.execute('select ImageId from Content where BookID is Null and ContentID = ?', t)
try:
result = next(cursor)
ImageID = result[0]
except StopIteration:
ImageID = self.imageid_from_contentid(ContentID)
debug_print("KoboTouch:_upload_cover - No rows exist in the database - generated ImageID='%s'" % ImageID)
cursor.close()
if ImageID is not None:
path = self.images_path(path, ImageID)
if show_debug:
debug_print("KoboTouch:_upload_cover - About to loop over cover endings")
image_dir = os.path.dirname(os.path.abspath(path))
if not os.path.exists(image_dir):
debug_print("KoboTouch:_upload_cover - Image folder does not exist. Creating path='%s'" % (image_dir))
os.makedirs(image_dir)
with lopen(cover, 'rb') as f:
cover_data = f.read()
fmt, width, height = identify(cover_data)
library_cover_size = (width, height)
for ending, cover_options in self.cover_file_endings().items():
kobo_size, min_dbversion, max_dbversion, is_full_size = cover_options
if show_debug:
debug_print("KoboTouch:_upload_cover - library_cover_size=%s -> kobo_size=%s, min_dbversion=%d max_dbversion=%d, is_full_size=%s" % (
library_cover_size, kobo_size, min_dbversion, max_dbversion, is_full_size))
if self.dbversion >= min_dbversion and self.dbversion <= max_dbversion:
if show_debug:
debug_print("KoboTouch:_upload_cover - creating cover for ending='%s'"%ending) # , "library_cover_size'%s'"%library_cover_size)
fpath = path + ending
fpath = self.normalize_path(fpath.replace('/', os.sep))
# Never letterbox thumbnails, that's ugly. But for fullscreen covers, honor the setting.
letterbox = letterbox_fs_covers and is_full_size
# NOTE: Full size means we have to fit *inside* the
# given boundaries. Thumbnails, on the other hand, are
# *expanded* around those boundaries.
# In Qt, it'd mean full-screen covers are resized
# using Qt::KeepAspectRatio, while thumbnails are
# resized using Qt::KeepAspectRatioByExpanding
# (i.e., QSize's boundedTo() vs. expandedTo(). See also IM's '^' geometry token, for the same "expand" behavior.)
# Note that Nickel itself will generate bounded thumbnails, while it will download expanded thumbnails for store-bought KePubs...
# We chose to emulate the KePub behavior.
resize_to, expand_to = self._calculate_kobo_cover_size(library_cover_size, kobo_size, not is_full_size, keep_cover_aspect, letterbox)
if show_debug:
debug_print(
"KoboTouch:_calculate_kobo_cover_size - expand_to=%s"
" (vs. kobo_size=%s) & resize_to=%s, keep_cover_aspect=%s & letterbox_fs_covers=%s, png_covers=%s" % (
expand_to, kobo_size, resize_to, keep_cover_aspect, letterbox_fs_covers, png_covers))
# NOTE: To speed things up, we enforce a lower
# compression level for png_covers, as the final
# optipng pass will then select a higher compression
# level anyway,
# so the compression level from that first pass
# is irrelevant, and only takes up precious time
# ;).
quality = 10 if png_covers else 90
# Return the data resized and properly grayscaled/dithered/letterboxed if requested
data = self._create_cover_data(
cover_data, resize_to, expand_to, kobo_size, upload_grayscale,
dithered_covers, keep_cover_aspect, is_full_size, letterbox, png_covers, quality,
letterbox_color=letterbox_color)
# NOTE: If we're writing a PNG file, go through a quick
# optipng pass to make sure it's encoded properly, as
# Qt doesn't afford us enough control to do it right...
# Unfortunately, optipng doesn't support reading
# pipes, so this gets a bit clunky as we have go
# through a temporary file...
if png_covers:
tmp_cover = better_mktemp()
with lopen(tmp_cover, 'wb') as f:
f.write(data)
optimize_png(tmp_cover, level=1)
# Crossing FS boundaries, can't rename, have to copy + delete :/
shutil.copy2(tmp_cover, fpath)
os.remove(tmp_cover)
else:
with lopen(fpath, 'wb') as f:
f.write(data)
fsync(f)
except Exception as e:
err = str(e)
debug_print("KoboTouch:_upload_cover - Exception string: %s"%err)
raise
def remove_book_from_device_bookshelves(self, connection, book):
show_debug = self.is_debugging_title(book.title) # or True
remove_shelf_list = set(book.current_shelves) - set(book.device_collections)
remove_shelf_list = remove_shelf_list - set(self.ignore_collections_names)
if show_debug:
debug_print('KoboTouch:remove_book_from_device_bookshelves - book.application_id="%s"'%book.application_id)
debug_print('KoboTouch:remove_book_from_device_bookshelves - book.contentID="%s"'%book.contentID)
debug_print('KoboTouch:remove_book_from_device_bookshelves - book.device_collections=', book.device_collections)
debug_print('KoboTouch:remove_book_from_device_bookshelves - book.current_shelves=', book.current_shelves)
debug_print('KoboTouch:remove_book_from_device_bookshelves - remove_shelf_list=', remove_shelf_list)
if len(remove_shelf_list) == 0:
return
query = 'DELETE FROM ShelfContent WHERE ContentId = ?'
values = [book.contentID,]
if book.device_collections:
placeholder = '?'
placeholders = ','.join(placeholder for unused in book.device_collections)
query += ' and ShelfName not in (%s)' % placeholders
values.extend(book.device_collections)
if show_debug:
debug_print('KoboTouch:remove_book_from_device_bookshelves query="%s"'%query)
debug_print('KoboTouch:remove_book_from_device_bookshelves values="%s"'%values)
cursor = connection.cursor()
cursor.execute(query, values)
cursor.close()
def set_filesize_in_device_database(self, connection, contentID, fpath):
show_debug = self.is_debugging_title(fpath)
if show_debug:
debug_print('KoboTouch:set_filesize_in_device_database contentID="%s"'%contentID)
test_query = 'SELECT ___FileSize ' \
'FROM content ' \
'WHERE ContentID = ? ' \
' AND ContentType = 6'
test_values = (contentID, )
updatequery = 'UPDATE content ' \
'SET ___FileSize = ? ' \
'WHERE ContentId = ? ' \
'AND ContentType = 6'
cursor = connection.cursor()
cursor.execute(test_query, test_values)
try:
result = next(cursor)
except StopIteration:
result = None
if result is None:
if show_debug:
debug_print(' Did not find a record - new book on device')
elif os.path.exists(fpath):
file_size = os.stat(self.normalize_path(fpath)).st_size
if show_debug:
debug_print(' Found a record - will update - ___FileSize=', result[0], ' file_size=', file_size)
if file_size != int(result[0]):
update_values = (file_size, contentID, )
cursor.execute(updatequery, update_values)
if show_debug:
debug_print(' Size updated.')
cursor.close()
# debug_print("KoboTouch:set_filesize_in_device_database - end")
def delete_empty_bookshelves(self, connection):
debug_print("KoboTouch:delete_empty_bookshelves - start")
ignore_collections_placeholder = ''
ignore_collections_values = []
if self.ignore_collections_names:
placeholder = ',?'
ignore_collections_placeholder = ''.join(placeholder for unused in self.ignore_collections_names)
ignore_collections_values.extend(self.ignore_collections_names)
debug_print("KoboTouch:delete_empty_bookshelves - ignore_collections_in=", ignore_collections_placeholder)
debug_print("KoboTouch:delete_empty_bookshelves - ignore_collections=", ignore_collections_values)
delete_query = ("DELETE FROM Shelf "
"WHERE Shelf._IsSynced = 'false' "
"AND Shelf.InternalName not in ('Shortlist', 'Wishlist'" + ignore_collections_placeholder + ") "
"AND (Type IS NULL OR Type <> 'SystemTag') " # Collections are created with Type of NULL and change after a sync.
"AND NOT EXISTS "
"(SELECT 1 FROM ShelfContent c "
"WHERE Shelf.Name = C.ShelfName "
"AND c._IsDeleted <> 'true')")
debug_print("KoboTouch:delete_empty_bookshelves - delete_query=", delete_query)
update_query = ("UPDATE Shelf "
"SET _IsDeleted = 'true' "
"WHERE Shelf._IsSynced = 'true' "
"AND Shelf.InternalName not in ('Shortlist', 'Wishlist'" + ignore_collections_placeholder + ") "
"AND (Type IS NULL OR Type <> 'SystemTag') "
"AND NOT EXISTS "
"(SELECT 1 FROM ShelfContent C "
"WHERE Shelf.Name = C.ShelfName "
"AND c._IsDeleted <> 'true')")
debug_print("KoboTouch:delete_empty_bookshelves - update_query=", update_query)
delete_activity_query = ("DELETE FROM Activity "
"WHERE Type = 'Shelf' "
"AND NOT EXISTS "
"(SELECT 1 FROM Shelf "
"WHERE Shelf.Name = Activity.Id "
"AND Shelf._IsDeleted = 'false')"
)
debug_print("KoboTouch:delete_empty_bookshelves - delete_activity_query=", delete_activity_query)
cursor = connection.cursor()
cursor.execute(delete_query, ignore_collections_values)
cursor.execute(update_query, ignore_collections_values)
if self.has_activity_table():
cursor.execute(delete_activity_query)
cursor.close()
debug_print("KoboTouch:delete_empty_bookshelves - end")
def get_bookshelflist(self, connection):
# Retrieve the list of booksehelves
# debug_print('KoboTouch:get_bookshelflist')
bookshelves = []
if not self.supports_bookshelves:
return bookshelves
query = 'SELECT Name FROM Shelf WHERE _IsDeleted = "false"'
cursor = connection.cursor()
cursor.execute(query)
# count_bookshelves = 0
for row in cursor:
bookshelves.append(row['Name'])
# count_bookshelves = i + 1
cursor.close()
# debug_print("KoboTouch:get_bookshelflist - count bookshelves=" + str(count_bookshelves))
return bookshelves
def set_bookshelf(self, connection, book, shelfName):
show_debug = self.is_debugging_title(book.title)
if show_debug:
debug_print('KoboTouch:set_bookshelf book.ContentID="%s"'%book.contentID)
debug_print('KoboTouch:set_bookshelf book.current_shelves="%s"'%book.current_shelves)
if shelfName in book.current_shelves:
if show_debug:
debug_print(' book already on shelf.')
return
test_query = 'SELECT _IsDeleted FROM ShelfContent WHERE ShelfName = ? and ContentId = ?'
test_values = (shelfName, book.contentID, )
addquery = 'INSERT INTO ShelfContent ("ShelfName","ContentId","DateModified","_IsDeleted","_IsSynced") VALUES (?, ?, ?, "false", "false")'
add_values = (shelfName, book.contentID, time.strftime(self.TIMESTAMP_STRING, time.gmtime()), )
updatequery = 'UPDATE ShelfContent SET _IsDeleted = "false" WHERE ShelfName = ? and ContentId = ?'
update_values = (shelfName, book.contentID, )
cursor = connection.cursor()
cursor.execute(test_query, test_values)
try:
result = next(cursor)
except StopIteration:
result = None
if result is None:
if show_debug:
debug_print(' Did not find a record - adding')
cursor.execute(addquery, add_values)
elif result['_IsDeleted'] == 'true':
if show_debug:
debug_print(' Found a record - updating - result=', result)
cursor.execute(updatequery, update_values)
cursor.close()
# debug_print("KoboTouch:set_bookshelf - end")
def check_for_bookshelf(self, connection, bookshelf_name):
show_debug = self.is_debugging_title(bookshelf_name)
if show_debug:
debug_print('KoboTouch:check_for_bookshelf bookshelf_name="%s"'%bookshelf_name)
test_query = 'SELECT InternalName, Name, _IsDeleted FROM Shelf WHERE Name = ?'
test_values = (bookshelf_name, )
addquery = 'INSERT INTO "main"."Shelf"'
add_values = (time.strftime(self.TIMESTAMP_STRING, time.gmtime()),
bookshelf_name,
time.strftime(self.TIMESTAMP_STRING, time.gmtime()),
bookshelf_name,
"false",
"true",
"false",
)
shelf_type = "UserTag" # if self.supports_reading_list else None
if self.dbversion < 64:
addquery += ' ("CreationDate","InternalName","LastModified","Name","_IsDeleted","_IsVisible","_IsSynced")'\
' VALUES (?, ?, ?, ?, ?, ?, ?)'
else:
addquery += ' ("CreationDate", "InternalName","LastModified","Name","_IsDeleted","_IsVisible","_IsSynced", "Id", "Type")'\
' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)'
add_values = add_values +(bookshelf_name, shelf_type)
if show_debug:
debug_print('KoboTouch:check_for_bookshelf addquery=', addquery)
debug_print('KoboTouch:check_for_bookshelf add_values=', add_values)
updatequery = 'UPDATE Shelf SET _IsDeleted = "false" WHERE Name = ?'
cursor = connection.cursor()
cursor.execute(test_query, test_values)
try:
result = next(cursor)
except StopIteration:
result = None
if result is None:
if show_debug:
debug_print(' Did not find a record - adding shelf "%s"' % bookshelf_name)
cursor.execute(addquery, add_values)
elif result['_IsDeleted'] == 'true':
debug_print("KoboTouch:check_for_bookshelf - Shelf '{}' is deleted - undeleting. result['_IsDeleted']='{}'".format(
bookshelf_name, str(result['_IsDeleted'])))
cursor.execute(updatequery, test_values)
cursor.close()
# Update the bookshelf list.
self.bookshelvelist = self.get_bookshelflist(connection)
# debug_print("KoboTouch:set_bookshelf - end")
def remove_from_bookshelves(self, connection, oncard, ContentID=None, bookshelves=None):
debug_print('KoboTouch:remove_from_bookshelf ContentID=', ContentID)
if not self.supports_bookshelves:
return
query = 'DELETE FROM ShelfContent'
values = []
if ContentID is not None:
query += ' WHERE ContentId = ?'
values.append(ContentID)
else:
if oncard == 'carda':
query += ' WHERE ContentID like \'file:///mnt/sd/%\''
elif oncard != 'carda' and oncard != 'cardb':
query += ' WHERE ContentID not like \'file:///mnt/sd/%\''
if bookshelves:
placeholder = '?'
placeholders = ','.join(placeholder for unused in bookshelves)
query += ' and ShelfName in (%s)' % placeholders
values.append(bookshelves)
debug_print('KoboTouch:remove_from_bookshelf query=', query)
debug_print('KoboTouch:remove_from_bookshelf values=', values)
cursor = connection.cursor()
cursor.execute(query, values)
cursor.close()
debug_print("KoboTouch:remove_from_bookshelf - end")
# No longer used, but keep for a little bit.
def set_series(self, connection, book):
show_debug = self.is_debugging_title(book.title)
if show_debug:
debug_print('KoboTouch:set_series book.kobo_series="%s"'%book.kobo_series)
debug_print('KoboTouch:set_series book.series="%s"'%book.series)
debug_print('KoboTouch:set_series book.series_index=', book.series_index)
if book.series == book.kobo_series:
kobo_series_number = None
if book.kobo_series_number is not None:
try:
kobo_series_number = float(book.kobo_series_number)
except:
kobo_series_number = None
if kobo_series_number == book.series_index:
if show_debug:
debug_print('KoboTouch:set_series - series info the same - not changing')
return
update_query = 'UPDATE content SET Series=?, SeriesNumber==? where BookID is Null and ContentID = ?'
if book.series is None:
update_values = (None, None, book.contentID, )
elif book.series_index is None: # This should never happen, but...
update_values = (book.series, None, book.contentID, )
else:
update_values = (book.series, "%g"%book.series_index, book.contentID, )
cursor = connection.cursor()
try:
if show_debug:
debug_print('KoboTouch:set_series - about to set - parameters:', update_values)
cursor.execute(update_query, update_values)
self.series_set += 1
except:
debug_print(' Database Exception: Unable to set series info')
raise
finally:
cursor.close()
if show_debug:
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)
show_debug = self.is_debugging_title(book.title)
if show_debug:
debug_print(f'KoboTouch:set_core_metadata book="{book}", \nseries_only="{series_only}"')
plugboard = None
if self.plugboard_func and not series_only:
if book.contentID.endswith('.kepub.epub') or not os.path.splitext(book.contentID)[1]:
extension = 'kepub'
else:
extension = os.path.splitext(book.contentID)[1][1:]
plugboard = self.plugboard_func(self.__class__.__name__, extension, self.plugboards)
# If the book is a kepub, and there is no kepub plugboard, use the epub plugboard if it exists.
if not plugboard and extension == 'kepub':
plugboard = self.plugboard_func(self.__class__.__name__, 'epub', self.plugboards)
if plugboard is not None:
newmi = book.deepcopy_metadata()
newmi.template_to_attribute(book, plugboard)
else:
newmi = book
update_query = 'UPDATE content SET '
update_values = []
set_clause = ''
changes_found = False
kobo_metadata = book.kobo_metadata
if show_debug:
debug_print(f'KoboTouch:set_core_metadata newmi.series="{newmi.series}"')
debug_print(f'KoboTouch:set_core_metadata kobo_metadata.series="{kobo_metadata.series}"')
debug_print(f'KoboTouch:set_core_metadata newmi.series_index="{newmi.series_index}"')
debug_print(f'KoboTouch:set_core_metadata kobo_metadata.series_index="{kobo_metadata.series_index}"')
debug_print(f'KoboTouch:set_core_metadata book.kobo_series_number="{book.kobo_series_number}"')
if newmi.series is not None:
new_series = newmi.series
try:
new_series_number = "%g" % newmi.series_index
except:
new_series_number = None
else:
new_series = None
new_series_number = None
series_changed = not (new_series == kobo_metadata.series)
series_number_changed = not (new_series_number == book.kobo_series_number)
if show_debug:
debug_print(f'KoboTouch:set_core_metadata new_series="{new_series}"')
debug_print(f'KoboTouch:set_core_metadata new_series_number="{new_series_number}"')
debug_print(f'KoboTouch:set_core_metadata series_number_changed="{series_number_changed}"')
debug_print(f'KoboTouch:set_core_metadata series_changed="{series_changed}"')
if series_changed or series_number_changed:
update_values.append(new_series)
set_clause += ', Series = ? '
update_values.append(new_series_number)
set_clause += ', SeriesNumber = ? '
if self.supports_series_list and book.is_sideloaded:
series_id = self.kobo_series_dict.get(new_series, new_series)
try:
kobo_series_id = book.kobo_series_id
kobo_series_number_float = book.kobo_series_number_float
except Exception: # This should mean the book was sent to the device during the current session.
kobo_series_id = None
kobo_series_number_float = None
if series_changed or series_number_changed \
or not kobo_series_id == series_id \
or not kobo_series_number_float == newmi.series_index:
update_values.append(series_id)
set_clause += ', SeriesID = ? '
update_values.append(newmi.series_index)
set_clause += ', 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:
if not (newmi.title == kobo_metadata.title):
update_values.append(newmi.title)
set_clause += ', 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 = ? '
if not (newmi.publisher == kobo_metadata.publisher):
update_values.append(newmi.publisher)
set_clause += ', 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 = ? '
if not (newmi.comments == kobo_metadata.comments):
update_values.append(newmi.comments)
set_clause += ', Description = ? '
if not (newmi.isbn == kobo_metadata.isbn):
update_values.append(newmi.isbn)
set_clause += ', 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 = ? '
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)
debug_print("KoboTouch:set_core_metadata - new_subtitle=", new_subtitle)
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 = ? '
if len(set_clause) > 0:
update_query += set_clause[1:]
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)
if changes_found:
update_query += 'WHERE ContentID = ? AND BookID IS NULL'
update_values.append(book.contentID)
cursor = connection.cursor()
try:
if show_debug:
debug_print('KoboTouch:set_core_metadata - about to set - parameters:', update_values)
debug_print('KoboTouch:set_core_metadata - about to set - update_query:', update_query)
cursor.execute(update_query, update_values)
self.core_metadata_set += 1
except:
debug_print(' Database Exception: Unable to set the core metadata')
raise
finally:
cursor.close()
if show_debug:
debug_print("KoboTouch:set_core_metadata - end")
@classmethod
def config_widget(cls):
# TODO: Cleanup the following
cls.current_friendly_name = cls.gui_name
from calibre.devices.kobo.kobotouch_config import KOBOTOUCHConfig
return KOBOTOUCHConfig(cls.settings(), cls.FORMATS,
cls.SUPPORTS_SUB_DIRS, cls.MUST_READ_METADATA,
cls.SUPPORTS_USE_AUTHOR_SORT, cls.EXTRA_CUSTOMIZATION_MESSAGE,
cls, extra_customization_choices=cls.EXTRA_CUSTOMIZATION_CHOICES
)
@classmethod
def get_pref(cls, key):
''' Get the setting named key. First looks for a device specific setting.
If that is not found looks for a device default and if that is not
found uses the global default.'''
# debug_print("KoboTouch::get_prefs - key=", key, "cls=", cls)
if not cls.opts:
cls.opts = cls.settings()
try:
return getattr(cls.opts, key)
except:
debug_print("KoboTouch::get_prefs - probably an extra_customization:", key)
return None
@classmethod
def save_settings(cls, config_widget):
cls.opts = None
config_widget.commit()
@classmethod
def save_template(cls):
return cls.settings().save_template
@classmethod
def _config(cls):
c = super()._config()
c.add_opt('manage_collections', default=True)
c.add_opt('collections_columns', default='')
c.add_opt('create_collections', default=False)
c.add_opt('delete_empty_collections', default=False)
c.add_opt('ignore_collections_names', default='')
c.add_opt('upload_covers', default=False)
c.add_opt('dithered_covers', default=False)
c.add_opt('keep_cover_aspect', default=False)
c.add_opt('upload_grayscale', default=False)
c.add_opt('letterbox_fs_covers', default=False)
c.add_opt('letterbox_fs_covers_color', default=DEFAULT_COVER_LETTERBOX_COLOR)
c.add_opt('png_covers', default=False)
c.add_opt('show_archived_books', default=False)
c.add_opt('show_previews', default=False)
c.add_opt('show_recommendations', default=False)
c.add_opt('update_series', default=True)
c.add_opt('update_core_metadata', default=False)
c.add_opt('update_purchased_kepubs', default=False)
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('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('support_newer_firmware', default=False)
c.add_opt('debugging_title', default='')
c.add_opt('driver_version', default='') # Mainly for debugging purposes, but might use if need to migrate between versions.
return c
@classmethod
def settings(cls):
opts = cls._config().parse()
if opts.extra_customization:
opts = cls.migrate_old_settings(opts)
cls.opts = opts
return opts
def isAura(self):
return self.detected_device.idProduct in self.AURA_PRODUCT_ID
def isAuraEdition2(self):
return self.detected_device.idProduct in self.AURA_EDITION2_PRODUCT_ID
def isAuraHD(self):
return self.detected_device.idProduct in self.AURA_HD_PRODUCT_ID
def isAuraH2O(self):
return self.detected_device.idProduct in self.AURA_H2O_PRODUCT_ID
def isAuraH2OEdition2(self):
return self.detected_device.idProduct in self.AURA_H2O_EDITION2_PRODUCT_ID
def isAuraOne(self):
return self.detected_device.idProduct in self.AURA_ONE_PRODUCT_ID
def isClaraHD(self):
return self.detected_device.idProduct in self.CLARA_HD_PRODUCT_ID
def isElipsa(self):
return self.detected_device.idProduct in self.ELIPSA_PRODUCT_ID
def isForma(self):
return self.detected_device.idProduct in self.FORMA_PRODUCT_ID
def isGlo(self):
return self.detected_device.idProduct in self.GLO_PRODUCT_ID
def isGloHD(self):
return self.detected_device.idProduct in self.GLO_HD_PRODUCT_ID
def isLibraH2O(self):
return self.detected_device.idProduct in self.LIBRA_H2O_PRODUCT_ID
def isLibra2(self):
return self.detected_device.idProduct in self.LIBRA2_PRODUCT_ID
def isMini(self):
return self.detected_device.idProduct in self.MINI_PRODUCT_ID
def isNia(self):
return self.detected_device.idProduct in self.NIA_PRODUCT_ID
def isSage(self):
return self.detected_device.idProduct in self.SAGE_PRODUCT_ID
def isTouch(self):
return self.detected_device.idProduct in self.TOUCH_PRODUCT_ID
def isTouch2(self):
return self.detected_device.idProduct in self.TOUCH2_PRODUCT_ID
def cover_file_endings(self):
if self.isAura():
_cover_file_endings = self.AURA_COVER_FILE_ENDINGS
elif self.isAuraEdition2():
_cover_file_endings = self.GLO_COVER_FILE_ENDINGS
elif self.isAuraHD():
_cover_file_endings = self.AURA_HD_COVER_FILE_ENDINGS
elif self.isAuraH2O():
_cover_file_endings = self.AURA_H2O_COVER_FILE_ENDINGS
elif self.isAuraH2OEdition2():
_cover_file_endings = self.AURA_HD_COVER_FILE_ENDINGS
elif self.isAuraOne():
_cover_file_endings = self.AURA_ONE_COVER_FILE_ENDINGS
elif self.isClaraHD():
_cover_file_endings = self.GLO_HD_COVER_FILE_ENDINGS
elif self.isElipsa():
_cover_file_endings = self.AURA_ONE_COVER_FILE_ENDINGS
elif self.isForma():
_cover_file_endings = self.FORMA_COVER_FILE_ENDINGS
elif self.isGlo():
_cover_file_endings = self.GLO_COVER_FILE_ENDINGS
elif self.isGloHD():
_cover_file_endings = self.GLO_HD_COVER_FILE_ENDINGS
elif self.isLibraH2O():
_cover_file_endings = self.LIBRA_H2O_COVER_FILE_ENDINGS
elif self.isLibra2():
_cover_file_endings = self.LIBRA_H2O_COVER_FILE_ENDINGS
elif self.isMini():
_cover_file_endings = self.LEGACY_COVER_FILE_ENDINGS
elif self.isNia():
_cover_file_endings = self.GLO_COVER_FILE_ENDINGS
elif self.isSage():
_cover_file_endings = self.FORMA_COVER_FILE_ENDINGS
elif self.isTouch():
_cover_file_endings = self.LEGACY_COVER_FILE_ENDINGS
elif self.isTouch2():
_cover_file_endings = self.LEGACY_COVER_FILE_ENDINGS
else:
_cover_file_endings = self.LEGACY_COVER_FILE_ENDINGS
# Don't forget to merge that on top of the common dictionary (c.f., https://stackoverflow.com/q/38987)
_all_cover_file_endings = self.COMMON_COVER_FILE_ENDINGS.copy()
_all_cover_file_endings.update(_cover_file_endings)
return _all_cover_file_endings
def set_device_name(self):
device_name = self.gui_name
if self.isAura():
device_name = 'Kobo Aura'
elif self.isAuraEdition2():
device_name = 'Kobo Aura Edition 2'
elif self.isAuraHD():
device_name = 'Kobo Aura HD'
elif self.isAuraH2O():
device_name = 'Kobo Aura H2O'
elif self.isAuraH2OEdition2():
device_name = 'Kobo Aura H2O Edition 2'
elif self.isAuraOne():
device_name = 'Kobo Aura ONE'
elif self.isClaraHD():
device_name = 'Kobo Clara HD'
elif self.isElipsa():
device_name = 'Kobo Elipsa'
elif self.isForma():
device_name = 'Kobo Forma'
elif self.isGlo():
device_name = 'Kobo Glo'
elif self.isGloHD():
device_name = 'Kobo Glo HD'
elif self.isLibraH2O():
device_name = 'Kobo Libra H2O'
elif self.isLibra2():
device_name = 'Kobo Libra 2'
elif self.isMini():
device_name = 'Kobo Mini'
elif self.isNia():
device_name = 'Kobo Nia'
elif self.isSage():
device_name = 'Kobo Sage'
elif self.isTouch():
device_name = 'Kobo Touch'
elif self.isTouch2():
device_name = 'Kobo Touch 2'
self.__class__.gui_name = device_name
return device_name
@property
def manage_collections(self):
return self.get_pref('manage_collections')
@property
def create_collections(self):
return self.manage_collections and self.supports_bookshelves and self.get_pref('create_collections') and len(self.collections_columns) > 0
@property
def collections_columns(self):
return self.get_pref('collections_columns') if self.manage_collections else ''
def get_collections_attributes(self):
collections_str = self.collections_columns
collections = [x.lower().strip() for x in collections_str.split(',')] if collections_str else []
return collections
@property
def delete_empty_collections(self):
return self.manage_collections and self.get_pref('delete_empty_collections')
@property
def ignore_collections_names(self):
# Cache the collection from the options string.
if not hasattr(self.opts, '_ignore_collections_names'):
icn = self.get_pref('ignore_collections_names')
self.opts._ignore_collections_names = [x.strip() for x in icn.split(',')] if icn else []
return self.opts._ignore_collections_names
@property
def create_bookshelves(self):
# Only for backwards compatibility
return self.manage_collections
@property
def delete_empty_shelves(self):
# Only for backwards compatibility
return self.delete_empty_collections
@property
def upload_covers(self):
return self.get_pref('upload_covers')
@property
def keep_cover_aspect(self):
return self.upload_covers and self.get_pref('keep_cover_aspect')
@property
def upload_grayscale(self):
return self.upload_covers and self.get_pref('upload_grayscale')
@property
def dithered_covers(self):
return self.upload_grayscale and self.get_pref('dithered_covers')
@property
def letterbox_fs_covers(self):
return self.keep_cover_aspect and self.get_pref('letterbox_fs_covers')
@property
def letterbox_fs_covers_color(self):
return self.get_pref('letterbox_fs_covers_color')
@property
def png_covers(self):
return self.upload_grayscale and self.get_pref('png_covers')
def modifying_epub(self):
return self.modifying_css()
def modifying_css(self):
return self.get_pref('modify_css')
@property
def override_kobo_replace_existing(self):
return self.get_pref('override_kobo_replace_existing')
@property
def update_device_metadata(self):
return self.get_pref('update_device_metadata')
@property
def update_series_details(self):
return self.update_device_metadata and self.get_pref('update_series') and self.supports_series()
@property
def update_subtitle(self):
# Subtitle was added to the database at the same time as the series support.
return self.update_device_metadata and self.supports_series() and self.get_pref('update_subtitle')
@property
def subtitle_template(self):
if not self.update_subtitle:
return None
subtitle_template = self.get_pref('subtitle_template')
subtitle_template = subtitle_template.strip() if subtitle_template is not None else None
return subtitle_template
@property
def update_core_metadata(self):
return self.update_device_metadata and self.get_pref('update_core_metadata')
@property
def update_purchased_kepubs(self):
return self.update_device_metadata and self.get_pref('update_purchased_kepubs')
@classmethod
def get_debugging_title(cls):
debugging_title = cls.get_pref('debugging_title')
if not debugging_title: # Make sure the value is set to prevent rereading the settings.
debugging_title = ''
return debugging_title
@property
def supports_bookshelves(self):
return self.dbversion >= self.min_supported_dbversion
@property
def show_archived_books(self):
return self.get_pref('show_archived_books')
@property
def show_previews(self):
return self.get_pref('show_previews')
@property
def show_recommendations(self):
return self.get_pref('show_recommendations')
@property
def read_metadata(self):
return self.get_pref('read_metadata')
def supports_series(self):
return self.dbversion >= self.min_dbversion_series
@property
def supports_series_list(self):
return self.dbversion >= self.min_dbversion_seriesid and self.fwversion >= self.min_fwversion_serieslist
@property
def supports_audiobooks(self):
return self.fwversion >= self.min_fwversion_audiobooks
def supports_kobo_archive(self):
return self.dbversion >= self.min_dbversion_archive
def supports_overdrive(self):
return self.fwversion >= self.min_fwversion_overdrive
def supports_covers_on_sdcard(self):
return self.dbversion >= self.min_dbversion_images_on_sdcard and self.fwversion >= self.min_fwversion_images_on_sdcard
def supports_images_tree(self):
return self.fwversion >= self.min_fwversion_images_tree
def has_externalid(self):
return self.dbversion >= self.min_dbversion_externalid
def has_activity_table(self):
return self.dbversion >= self.min_dbversion_activity
def modify_database_check(self, function):
# Checks to see whether the database version is supported
# and whether the user has chosen to support the firmware version
if self.dbversion > self.supported_dbversion or self.is_supported_fwversion:
# Unsupported database
if not self.get_pref('support_newer_firmware'):
debug_print('The database has been upgraded past supported version')
self.report_progress(1.0, _('Removing books from device...'))
from calibre.devices.errors import UserFeedback
raise UserFeedback(_("Kobo database version unsupported - See details"),
_('Your Kobo is running an updated firmware/database version.'
' As calibre does not know about this updated firmware,'
' database editing is disabled, to prevent corruption.'
' You can still send books to your Kobo with calibre, '
' but deleting books and managing collections is disabled.'
' If you are willing to experiment and know how to reset'
' your Kobo to Factory defaults, you can override this'
' check by right clicking the device icon in calibre and'
' selecting "Configure this device" and then the'
' "Attempt to support newer firmware" option.'
' Doing so may require you to perform a Factory reset of'
' your Kobo.'
) +
'\n\n' +
_('Discussion of any new Kobo firmware can be found in the'
' Kobo forum at MobileRead. This is at %s.'
) % 'https://www.mobileread.com/forums/forumdisplay.php?f=223' + '\n' +
(
'\nDevice database version: %s.'
'\nDevice firmware version: %s'
) % (self.dbversion, self.fwversion),
UserFeedback.WARN
)
return False
else:
# The user chose to edit the database anyway
return True
else:
# Supported database version
return True
@property
def is_supported_fwversion(self):
# Starting with firmware version 3.19.x, the last number appears to be is a
# build number. It can be safely ignored when testing the firmware version.
debug_print("KoboTouch::is_supported_fwversion - self.fwversion[:2]", self.fwversion[:2])
return self.fwversion[:2] > self.max_supported_fwversion
@classmethod
def migrate_old_settings(cls, settings):
debug_print("KoboTouch::migrate_old_settings - start")
debug_print("KoboTouch::migrate_old_settings - settings.extra_customization=", settings.extra_customization)
debug_print("KoboTouch::migrate_old_settings - For class=", cls.name)
count_options = 0
OPT_COLLECTIONS = count_options
count_options += 1
OPT_CREATE_BOOKSHELVES = count_options
count_options += 1
OPT_DELETE_BOOKSHELVES = count_options
count_options += 1
OPT_UPLOAD_COVERS = count_options
count_options += 1
OPT_UPLOAD_GRAYSCALE_COVERS = count_options
count_options += 1
OPT_KEEP_COVER_ASPECT_RATIO = count_options
count_options += 1
OPT_SHOW_ARCHIVED_BOOK_RECORDS = count_options
count_options += 1
OPT_SHOW_PREVIEWS = count_options
count_options += 1
OPT_SHOW_RECOMMENDATIONS = count_options
count_options += 1
OPT_UPDATE_SERIES_DETAILS = count_options
count_options += 1
OPT_MODIFY_CSS = count_options
count_options += 1
OPT_SUPPORT_NEWER_FIRMWARE = count_options
count_options += 1
OPT_DEBUGGING_TITLE = count_options
# Always migrate options if for the KoboTouch class.
# For a subclass, only migrate the KoboTouch options if they haven't already been migrated. This is based on
# the total number of options.
if cls == KOBOTOUCH or len(settings.extra_customization) >= count_options:
config = cls._config()
debug_print("KoboTouch::migrate_old_settings - config.preferences=", config.preferences)
debug_print("KoboTouch::migrate_old_settings - settings need to be migrated")
settings.manage_collections = True
settings.collections_columns = settings.extra_customization[OPT_COLLECTIONS]
debug_print("KoboTouch::migrate_old_settings - settings.collections_columns=", settings.collections_columns)
settings.create_collections = settings.extra_customization[OPT_CREATE_BOOKSHELVES]
settings.delete_empty_collections = settings.extra_customization[OPT_DELETE_BOOKSHELVES]
settings.upload_covers = settings.extra_customization[OPT_UPLOAD_COVERS]
settings.keep_cover_aspect = settings.extra_customization[OPT_KEEP_COVER_ASPECT_RATIO]
settings.upload_grayscale = settings.extra_customization[OPT_UPLOAD_GRAYSCALE_COVERS]
settings.show_archived_books = settings.extra_customization[OPT_SHOW_ARCHIVED_BOOK_RECORDS]
settings.show_previews = settings.extra_customization[OPT_SHOW_PREVIEWS]
settings.show_recommendations = settings.extra_customization[OPT_SHOW_RECOMMENDATIONS]
# If the configuration hasn't been change for a long time, the last few option will be out
# of sync. The last two options are always the support newer firmware and the debugging
# title. Set seties and Modify CSS were the last two new options. The debugging title is
# a string, so looking for that.
start_subclass_extra_options = OPT_MODIFY_CSS
debugging_title = ''
if isinstance(settings.extra_customization[OPT_MODIFY_CSS], string_or_bytes):
debug_print("KoboTouch::migrate_old_settings - Don't have update_series option")
settings.update_series = config.get_option('update_series').default
settings.modify_css = config.get_option('modify_css').default
settings.support_newer_firmware = settings.extra_customization[OPT_UPDATE_SERIES_DETAILS]
debugging_title = settings.extra_customization[OPT_MODIFY_CSS]
start_subclass_extra_options = OPT_MODIFY_CSS + 1
elif isinstance(settings.extra_customization[OPT_SUPPORT_NEWER_FIRMWARE], string_or_bytes):
debug_print("KoboTouch::migrate_old_settings - Don't have modify_css option")
settings.update_series = settings.extra_customization[OPT_UPDATE_SERIES_DETAILS]
settings.modify_css = config.get_option('modify_css').default
settings.support_newer_firmware = settings.extra_customization[OPT_MODIFY_CSS]
debugging_title = settings.extra_customization[OPT_SUPPORT_NEWER_FIRMWARE]
start_subclass_extra_options = OPT_SUPPORT_NEWER_FIRMWARE + 1
else:
debug_print("KoboTouch::migrate_old_settings - Have all options")
settings.update_series = settings.extra_customization[OPT_UPDATE_SERIES_DETAILS]
settings.modify_css = settings.extra_customization[OPT_MODIFY_CSS]
settings.support_newer_firmware = settings.extra_customization[OPT_SUPPORT_NEWER_FIRMWARE]
debugging_title = settings.extra_customization[OPT_DEBUGGING_TITLE]
start_subclass_extra_options = OPT_DEBUGGING_TITLE + 1
settings.debugging_title = debugging_title if isinstance(debugging_title, string_or_bytes) else ''
settings.update_device_metadata = settings.update_series
settings.extra_customization = settings.extra_customization[start_subclass_extra_options:]
return settings
def is_debugging_title(self, title):
if not DEBUG:
return False
# debug_print("KoboTouch:is_debugging - title=", title)
if not self.debugging_title and not self.debugging_title == '':
self.debugging_title = self.get_debugging_title()
try:
is_debugging = len(self.debugging_title) > 0 and title.lower().find(self.debugging_title.lower()) >= 0 or len(title) == 0
except:
debug_print(("KoboTouch::is_debugging_title - Exception checking debugging title for title '{}'.").format(title))
is_debugging = False
return is_debugging
def dump_bookshelves(self, connection):
if not (DEBUG and self.supports_bookshelves and False):
return
debug_print('KoboTouch:dump_bookshelves - start')
shelf_query = 'SELECT * FROM Shelf'
shelfcontent_query = 'SELECT * FROM ShelfContent'
placeholder = '%s'
cursor = connection.cursor()
prints('\nBookshelves on device:')
cursor.execute(shelf_query)
i = 0
for row in cursor:
placeholders = ', '.join(placeholder for unused in row)
prints(placeholders%row)
i += 1
if i == 0:
prints("No shelves found!!")
else:
prints("Number of shelves=%d"%i)
prints('\nBooks on shelves on device:')
cursor.execute(shelfcontent_query)
i = 0
for row in cursor:
placeholders = ', '.join(placeholder for unused in row)
prints(placeholders%row)
i += 1
if i == 0:
prints("No books are on any shelves!!")
else:
prints("Number of shelved books=%d"%i)
cursor.close()
debug_print('KoboTouch:dump_bookshelves - end')
def __str__(self, *args, **kwargs):
options = ', '.join([f'{x.name}: {self.get_pref(x.name)}' for x in self._config().preferences])
return f"Driver:{self.name}, Options - {options}"
if __name__ == '__main__':
dev = KOBOTOUCH(None)
dev.startup()
try:
dev.initialize()
from calibre.devices.scanner import DeviceScanner
scanner = DeviceScanner()
scanner.scan()
devs = scanner.devices
# debug_print("unit test: devs.__class__=", devs.__class__)
# debug_print("unit test: devs.__class__=", devs.__class__.__name__)
debug_print("unit test: devs=", devs)
debug_print("unit test: dev=", dev)
# cd = dev.detect_managed_devices(devs)
# if cd is None:
# raise ValueError('Failed to detect KOBOTOUCH device')
dev.set_progress_reporter(prints)
# dev.open(cd, None)
# dev.filesystem_cache.dump()
print('Prefix for main memory:', dev.dbversion)
finally:
dev.shutdown()