Kobo driver: Improved configuration dialog for the Kobo Touch device driver

This commit is contained in:
Kovid Goyal 2016-06-22 17:22:29 +05:30
commit dcc2e3bc26
4 changed files with 1251 additions and 201 deletions

View File

@ -182,7 +182,7 @@ def print_basic_debug_info(out=None):
pass
from calibre.customize.ui import has_external_plugins, initialized_plugins
if has_external_plugins():
names = (p.name for p in initialized_plugins() if getattr(p, 'plugin_path', None) is not None)
names = ('{0} {1}'.format(p.name, p.version) for p in initialized_plugins() if getattr(p, 'plugin_path', None) is not None)
out('Successfully initialized third party plugins:', ' && '.join(names))
def run_debug_gui(logpath):

View File

@ -145,6 +145,11 @@ class KOBO(USBMS):
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
@ -311,7 +316,7 @@ class KOBO(USBMS):
'BookID is Null %(previews)s %(recomendations)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 opts.extra_customization[self.OPT_SHOW_PREVIEWS] == False else '',
if not self.show_previews else '',
recomendations=' and IsDownloaded in (\'true\', 1)'
if opts.extra_customization[self.OPT_SHOW_RECOMMENDATIONS] == False else '')
elif self.dbversion >= 16 and self.dbversion < 33:
@ -709,7 +714,7 @@ class KOBO(USBMS):
# debug_print("KOBO:book_from_path - title=%s"%title)
from calibre.ebooks.metadata import MetaInformation
if cls.settings().read_metadata or cls.MUST_READ_METADATA:
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
@ -898,12 +903,15 @@ class KOBO(USBMS):
# debug_print('Finished update_device_database_collections', collections_attributes)
def get_collections_attributes(self):
collections = []
opts = self.settings()
if opts.extra_customization and len(opts.extra_customization[self.OPT_COLLECTIONS]) > 0:
collections = [x.lower().strip() for x in opts.extra_customization[self.OPT_COLLECTIONS].split(',')]
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]
def sync_booklists(self, booklists, end_session=True):
debug_print('KOBO:sync_booklists - start')
paths = self.get_device_paths()
@ -1045,6 +1053,50 @@ class KOBO(USBMS):
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
@ -1286,83 +1338,9 @@ class KOBOTOUCH(KOBO):
KOBO_EXTRA_CSSFILE = 'kobo_extra.css'
EXTRA_CUSTOMIZATION_MESSAGE = [
_('The Kobo from firmware V2.0.0 supports bookshelves.'
' These are created on the Kobo. ' +
'Specify a tags type column for automatic management.'),
_('Create Bookshelves') +
':::'+_('Create new bookshelves on the Kobo if they do not exist. This is only for firmware V2.0.0 or later.'),
_('Delete Empty Bookshelves') +
':::'+_('Delete any empty bookshelves from the Kobo when syncing is finished. This is only for firmware V2.0.0 or later.'),
_('Upload covers for books') +
':::'+_('Upload cover images from the calibre library when sending books to the device.'),
_('Upload Black and White Covers'),
_('Keep cover aspect ratio') +
':::'+_('When uploading covers, do not change the aspect ratio when resizing for the device.'
' This is for firmware versions 2.3.1 and later.'),
_('Show archived books') +
':::'+_('Archived books are listed on the device but need to be downloaded to read.'
' Use this option to show these books and match them with books in the calibre library.'),
_('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 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.'),
_('Set Series information') +
':::'+_('The book lists on the Kobo devices can display series information. '
'This is not read by the device from the sideloaded books. '
'Series information can only be added to the device after the book has been processed by the device. '
'Enable if you wish to set series information.'),
_('Modify CSS') +
':::'+_('This allows addition of user CSS rules and removal of some CSS. '
'When sending a book, the driver adds the contents of {0} to all stylesheets in the ePub. '
'This file is searched for in the root directory of the main memory of the device. '
'As well as this, if the file contains settings for the "orphans" or "widows", '
'these are removed for all styles in the original stylesheet.').format(KOBO_EXTRA_CSSFILE),
_('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. '
'This driver supports firmware V2.x.x and DBVersion up to ') + unicode(supported_dbversion),
_('Title to test when debugging') +
':::'+_('Part of title of a book that can be used when doing some tests for debugging. '
'The test is to see if the string is contained in the title of a book. '
'The better the match, the less extraneous output.'),
]
]
EXTRA_CUSTOMIZATION_DEFAULT = [
u'',
False,
False,
False,
False,
False,
False,
False,
False,
False,
False,
False,
u''
]
OPT_COLLECTIONS = 0
OPT_CREATE_BOOKSHELVES = 1
OPT_DELETE_BOOKSHELVES = 2
OPT_UPLOAD_COVERS = 3
OPT_UPLOAD_GRAYSCALE_COVERS = 4
OPT_KEEP_COVER_ASPECT_RATIO = 5
OPT_SHOW_ARCHIVED_BOOK_RECORDS = 6
OPT_SHOW_PREVIEWS = 7
OPT_SHOW_RECOMMENDATIONS = 8
OPT_UPDATE_SERIES_DETAILS = 9
OPT_MODIFY_CSS = 10
OPT_SUPPORT_NEWER_FIRMWARE = 11
OPT_DEBUGGING_TITLE = 12
]
opts = None
@ -1415,6 +1393,10 @@ class KOBOTOUCH(KOBO):
# ' - 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(KOBOTOUCH, self).initialize()
self.bookshelvelist = []
@ -1426,6 +1408,7 @@ class KOBOTOUCH(KOBO):
def books(self, oncard=None, end_session=True):
debug_print("KoboTouch:books - oncard='%s'"%oncard)
from calibre.ebooks.metadata.meta import path_to_ext
self.debugging_title = self.get_debugging_title()
dummy_bl = self.booklist_class(None, None, None)
@ -1467,11 +1450,11 @@ class KOBOTOUCH(KOBO):
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'])
if opts.extra_customization:
debugging_title = opts.extra_customization[self.OPT_DEBUGGING_TITLE]
debug_print("KoboTouch:books - set_debugging_title to '%s'" % debugging_title)
bl.set_debugging_title(debugging_title)
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))
@ -1658,7 +1641,7 @@ class KOBOTOUCH(KOBO):
def get_bookshelvesforbook(connection, ContentID):
# debug_print("KoboTouch:get_bookshelvesforbook - " + ContentID)
bookshelves = []
if not self.supports_bookshelves():
if not self.supports_bookshelves:
return bookshelves
cursor = connection.cursor()
@ -1717,32 +1700,30 @@ class KOBOTOUCH(KOBO):
" %(previews)s %(recomendations)s )"
" and not ((___ExpirationStatus=3 or ___ExpirationStatus is Null) and ContentType = 6)") % \
dict(
expiry="" if opts.extra_customization[self.OPT_SHOW_ARCHIVED_BOOK_RECORDS] else "and IsDownloaded in ('true', 1)",
previews=" or (Accessibility in (6) and ___UserID <> '')" if opts.extra_customization[self.OPT_SHOW_PREVIEWS] else "",
recomendations=" or (Accessibility in (-1, 4, 6) and ___UserId = '')" if opts.extra_customization[
self.OPT_SHOW_RECOMMENDATIONS] else ""
expiry="" if self.show_archived_books else "and IsDownloaded in ('true', 1)",
previews=" or (Accessibility in (6) and ___UserID <> '')" if self.show_previews else "",
recomendations=" or (Accessibility in (-1, 4, 6) and ___UserId = '')" if self.show_recommendations else ""
)
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 %(recomendations)s )"
" and not ((___ExpirationStatus=3 or ___ExpirationStatus is Null) %(expiry)s)") % \
dict(
expiry=" and ContentType = 6" if opts.extra_customization[self.OPT_SHOW_ARCHIVED_BOOK_RECORDS] else "",
previews=" or (Accessibility in (6) and ___UserID <> '')" if opts.extra_customization[self.OPT_SHOW_PREVIEWS] else "",
recomendations=" or (Accessibility in (-1, 4, 6) and ___UserId = '')" if opts.extra_customization[
self.OPT_SHOW_RECOMMENDATIONS] else ""
expiry=" and ContentType = 6" if self.show_archived_books else "",
previews=" or (Accessibility in (6) and ___UserID <> '')" if self.show_previews else "",
recomendations=" 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 %(recomendations)s and not ((___ExpirationStatus=3 or ___ExpirationStatus is Null) %(expiry)s)') % \
dict(
expiry=' and ContentType = 6' if opts.extra_customization[self.OPT_SHOW_ARCHIVED_BOOK_RECORDS] else '',
previews=' and Accessibility <> 6' if opts.extra_customization[self.OPT_SHOW_PREVIEWS] == False else '',
recomendations=' and IsDownloaded in (\'true\', 1)' if opts.extra_customization[self.OPT_SHOW_RECOMMENDATIONS] == False else ''
expiry=' and ContentType = 6' if self.show_archived_books else '',
previews=' and Accessibility <> 6' if not self.show_previews else '',
recomendations=' 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 opts.extra_customization[self.OPT_SHOW_ARCHIVED_BOOK_RECORDS] else '')
dict(expiry=' and ContentType = 6' if self.show_archived_books else '')
else:
where_clause = ' where BookID is Null'
@ -1884,6 +1865,7 @@ class KOBOTOUCH(KOBO):
def get_extra_css(self):
extra_sheet = None
from cssutils.css import CSSRule
if self.modifying_css():
extra_css_path = os.path.join(self._main_prefix, self.KOBO_EXTRA_CSSFILE)
@ -1898,8 +1880,27 @@ class KOBOTOUCH(KOBO):
except Exception as e:
debug_print("KoboTouch:get_extra_css: Problem parsing extra CSS file {0}".format(extra_css_path))
debug_print("KoboTouch:get_extra_css: Exception {0}".format(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):
from cssutils.css import CSSRule
return [r for r in sheet.cssRules.rulesOfType(css_rule)]
def get_extra_css_rules_widow_orphan(self, sheet):
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)))
@ -1943,7 +1944,7 @@ class KOBOTOUCH(KOBO):
self.set_filesize_in_device_database(connection, contentID, fname)
if not self.copying_covers():
if not self.upload_covers:
imageID = self.imageid_from_contentid(contentID)
self.delete_images(imageID, fname)
connection.commit()
@ -1954,7 +1955,7 @@ class KOBOTOUCH(KOBO):
return result
def _modify_epub(self, file, metadata, container=None):
def _modify_epub(self, book_file, metadata, container=None):
debug_print("KoboTouch:_modify_epub:Processing {0} - {1}".format(metadata.author_sort, metadata.title))
# Currently only modifying CSS, so if no stylesheet, don't do anything
@ -1962,58 +1963,96 @@ class KOBOTOUCH(KOBO):
debug_print("KoboTouch:_modify_epub: no CSS file")
return True
commit_container = False
container, commit_container = self.create_container(book_file, metadata, container)
if not container:
commit_container = True
try:
from calibre.ebooks.oeb.polish.container import get_container
debug_print("KoboTouch:_modify_epub: creating container")
container = get_container(file)
container.css_preprocessor = DummyCSSPreProcessor()
except Exception as e:
debug_print("KoboTouch:_modify_epub: exception from get_container {0} - {1}".format(metadata.author_sort, metadata.title))
debug_print("KoboTouch:_modify_epub: exception is: {0}".format(e))
return False
else:
debug_print("KoboTouch:_modify_epub: received container")
return False
from calibre.ebooks.oeb.base import OEB_STYLES
is_dirty = False
for cssname, mt in container.mime_map.iteritems():
if mt in OEB_STYLES:
newsheet = container.parsed(cssname)
oldrules = len(newsheet.cssRules)
# remove any existing @page rules in epub css
# if css to be appended contains an @page rule
if self.extra_sheet and len([r for r in self.extra_sheet if r.type == r.PAGE_RULE]):
page_rules = [r for r in newsheet if r.type == r.PAGE_RULE]
if len(page_rules) > 0:
debug_print("KoboTouch:_modify_epub:Removing existing @page rules")
for rule in page_rules:
rule.style = ''
# remove any existing widow/orphan settings in epub css
# if css to be appended contains a widow/orphan rule or we there is no extra CSS file
if (len([r for r in self.extra_sheet if r.type == r.STYLE_RULE
and (r.style['widows'] or r.style['orphans'])]) > 0):
widow_orphan_rules = [r for r in newsheet if r.type == r.STYLE_RULE
and (r.style['widows'] or r.style['orphans'])]
if len(widow_orphan_rules) > 0:
debug_print("KoboTouch:_modify_epub:Removing existing widows/orphans attribs")
for rule in widow_orphan_rules:
rule.style.removeProperty('widows')
rule.style.removeProperty('orphans')
# append all rules from kobo extra css stylesheet
for addrule in [r for r in self.extra_sheet.cssRules]:
newsheet.insertRule(addrule, len(newsheet.cssRules))
debug_print("KoboTouch:_modify_epub:CSS rules {0} -> {1} ({2})".format(oldrules, len(newsheet.cssRules), cssname))
container.dirty(cssname)
# future css mods may be epub/kepub specific, so pass file extension arg
fileext = os.path.splitext(book_file)[-1].lower()
debug_print("KoboTouch:_modify_epub: Modifying {0}".format(cssname))
if self._modify_stylesheet(newsheet, fileext):
debug_print("KoboTouch:_modify_epub:CSS rules {0} -> {1} ({2})".format(oldrules, len(newsheet.cssRules), cssname))
container.dirty(cssname)
is_dirty = True
if commit_container:
debug_print("KoboTouch:_modify_epub: committing container.")
os.unlink(file)
container.commit(file)
self.commit_container(container, is_dirty)
return True
def _modify_stylesheet(self, sheet, fileext, is_dirty=False):
from cssutils.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("KoboTouch:create_container: exception from get_container {0} - {1}".format(metadata.author_sort, metadata.title))
debug_print("KoboTouch:create_container: exception is: {0}".format(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:
pass
def delete_via_sql(self, ContentID, ContentType):
imageId = super(KOBOTOUCH, self).delete_via_sql(ContentID, ContentType)
@ -2148,7 +2187,6 @@ class KOBOTOUCH(KOBO):
"Closed": 3,
"Shortlist": 4,
"Archived": 5,
# "Preview":99, # Unsupported as we don't want to change it
}
# Define lists for the ReadStatus
@ -2163,33 +2201,16 @@ class KOBOTOUCH(KOBO):
"Recommendation":4,
"Deleted":1,
}
# specialshelveslist = {
# "Shortlist":1,
# "Wishlist":2,
# }
# debug_print('KoboTouch:update_device_database_collections - collections_attributes=', collections_attributes)
opts = self.settings()
if opts.extra_customization:
create_bookshelves = opts.extra_customization[self.OPT_CREATE_BOOKSHELVES] and self.supports_bookshelves()
delete_empty_shelves = opts.extra_customization[self.OPT_DELETE_BOOKSHELVES] and self.supports_bookshelves()
update_series_details = opts.extra_customization[self.OPT_UPDATE_SERIES_DETAILS] and self.supports_series()
debugging_title = opts.extra_customization[self.OPT_DEBUGGING_TITLE]
debug_print("KoboTouch:update_device_database_collections - set_debugging_title to '%s'" % debugging_title)
booklists.set_debugging_title(debugging_title)
else:
delete_empty_shelves = False
create_bookshelves = False
update_series_details = False
create_bookshelves = self.create_bookshelves
delete_empty_shelves = self.delete_empty_shelves
update_series_details = self.update_series_details
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)
opts = self.settings()
if opts.extra_customization:
create_bookshelves = opts.extra_customization[self.OPT_CREATE_BOOKSHELVES] and self.supports_bookshelves()
delete_empty_shelves = opts.extra_customization[self.OPT_DELETE_BOOKSHELVES] and self.supports_bookshelves()
else:
delete_empty_shelves = False
bookshelf_attribute = len(collections_attributes)
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)
@ -2200,13 +2221,12 @@ class KOBOTOUCH(KOBO):
# and the removal of the last book would not occur
import sqlite3 as sqlite
with closing(sqlite.connect(self.normalize_path(self._main_prefix +
'.kobo/KoboReader.sqlite'))) as connection:
with closing(sqlite.connect(self.device_database_path())) as connection:
# return bytestrings if the content cannot the decoded as unicode
connection.text_factory = lambda x: unicode(x, "utf-8", "ignore")
if collections:
if self.manage_collections and collections:
# debug_print("KoboTouch:update_device_database_collections - length collections=" + unicode(len(collections)))
# Need to reset the collections outside the particular loops
@ -2248,7 +2268,7 @@ class KOBOTOUCH(KOBO):
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.bookshelvelist and self.supports_bookshelves():
if 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:
@ -2265,7 +2285,7 @@ class KOBOTOUCH(KOBO):
if show_debug:
debug_print(' Have an older version shortlist - %s'%book.title)
# Manage FavouritesIndex/Shortlist
if not self.supports_bookshelves():
if not self.supports_bookshelves:
if show_debug:
debug_print(' and about to set it - %s'%book.title)
self.set_favouritesindex(connection, book.contentID)
@ -2293,7 +2313,7 @@ class KOBOTOUCH(KOBO):
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() or self.supports_series()) and (bookshelf_attribute or update_series_details):
if (self.supports_bookshelves and self.manage_collections or self.supports_series()) and (bookshelf_attribute or update_series_details):
debug_print("KoboTouch:update_device_database_collections - managing bookshelves and series.")
self.series_set = 0
@ -2306,7 +2326,7 @@ class KOBOTOUCH(KOBO):
debug_print("KoboTouch:update_device_database_collections - book.title=%s" % book.title)
if update_series_details:
self.set_series(connection, book)
if bookshelf_attribute:
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)
@ -2341,8 +2361,7 @@ class KOBOTOUCH(KOBO):
debug_print("KoboTouch:upload_cover - path='%s' filename='%s' "%(path, filename))
debug_print(" filepath='%s' "%(filepath))
opts = self.settings()
if not self.copying_covers():
if not self.upload_covers:
# Building thumbnails disabled
# debug_print('KoboTouch: not uploading cover')
return
@ -2351,14 +2370,9 @@ class KOBOTOUCH(KOBO):
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
if not opts.extra_customization[self.OPT_UPLOAD_GRAYSCALE_COVERS]:
uploadgrayscale = False
else:
uploadgrayscale = True
# debug_print('KoboTouch: uploading cover')
try:
self._upload_cover(path, filename, metadata, filepath, uploadgrayscale, self.keep_cover_aspect())
self._upload_cover(path, filename, metadata, filepath, self.upload_grayscale, self.keep_cover_aspect)
except Exception as e:
debug_print('KoboTouch: FAILED to upload cover=%s Exception=%s'%(filepath, str(e)))
@ -2589,7 +2603,7 @@ class KOBOTOUCH(KOBO):
# debug_print('KoboTouch:get_bookshelflist')
bookshelves = []
if not self.supports_bookshelves():
if not self.supports_bookshelves:
return bookshelves
query = 'SELECT Name FROM Shelf WHERE _IsDeleted = "false"'
@ -2691,7 +2705,7 @@ class KOBOTOUCH(KOBO):
def remove_from_bookshelves(self, connection, oncard, ContentID=None, bookshelves=None):
debug_print('KoboTouch:remove_from_bookshelf ContentID=', ContentID)
if not self.supports_bookshelves():
if not self.supports_bookshelves:
return
query = 'DELETE FROM ShelfContent'
@ -2762,10 +2776,72 @@ class KOBOTOUCH(KOBO):
if show_debug:
debug_print("KoboTouch:set_series - end")
@classmethod
def config_widget(self):
# TODO: Cleanup the following
self.current_friendly_name = self.gui_name
from calibre.devices.kobo.kobotouch_config import KOBOTOUCHConfig
return KOBOTOUCHConfig(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
)
@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)
opts = cls.settings()
try:
return getattr(opts, key)
except:
debug_print("KoboTouch::get_prefs - probably an extra_customization:", key)
return None
@classmethod
def save_settings(cls, config_widget):
config_widget.commit()
@classmethod
def save_template(cls):
return cls.settings().save_template
@classmethod
def _config(cls):
c = super(KOBOTOUCH, cls)._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('upload_covers', default=False)
c.add_opt('keep_cover_aspect', default=False)
c.add_opt('upload_grayscale', 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_device_metadata', default=True)
c.add_opt('modify_css', default=False)
c.add_opt('support_newer_firmware', default=False)
c.add_opt('debugging_title', default='')
return c
@classmethod
def settings(cls):
opts = cls._config().parse()
if isinstance(cls.EXTRA_CUSTOMIZATION_DEFAULT, list):
if isinstance(cls.EXTRA_CUSTOMIZATION_DEFAULT, list) and len(cls.EXTRA_CUSTOMIZATION_DEFAULT) > 0:
if opts.extra_customization is None:
opts.extra_customization = []
if not isinstance(opts.extra_customization, list):
@ -2782,6 +2858,10 @@ class KOBOTOUCH(KOBO):
else:
extra_customization.append(opts.extra_customization[i - extra_options_offset])
opts.extra_customization = extra_customization
if opts.extra_customization:
opts = cls.migrate_old_settings(opts)
cls.opts = opts
return opts
def isAura(self):
@ -2827,24 +2907,73 @@ class KOBOTOUCH(KOBO):
self.__class__.gui_name = device_name
return device_name
def copying_covers(self):
opts = self.settings()
return opts.extra_customization[self.OPT_UPLOAD_COVERS] or opts.extra_customization[self.OPT_KEEP_COVER_ASPECT_RATIO]
@property
def manage_collections(self):
return self.get_pref('manage_collections')
@property
def create_collections(self):
return self.get_pref('create_collections')
@property
def collections_columns(self):
return self.get_pref('collections_columns')
@property
def delete_empty_collections(self):
return self.get_pref('delete_empty_collections')
@property
def upload_covers(self):
return self.get_pref('upload_covers')
@property
def keep_cover_aspect(self):
opts = self.settings()
return opts.extra_customization[self.OPT_KEEP_COVER_ASPECT_RATIO]
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')
def modifying_epub(self):
return self.modifying_css()
def modifying_css(self):
opts = self.settings()
return opts.extra_customization[self.OPT_MODIFY_CSS]
return self.get_pref('modify_css')
@property
def create_bookshelves(self):
return self.get_pref('create_collections') and self.supports_bookshelves
@property
def delete_empty_shelves(self):
return self.get_pref('delete_empty_collections') and self.supports_bookshelves
@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()
@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
@ -2869,8 +2998,7 @@ class KOBOTOUCH(KOBO):
# debug_print("KoboTouch:modify_database_check - self.fwversion > self.max_supported_fwversion=", self.fwversion > self.max_supported_fwversion)
if self.dbversion > self.supported_dbversion or self.fwversion > self.max_supported_fwversion:
# Unsupported database
opts = self.settings()
if not opts.extra_customization[self.OPT_SUPPORT_NEWER_FIRMWARE]:
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
@ -2901,21 +3029,77 @@ class KOBOTOUCH(KOBO):
return True
@classmethod
def is_debugging_title(cls, title):
def migrate_old_settings(cls, settings):
debug_print("KoboTouch::migrate_old_settings - start")
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
if len(settings.extra_customization) >= count_options:
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]
settings.update_series = settings.extra_customization[OPT_UPDATE_SERIES_DETAILS]
settings.update_metadata = settings.update_series
settings.modify_css = settings.extra_customization[OPT_MODIFY_CSS]
settings.support_newer_firmware = settings.extra_customization[OPT_SUPPORT_NEWER_FIRMWARE]
settings.debugging_title = settings.extra_customization[OPT_DEBUGGING_TITLE]
settings.extra_customization = settings.extra_customization[count_options + 1:]
return settings
def is_debugging_title(self, title):
if not DEBUG:
return False
# debug_print("KoboTouch:is_debugging - title=", title)
is_debugging = False
opts = cls.settings()
# debug_print("KoboTouch:is_debugging - title=", title)
if opts.extra_customization:
debugging_title = opts.extra_customization[cls.OPT_DEBUGGING_TITLE]
is_debugging = len(debugging_title) > 0 and title.lower().find(debugging_title.lower()) >= 0 or len(title) == 0
if not self.debugging_title and not self.debugging_title == '':
self.debugging_title = self.get_debugging_title()
is_debugging = len(self.debugging_title) > 0 and title.lower().find(self.debugging_title.lower()) >= 0 or len(title) == 0
return is_debugging
def dump_bookshelves(self, connection):
if not (DEBUG and self.supports_bookshelves() and False):
if not (DEBUG and self.supports_bookshelves and False):
return
debug_print('KoboTouch:dump_bookshelves - start')
@ -2952,3 +3136,29 @@ class KOBOTOUCH(KOBO):
cursor.close()
debug_print('KoboTouch:dump_bookshelves - end')
def __str__(self, *args, **kwargs):
options = ', '.join(['%s: %s' % (x.name, self.get_pref(x.name)) for x in self._config().preferences])
return u"Driver:%s, Options - %s" % (self.name, 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()

View File

@ -0,0 +1,450 @@
#!/usr/bin/env python2
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import textwrap
from PyQt5.Qt import (QLabel, QGridLayout, QLineEdit, QVBoxLayout,
QDialog, QDialogButtonBox, QCheckBox)
from calibre.gui2.device_drivers.tabbed_device_config import TabbedDeviceConfig, DeviceConfigTab, DeviceOptionsGroupBox
from calibre.devices.usbms.driver import debug_print
def wrap_msg(msg):
return textwrap.fill(msg.strip(), 100)
def setToolTipFor(widget, tt):
widget.setToolTip(wrap_msg(tt))
def create_checkbox(title, tt, state):
cb = QCheckBox(title)
cb.setToolTip(wrap_msg(tt))
cb.setChecked(bool(state))
return cb
class KOBOTOUCHConfig(TabbedDeviceConfig):
def __init__(self, device_settings, all_formats, supports_subdirs,
must_read_metadata, supports_use_author_sort,
extra_customization_message, device, extra_customization_choices=None, parent=None):
super(KOBOTOUCHConfig, self).__init__(device_settings, all_formats, supports_subdirs,
must_read_metadata, supports_use_author_sort,
extra_customization_message, device, extra_customization_choices, parent)
self.device_settings = device_settings
self.all_formats = all_formats
self.supports_subdirs = supports_subdirs
self.must_read_metadata = must_read_metadata
self.supports_use_author_sort = supports_use_author_sort
self.extra_customization_message = extra_customization_message
self.extra_customization_choices = extra_customization_choices
self.tab1 = Tab1Config(self, self.device)
self.tab2 = Tab2Config(self, self.device)
self.addDeviceTab(self.tab1, _("Collections, Covers && Uploads"))
self.addDeviceTab(self.tab2, _('Metadata, On Device && Advanced'))
def get_pref(self, key):
return self.device.get_pref(key)
@property
def device(self):
return self._device()
def validate(self):
if hasattr(self, 'formats'):
if not self.formats.validate():
return False
if not self.template.validate():
return False
return True
@property
def book_uploads_options(self):
return self.tab1.book_uploads_options
@property
def collections_options(self):
return self.tab1.collections_options
@property
def cover_options(self):
return self.tab1.covers_options
@property
def device_list_options(self):
return self.tab2.device_list_options
@property
def advanced_options(self):
return self.tab2.advanced_options
@property
def metadata_options(self):
return self.tab2.metadata_options
def commit(self):
debug_print("KOBOTOUCHConfig::commit: start")
p = super(KOBOTOUCHConfig, self).commit()
p['manage_collections'] = self.manage_collections
p['create_collections'] = self.create_collections
p['collections_columns'] = self.collections_columns
p['delete_empty_collections'] = self.delete_empty_collections
p['upload_covers'] = self.upload_covers
p['keep_cover_aspect'] = self.keep_cover_aspect
p['upload_grayscale'] = self.upload_grayscale
p['show_recommendations'] = self.show_recommendations
p['show_previews'] = self.show_previews
p['show_archived_books'] = self.show_archived_books
p['update_series'] = self.update_series
p['modify_css'] = self.modify_css
p['support_newer_firmware'] = self.support_newer_firmware
p['debugging_title'] = self.debugging_title
return p
class Tab1Config(DeviceConfigTab): # {{{
def __init__(self, parent, device):
super(Tab1Config, self).__init__(parent)
self.l = QVBoxLayout(self)
self.setLayout(self.l)
self.collections_options = CollectionsGroupBox(self, device)
self.l.addWidget(self.collections_options)
self.addDeviceWidget(self.collections_options)
self.covers_options = CoversGroupBox(self, device)
self.l.addWidget(self.covers_options)
self.addDeviceWidget(self.covers_options)
self.book_uploads_options = BookUploadsGroupBox(self, device)
self.l.addWidget(self.book_uploads_options)
self.addDeviceWidget(self.book_uploads_options)
# }}}
class Tab2Config(DeviceConfigTab): # {{{
def __init__(self, parent, device):
super(Tab2Config, self).__init__(parent)
self.l = QVBoxLayout(self)
self.setLayout(self.l)
self.metadata_options = MetadataGroupBox(self, device)
self.l.addWidget(self.metadata_options)
self.addDeviceWidget(self.metadata_options)
self.device_list_options = DeviceListGroupBox(self, device)
self.l.addWidget(self.device_list_options)
self.addDeviceWidget(self.device_list_options)
self.advanced_options = AdvancedGroupBox(self, device)
self.l.addWidget(self.advanced_options)
self.addDeviceWidget(self.advanced_options)
# }}}
class BookUploadsGroupBox(DeviceOptionsGroupBox):
def __init__(self, parent, device):
super(BookUploadsGroupBox, self).__init__(parent, device)
self.setTitle(_("Book Uploading"))
self.options_layout = QGridLayout()
self.options_layout.setObjectName("options_layout")
self.setLayout(self.options_layout)
self.modify_css_checkbox = create_checkbox(
_("Modify CSS"),
_('This allows addition of user CSS rules and removal of some CSS. '
'When sending a book, the driver adds the contents of {0} to all stylesheets in the ePub. '
'This file is searched for in the root directory of the main memory of the device. '
'As well as this, if the file contains settings for the "orphans" or "widows", '
'these are removed for all styles in the original stylesheet.').format(device.KOBO_EXTRA_CSSFILE),
device.get_pref('modify_css')
)
self.options_layout.addWidget(self.modify_css_checkbox, 0, 0, 1, 2)
self.options_layout.setRowStretch(1, 1)
@property
def modify_css(self):
return self.modify_css_checkbox.isChecked()
class CollectionsGroupBox(DeviceOptionsGroupBox):
def __init__(self, parent, device):
super(CollectionsGroupBox, self).__init__(parent, device)
self.setTitle(_("Collections"))
self.options_layout = QGridLayout()
self.options_layout.setObjectName("options_layout")
self.setLayout(self.options_layout)
self.setCheckable(True)
self.setChecked(device.get_pref('manage_collections'))
self.setToolTip(wrap_msg(_('Create new bookshelves on the Kobo if they do not exist. This is only for firmware V2.0.0 or later.')))
self.collections_columns_label = QLabel(_('Collections Columns'))
self.collections_columns_edit = QLineEdit(self)
self.collections_columns_edit.setToolTip(_('The Kobo from firmware V2.0.0 supports bookshelves.'
' These are created on the Kobo. ' +
'Specify a tags type column for automatic management.'))
self.collections_columns_edit.setText(device.get_pref('collections_columns'))
self.create_collections_checkbox = create_checkbox(
_("Create Collections"),
_('Create new bookshelves on the Kobo if they do not exist. This is only for firmware V2.0.0 or later.'),
device.get_pref('create_collections')
)
self.delete_empty_collections_checkbox = create_checkbox(
_('Delete Empty Bookshelves'),
_('Delete any empty bookshelves from the Kobo when syncing is finished. This is only for firmware V2.0.0 or later.'),
device.get_pref('delete_empty_collections')
)
self.options_layout.addWidget(self.collections_columns_label, 1, 0, 1, 1)
self.options_layout.addWidget(self.collections_columns_edit, 1, 1, 1, 1)
self.options_layout.addWidget(self.create_collections_checkbox, 2, 0, 1, 2)
self.options_layout.addWidget(self.delete_empty_collections_checkbox, 3, 0, 1, 2)
self.options_layout.setRowStretch(4, 1)
@property
def manage_collections(self):
return self.isChecked()
@property
def collections_columns(self):
return self.collections_columns_edit.text().strip()
@property
def create_collections(self):
return self.create_collections_checkbox.isChecked()
@property
def delete_empty_collections(self):
return self.delete_empty_collections_checkbox.isChecked()
class CoversGroupBox(DeviceOptionsGroupBox):
def __init__(self, parent, device):
super(CoversGroupBox, self).__init__(parent, device)
self.setTitle(_("Upload covers"))
self.options_layout = QGridLayout()
self.options_layout.setObjectName("options_layout")
self.setLayout(self.options_layout)
self.setCheckable(True)
self.setChecked(device.get_pref('upload_covers'))
self.setToolTip(wrap_msg(_('Upload cover images from the calibre library when sending books to the device.')))
self.upload_grayscale_checkbox = create_checkbox(
_('Upload Black and White Covers'),
_('Convert covers to Black and White when uploading'),
device.get_pref('upload_grayscale')
)
self.keep_cover_aspect_checkbox = create_checkbox(
_('Keep cover aspect ratio'),
_('When uploading covers, do not change the aspect ratio when resizing for the device.'
' This is for firmware versions 2.3.1 and later.'),
device.get_pref('keep_cover_aspect'))
self.options_layout.addWidget(self.keep_cover_aspect_checkbox, 0, 0, 1, 1)
self.options_layout.addWidget(self.upload_grayscale_checkbox, 1, 0, 1, 1)
self.options_layout.setRowStretch(2, 1)
@property
def upload_covers(self):
return self.isChecked()
@property
def upload_grayscale(self):
return self.upload_grayscale_checkbox.isChecked()
@property
def keep_cover_aspect(self):
return self.keep_cover_aspect_checkbox.isChecked()
class DeviceListGroupBox(DeviceOptionsGroupBox):
def __init__(self, parent, device):
super(DeviceListGroupBox, self).__init__(parent, device)
self.setTitle(_("Show as on device"))
self.options_layout = QGridLayout()
self.options_layout.setObjectName("options_layout")
self.setLayout(self.options_layout)
self.show_recommendations_checkbox = create_checkbox(
_("Show Recommendations"),
_('Kobo 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.'),
device.get_pref('show_recommendations')
)
self.show_archived_books_checkbox = create_checkbox(
_("Show archived books"),
_('Archived books are listed on the device but need to be downloaded to read.'
' Use this option to show these books and match them with books in the calibre library.'),
device.get_pref('show_archived_books')
)
self.show_previews_checkbox = create_checkbox(
_('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.'),
device.get_pref('show_previews')
)
self.options_layout.addWidget(self.show_recommendations_checkbox, 0, 0, 1, 1)
self.options_layout.addWidget(self.show_archived_books_checkbox, 1, 0, 1, 1)
self.options_layout.addWidget(self.show_previews_checkbox, 2, 0, 1, 1)
self.options_layout.setRowStretch(3, 1)
@property
def show_recommendations(self):
return self.show_recommendations_checkbox.isChecked()
@property
def show_archived_books(self):
return self.show_archived_books_checkbox.isChecked()
@property
def show_previews(self):
return self.show_previews_checkbox.isChecked()
class AdvancedGroupBox(DeviceOptionsGroupBox):
def __init__(self, parent, device):
super(AdvancedGroupBox, self).__init__(parent, device, _("Advanced Options"))
# self.setTitle(_("Advanced Options"))
self.options_layout = QGridLayout()
self.options_layout.setObjectName("options_layout")
self.setLayout(self.options_layout)
self.support_newer_firmware_checkbox = create_checkbox(
_("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. '
'This driver supports firmware V2.x.x and DBVersion up to ') + unicode(
device.supported_dbversion), device.get_pref('support_newer_firmware')
)
self.debugging_title_checkbox = create_checkbox(
_("Title to test when debugging"),
_('Part of title of a book that can be used when doing some tests for debugging. '
'The test is to see if the string is contained in the title of a book. '
'The better the match, the less extraneous output.'),
device.get_pref('debugging_title')
)
self.debugging_title_label = QLabel(_('Title to test when debugging'))
self.debugging_title_edit = QLineEdit(self)
self.debugging_title_edit.setToolTip(_('Part of title of a book that can be used when doing some tests for debugging. '
'The test is to see if the string is contained in the title of a book. '
'The better the match, the less extraneous output.'))
self.debugging_title_edit.setText(device.get_pref('debugging_title'))
self.debugging_title_label.setBuddy(self.debugging_title_edit)
self.options_layout.addWidget(self.support_newer_firmware_checkbox, 0, 0, 1, 2)
self.options_layout.addWidget(self.debugging_title_label, 1, 0, 1, 1)
self.options_layout.addWidget(self.debugging_title_edit, 1, 1, 1, 1)
self.options_layout.setRowStretch(2, 2)
@property
def support_newer_firmware(self):
return self.support_newer_firmware_checkbox.isChecked()
@property
def debugging_title(self):
return self.debugging_title_edit.text().strip()
class MetadataGroupBox(DeviceOptionsGroupBox):
def __init__(self, parent, device):
super(MetadataGroupBox, self).__init__(parent, device)
self.setTitle(_("Update metadata on the device"))
self.options_layout = QGridLayout()
self.options_layout.setObjectName("options_layout")
self.setLayout(self.options_layout)
self.setCheckable(True)
self.setChecked(device.get_pref('update_device_metadata'))
self.setToolTip(wrap_msg(_('Update the metadata on the device when it is connected. '
'Be careful when doing this as it will take time and could make the initial connection take a long time.')))
self.update_series_checkbox = create_checkbox(
_("Set Series information"),
_('The book lists on the Kobo devices can display series information. '
'This is not read by the device from the sideloaded books. '
'Series information can only be added to the device after the book has been processed by the device. '
'Enable if you wish to set series information.'),
device.get_pref('update_series')
)
self.options_layout.addWidget(self.update_series_checkbox, 0, 0, 1, 1)
self.options_layout.setRowStretch(1, 1)
@property
def update_series(self):
return self.update_series_checkbox.isChecked()
@property
def update_device_metadata(self):
return self.isChecked()
if __name__ == '__main__':
from calibre.gui2 import Application
from calibre.devices.kobo.driver import KOBOTOUCH
from calibre.devices.scanner import DeviceScanner
s = DeviceScanner()
s.scan()
app = Application([])
debug_print("KOBOTOUCH:", KOBOTOUCH)
dev = KOBOTOUCH(None)
# dev.startup()
# cd = dev.detect_managed_devices(s.devices)
# dev.open(cd, 'test')
cw = dev.config_widget()
d = QDialog()
d.l = QVBoxLayout()
d.setLayout(d.l)
d.l.addWidget(cw)
bb = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel)
d.l.addWidget(bb)
bb.accepted.connect(d.accept)
bb.rejected.connect(d.reject)
if d.exec_() == d.Accepted:
cw.commit()
dev.shutdown()

View File

@ -0,0 +1,390 @@
#!/usr/bin/env python2
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai
from __future__ import (unicode_literals, # division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import weakref, textwrap
from PyQt5.Qt import (
QWidget, QLabel, QTabWidget, QGridLayout, QLineEdit, QVBoxLayout,
QGroupBox, QComboBox, QSizePolicy, QDialog, QDialogButtonBox, QCheckBox,
QSpacerItem)
from calibre.ebooks import BOOK_EXTENSIONS
from calibre.gui2.device_drivers.mtp_config import (FormatsConfig, TemplateConfig)
from calibre.devices.usbms.driver import debug_print
def wrap_msg(msg):
return textwrap.fill(msg.strip(), 100)
def setToolTipFor(widget, tt):
widget.setToolTip(wrap_msg(tt))
def create_checkbox(title, tt, state):
cb = QCheckBox(title)
cb.setToolTip(wrap_msg(tt))
cb.setChecked(bool(state))
return cb
class TabbedDeviceConfig(QTabWidget):
"""
This is a generic Tabbed Device config widget. It designed for devices with more
complex configuration. But, it is backwards compatible to the standard device
configuration widget.
The configuration made up of two default tabs plus extra tabs as needed for the
device. The extra tabs are defined as part of the subclass of this widget for
the device.
The two default tabs are the "File Formats" and "Extra Customization". These
tabs are the same as the two sections of the standard device configuration
widget. The second of these tabs will only be created if the device driver has
extra configuration options. All options on these tabs work the same way as for
the standard device configuration widget.
When implementing a subclass for a device driver, create tabs, subclassed from
DeviceConfigTab, for each set of options. Within the tabs, group boxes, subclassed
from DeviceOptionsGroupBox, are created to further group the options. The group
boxes can be coded to support any control type and dependencies between them.
"""
def __init__(self, device_settings, all_formats, supports_subdirs,
must_read_metadata, supports_use_author_sort,
extra_customization_message, device,
extra_customization_choices=None, parent=None):
QTabWidget.__init__(self, parent)
self._device = weakref.ref(device)
self.device_settings = device_settings
self.all_formats = set(all_formats)
self.supports_subdirs = supports_subdirs
self.must_read_metadata = must_read_metadata
self.supports_use_author_sort = supports_use_author_sort
self.extra_customization_message = extra_customization_message
self.extra_customization_choices = extra_customization_choices
try:
self.device_name = device.get_gui_name()
except TypeError:
self.device_name = getattr(device, 'gui_name', None) or _('Device')
if device.USER_CAN_ADD_NEW_FORMATS:
self.all_formats = set(self.all_formats) | set(BOOK_EXTENSIONS)
self.base = QWidget(self)
# self.insertTab(0, self.base, _('Configure %s') % self.device.current_friendly_name)
self.insertTab(0, self.base, _("File Formats"))
l = self.base.l = QGridLayout(self.base)
self.base.setLayout(l)
self.formats = FormatsConfig(self.all_formats, device_settings.format_map)
if device.HIDE_FORMATS_CONFIG_BOX:
self.formats.hide()
self.opt_use_subdirs = create_checkbox(
_("Use sub-directories"),
_('Place files in sub-directories if the device supports them'),
device_settings.use_subdirs
)
self.opt_read_metadata = create_checkbox(
_("Read metadata from files on device"),
_('Read metadata from files on device'),
device_settings.read_metadata
)
self.template = TemplateConfig(device_settings.save_template)
self.opt_use_author_sort = create_checkbox(
_("Use author sort for author"),
_("Use author sort for author"),
device_settings.read_metadata
)
self.opt_use_author_sort.setObjectName("opt_use_author_sort")
self.base.la = la = QLabel(_(
'Choose the formats to send to the %s')%self.device_name)
la.setWordWrap(True)
l.addWidget(la, 1, 0, 1, 1)
l.addWidget(self.formats, 2, 0, 1, 1)
l.addWidget(self.opt_read_metadata, 3, 0, 1, 1)
l.addWidget(self.opt_use_subdirs, 4, 0, 1, 1)
l.addWidget(self.opt_use_author_sort, 5, 0, 1, 1)
l.addWidget(self.template, 6, 0, 1, 1)
l.setRowStretch(2, 10)
if device.HIDE_FORMATS_CONFIG_BOX:
self.formats.hide()
if supports_subdirs:
self.opt_use_subdirs.setChecked(device_settings.use_subdirs)
else:
self.opt_use_subdirs.hide()
if not must_read_metadata:
self.opt_read_metadata.setChecked(device_settings.read_metadata)
else:
self.opt_read_metadata.hide()
if supports_use_author_sort:
self.opt_use_author_sort.setChecked(device_settings.use_author_sort)
else:
self.opt_use_author_sort.hide()
self.extra_tab = ExtraCustomization(self.extra_customization_message,
self.extra_customization_choices,
self.device_settings)
# Only display the extra customization tab if there are options on it.
if self.extra_tab.has_extra_customizations:
self.addTab(self.extra_tab, _('Extra Customization'))
self.setCurrentIndex(0)
def addDeviceTab(self, tab, label):
'''
This is used to add a new tab for the device config. The new tab will always be added
as immediately before the "Extra Customization" tab.
'''
extra_tab_pos = self.indexOf(self.extra_tab)
self.insertTab(extra_tab_pos, tab, label)
def __getattr__(self, attr_name):
"If the object doesn't have an attribute, then check each tab."
try:
return super(TabbedDeviceConfig, self).__getattr__(attr_name)
except AttributeError as ae:
for i in range(0, self.count()):
atab = self.widget(i)
try:
return getattr(atab, attr_name)
except AttributeError:
pass
raise ae
@property
def device(self):
return self._device()
def format_map(self):
return self.formats.format_map
def use_subdirs(self):
return self.opt_use_subdirs.isChecked()
def read_metadata(self):
return self.opt_read_metadata.isChecked()
def use_author_sort(self):
return self.opt_use_author_sort.isChecked()
@property
def opt_save_template(self):
# Really shouldn't be accessing the template this way
return self.template.t
def text(self):
# Really shouldn't be accessing the template this way
return self.template.t.text()
@property
def opt_extra_customization(self):
return self.extra_tab.opt_extra_customization
@property
def label(self):
return self.opt_save_template
def validate(self):
if hasattr(self, 'formats'):
if not self.formats.validate():
return False
if not self.template.validate():
return False
return True
def commit(self):
debug_print("TabbedDeviceConfig::commit: start")
p = self.device._configProxy()
p['format_map'] = self.formats.format_map
p['use_subdirs'] = self.use_subdirs()
p['read_metadata'] = self.read_metadata()
p['save_template'] = self.template.template
p['extra_customization'] = self.extra_tab.extra_customization()
return p
class DeviceConfigTab(QWidget): # {{{
'''
This is an abstraction for a tab in the configuration. The main reason for it is to
abstract the properties of the configuration tab. When a property is accessed, it
will iterate over all known widgets looking for the property.
'''
def __init__(self, parent=None):
QWidget.__init__(self)
self.parent = parent
self.device_widgets = []
def addDeviceWidget(self, widget):
self.device_widgets.append(widget)
def __getattr__(self, attr_name):
try:
return super(DeviceConfigTab, self).__getattr__(attr_name)
except AttributeError as ae:
for awidget in self.device_widgets:
try:
return getattr(awidget, attr_name)
except AttributeError:
pass
raise ae
class ExtraCustomization(DeviceConfigTab): # {{{
def __init__(self, extra_customization_message, extra_customization_choices, device_settings):
super(ExtraCustomization, self).__init__()
debug_print("ExtraCustomization.__init__ - extra_customization_message=", extra_customization_message)
debug_print("ExtraCustomization.__init__ - extra_customization_choices=", extra_customization_choices)
debug_print("ExtraCustomization.__init__ - device_settings.extra_customization=", device_settings.extra_customization)
debug_print("ExtraCustomization.__init__ - device_settings=", device_settings)
self.extra_customization_message = extra_customization_message
self.l = QVBoxLayout(self)
self.setLayout(self.l)
options_group = QGroupBox(_("Extra driver customization options"), self)
self.l.addWidget(options_group)
self.extra_layout = QGridLayout()
self.extra_layout.setObjectName("extra_layout")
options_group.setLayout(self.extra_layout)
if extra_customization_message:
extra_customization_choices = extra_customization_choices or {}
def parse_msg(m):
msg, _, tt = m.partition(':::') if m else ('', '', '')
return msg.strip(), textwrap.fill(tt.strip(), 100)
if isinstance(extra_customization_message, list):
self.opt_extra_customization = []
if len(extra_customization_message) > 6:
row_func = lambda x, y: ((x/2) * 2) + y
col_func = lambda x: x%2
else:
row_func = lambda x, y: x*2 + y
col_func = lambda x: 0
for i, m in enumerate(extra_customization_message):
label_text, tt = parse_msg(m)
if not label_text:
self.opt_extra_customization.append(None)
continue
if isinstance(device_settings.extra_customization[i], bool):
self.opt_extra_customization.append(QCheckBox(label_text))
self.opt_extra_customization[-1].setToolTip(tt)
self.opt_extra_customization[i].setChecked(bool(device_settings.extra_customization[i]))
elif i in extra_customization_choices:
cb = QComboBox(self)
self.opt_extra_customization.append(cb)
l = QLabel(label_text)
l.setToolTip(tt), cb.setToolTip(tt), l.setBuddy(cb), cb.setToolTip(tt)
for li in sorted(extra_customization_choices[i]):
self.opt_extra_customization[i].addItem(li)
cb.setCurrentIndex(max(0, cb.findText(device_settings.extra_customization[i])))
else:
self.opt_extra_customization.append(QLineEdit(self))
l = QLabel(label_text)
l.setToolTip(tt)
self.opt_extra_customization[i].setToolTip(tt)
l.setBuddy(self.opt_extra_customization[i])
l.setWordWrap(True)
self.opt_extra_customization[i].setText(device_settings.extra_customization[i])
self.opt_extra_customization[i].setCursorPosition(0)
self.extra_layout.addWidget(l, row_func(i + 2, 0), col_func(i))
self.extra_layout.addWidget(self.opt_extra_customization[i],
row_func(i + 2, 1), col_func(i))
spacerItem1 = QSpacerItem(10, 10, QSizePolicy.Minimum, QSizePolicy.Expanding)
self.extra_layout.addItem(spacerItem1, row_func(i + 2 + 2, 1), 0, 1, 2)
self.extra_layout.setRowStretch(row_func(i + 2 + 2, 1), 2)
else:
self.opt_extra_customization = QLineEdit()
label_text, tt = parse_msg(extra_customization_message)
l = QLabel(label_text)
l.setToolTip(tt)
l.setBuddy(self.opt_extra_customization)
l.setWordWrap(True)
if device_settings.extra_customization:
self.opt_extra_customization.setText(device_settings.extra_customization)
self.opt_extra_customization.setCursorPosition(0)
self.opt_extra_customization.setCursorPosition(0)
self.extra_layout.addWidget(l, 0, 0)
self.extra_layout.addWidget(self.opt_extra_customization, 1, 0)
def extra_customization(self):
ec = []
if self.extra_customization_message:
if isinstance(self.extra_customization_message, list):
for i in range(0, len(self.extra_customization_message)):
if self.opt_extra_customization[i] is None:
ec.append(None)
continue
if hasattr(self.opt_extra_customization[i], 'isChecked'):
ec.append(self.opt_extra_customization[i].isChecked())
elif hasattr(self.opt_extra_customization[i], 'currentText'):
ec.append(unicode(self.opt_extra_customization[i].currentText()).strip())
else:
ec.append(unicode(self.opt_extra_customization[i].text()).strip())
else:
ec = unicode(self.opt_extra_customization.text()).strip()
if not ec:
ec = None
return ec
@property
def has_extra_customizations(self):
debug_print("ExtraCustomization::has_extra_customizations - self.extra_customization_message", self.extra_customization_message)
return self.extra_customization_message and len(self.extra_customization_message) > 0
# }}}
class DeviceOptionsGroupBox(QGroupBox):
"""
This is a container for the individual options for a device driver.
"""
def __init__(self, parent, device=None, title=_("Unknown")):
QGroupBox.__init__(self, parent)
self.device = device
self.setTitle(title)
if __name__ == '__main__':
from calibre.gui2 import Application
from calibre.devices.kobo.driver import KOBO
from calibre.devices.scanner import DeviceScanner
s = DeviceScanner()
s.scan()
app = Application([])
dev = KOBO(None)
debug_print("KOBO:", KOBO)
# dev.startup()
# cd = dev.detect_managed_devices(s.devices)
# dev.open(cd, 'test')
cw = dev.config_widget()
d = QDialog()
d.l = QVBoxLayout()
d.setLayout(d.l)
d.l.addWidget(cw)
bb = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel)
d.l.addWidget(bb)
bb.accepted.connect(d.accept)
bb.rejected.connect(d.reject)
if d.exec_() == d.Accepted:
cw.commit()
dev.shutdown()