Kobo driver: Allow using templates to generate collections

Merge branch 'master' of https://github.com/davidfor/calibre
This commit is contained in:
Kovid Goyal 2022-05-03 20:07:13 +05:30
commit c1f6765fce
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
3 changed files with 126 additions and 51 deletions

View File

@ -9,6 +9,7 @@ from calibre.constants import preferred_encoding, DEBUG
from calibre import isbytestring
from calibre.ebooks.metadata.book.base import Metadata
from calibre.ebooks.metadata.book.formatter import SafeFormat
from calibre.devices.usbms.books import Book as Book_, CollectionsBookList, none_cmp
from calibre.utils.config_base import prefs
from calibre.devices.usbms.driver import debug_print
@ -132,7 +133,7 @@ class KTCollectionsBookList(CollectionsBookList):
super().__init__(oncard, prefix, settings)
self.set_device_managed_collections([])
def get_collections(self, collection_attributes):
def get_collections(self, collection_attributes, collections_template=None, template_globals=None):
debug_print("KTCollectionsBookList:get_collections - start - collection_attributes=", collection_attributes)
collections = {}
@ -157,14 +158,17 @@ class KTCollectionsBookList(CollectionsBookList):
lpath = getattr(book, 'lpath', None)
if lpath is None:
continue
# If the book is not in the current library, we don't want to use the metadtaa for the collections
if book.application_id is None:
# debug_print("KTCollectionsBookList:get_collections - Book not in current library")
# If the book is not in the current library, we don't want to use the metadata for the collections
# or it is a book that cannot be put in a collection (such as recommendations or previews)
if book.application_id is None or not book.can_put_on_shelves:
# debug_print("KTCollectionsBookList:get_collections - Book not in current library or cannot be put in a collection")
continue
# Decide how we will build the collections. The default: leave the
# book in all existing collections. Do not add any new ones.
attrs = ['device_collections']
if getattr(book, '_new_book', False):
debug_print("KTCollectionsBookList:get_collections - sending new book")
if prefs['manage_device_metadata'] == 'manual':
# Ensure that the book is in all the book's existing
# collections plus all metadata collections
@ -173,11 +177,11 @@ class KTCollectionsBookList(CollectionsBookList):
# For new books, both 'on_send' and 'on_connect' do the same
# thing. The book's existing collections are ignored. Put
# the book in collections defined by its metadata.
attrs = collection_attributes
attrs = list(collection_attributes)
elif prefs['manage_device_metadata'] == 'on_connect':
# For existing books, modify the collections only if the user
# specified 'on_connect'
attrs = collection_attributes
attrs = list(collection_attributes)
for cat_name in self.device_managed_collections:
if cat_name in book.device_collections:
if cat_name not in collections:
@ -185,17 +189,22 @@ class KTCollectionsBookList(CollectionsBookList):
if show_debug:
debug_print("KTCollectionsBookList:get_collections - Device Managed Collection:", cat_name)
if lpath not in collections[cat_name]:
collections[cat_name][lpath] = (book, tsval, tsval)
collections[cat_name][lpath] = book
if show_debug:
debug_print("KTCollectionsBookList:get_collections - Device Managed Collection -added book to cat_name", cat_name)
book.device_collections = []
if show_debug:
debug_print("KTCollectionsBookList:get_collections - attrs=", attrs)
if collections_template is not None:
attrs.append('%template%')
for attr in attrs:
fm = None
attr = attr.strip()
if show_debug:
debug_print("KTCollectionsBookList:get_collections - attr='%s'"%attr)
# If attr is device_collections, then we cannot use
# format_field, because we don't know the fields where the
# values came from.
@ -204,10 +213,16 @@ class KTCollectionsBookList(CollectionsBookList):
val = book.device_collections # is a list
if show_debug:
debug_print("KTCollectionsBookList:get_collections - adding book.device_collections", book.device_collections)
# If the book is not in the current library, we don't want to use the metadtaa for the collections
elif book.application_id is None or not book.can_put_on_shelves:
# debug_print("KTCollectionsBookList:get_collections - Book not in current library")
continue
elif attr == '%template%':
doing_dc = False
val = ''
if collections_template is not None:
nv = SafeFormat().safe_format(collections_template, book,
'KOBO', book, global_vars=template_globals)
if show_debug:
debug_print("KTCollectionsBookList:get_collections collections_template - result", nv)
if nv:
val = [v.strip() for v in nv.split(':@:') if v.strip()]
else:
doing_dc = False
ign, val, orig_val, fm = book.format_field_extended(attr)
@ -216,6 +231,7 @@ class KTCollectionsBookList(CollectionsBookList):
debug_print("KTCollectionsBookList:get_collections - not device_collections")
debug_print(' ign=', ign, ', val=', val, ' orig_val=', orig_val, 'fm=', fm)
debug_print(' val=', val)
if not val:
continue
if isbytestring(val):
@ -246,26 +262,16 @@ class KTCollectionsBookList(CollectionsBookList):
for category in val:
# debug_print("KTCollectionsBookList:get_collections - category=", category)
is_series = False
if doing_dc:
# Attempt to determine if this value is a series by
# comparing it to the series name.
if category == book.series:
is_series = True
pass # No need to do anything with device_collections
elif fm is not None and fm['is_custom']: # is a custom field
if fm['datatype'] == 'text' and len(category) > 1 and \
category[0] == '[' and category[-1] == ']':
continue
if fm['datatype'] == 'series':
is_series = True
else: # is a standard field
if attr == 'tags' and len(category) > 1 and \
category[0] == '[' and category[-1] == ']':
continue
if attr == 'series' or \
('series' in collection_attributes and
book.get('series', None) == category):
is_series = True
# The category should not be None, but, it has happened.
if not category:
@ -278,15 +284,7 @@ class KTCollectionsBookList(CollectionsBookList):
if show_debug:
debug_print("KTCollectionsBookList:get_collections - created collection for cat_name", cat_name)
if lpath not in collections[cat_name]:
if is_series:
if doing_dc:
collections[cat_name][lpath] = \
(book, book.get('series_index', sys.maxsize), tsval)
else:
collections[cat_name][lpath] = \
(book, book.get(attr+'_index', sys.maxsize), tsval)
else:
collections[cat_name][lpath] = (book, tsval, tsval)
collections[cat_name][lpath] = book
if show_debug:
debug_print("KTCollectionsBookList:get_collections - added book to collection for cat_name", cat_name)
if show_debug:
@ -296,8 +294,7 @@ class KTCollectionsBookList(CollectionsBookList):
result = {}
for category, lpaths in collections.items():
books = sorted(lpaths.values(), key=cmp_to_key(none_cmp))
result[category] = [x[0] for x in books]
result[category] = lpaths.values()
# debug_print("KTCollectionsBookList:get_collections - result=", result.keys())
debug_print("KTCollectionsBookList:get_collections - end")
return result

View File

@ -632,6 +632,7 @@ class KOBO(USBMS):
book.size = os.stat(self.normalize_path(path)).st_size
b = booklists[blist].add_book(book, replace_metadata=True)
if b:
debug_print("KoboTouch::add_books_to_metadata - have a new book - book=%s" % book)
b._new_book = True
self.report_progress(1.0, _('Adding books to device metadata listing...'))
@ -740,7 +741,7 @@ class KOBO(USBMS):
' 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))
'\nDevice firmware version: %s') % (self.dbversion, self.display_fwversion))
, UserFeedback.WARN)
return False
@ -973,6 +974,12 @@ class KOBO(USBMS):
opts = self.settings()
return opts.extra_customization[self.OPT_SHOW_PREVIEWS] is False
@property
def display_fwversion(self):
if self.fwversion is None:
return ''
return '.'.join([str(v) for v in list(self.fwversion)])
def sync_booklists(self, booklists, end_session=True):
debug_print('KOBO:sync_booklists - start')
paths = self.get_device_paths()
@ -2464,6 +2471,7 @@ class KOBOTOUCH(KOBO):
def update_device_database_collections(self, booklists, collections_attributes, oncard):
debug_print("KoboTouch:update_device_database_collections - oncard='%s'"%oncard)
debug_print("KoboTouch:update_device_database_collections - device='%s'" % self)
if self.modify_database_check("update_device_database_collections") is False:
return
@ -2501,9 +2509,17 @@ class KOBOTOUCH(KOBO):
booklists.set_debugging_title(debugging_title)
booklists.set_device_managed_collections(self.ignore_collections_names)
bookshelf_attribute = len(collections_attributes) > 0
have_bookshelf_attributes = len(collections_attributes) > 0 and self.use_collections_template
collections = booklists.get_collections(collections_attributes) if bookshelf_attribute else None
collections = booklists.get_collections(collections_attributes,
collections_template=self.collections_template,
template_globals={
'serial_number': self.device_serial_no(),
'firmware_version': self.fwversion,
'display_firmware_version': self.display_fwversion,
'dbversion': self.dbversion,
}
) if have_bookshelf_attributes else None
# debug_print('KoboTouch:update_device_database_collections - Collections:', collections)
# Create a connection to the sqlite database
@ -2514,7 +2530,7 @@ class KOBOTOUCH(KOBO):
with closing(self.device_database_connection(use_row_factory=True)) as connection:
if self.manage_collections:
if collections:
if collections is not None:
# debug_print("KoboTouch:update_device_database_collections - length collections=" + str(len(collections)))
# Need to reset the collections outside the particular loops
@ -2595,7 +2611,7 @@ class KOBOTOUCH(KOBO):
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
elif have_bookshelf_attributes: # 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:
@ -2606,7 +2622,7 @@ class KOBOTOUCH(KOBO):
# 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):
have_bookshelf_attributes or update_series_details or update_core_metadata):
debug_print("KoboTouch:update_device_database_collections - managing bookshelves and series.")
self.series_set = 0
@ -2631,7 +2647,7 @@ class KOBOTOUCH(KOBO):
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 self.manage_collections and have_bookshelf_attributes:
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)
@ -3468,7 +3484,10 @@ class KOBOTOUCH(KOBO):
c = super()._config()
c.add_opt('manage_collections', default=True)
c.add_opt('use_collections_columns', default=True)
c.add_opt('collections_columns', default='')
c.add_opt('use_collections_template', default=False)
c.add_opt('collections_template', default='')
c.add_opt('create_collections', default=False)
c.add_opt('delete_empty_collections', default=False)
c.add_opt('ignore_collections_names', default='')
@ -3663,9 +3682,21 @@ class KOBOTOUCH(KOBO):
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 use_collections_columns(self):
return self.get_pref('use_collections_columns') and self.manage_collections
@property
def collections_columns(self):
return self.get_pref('collections_columns') if self.manage_collections else ''
return self.get_pref('collections_columns') if self.use_collections_columns else ''
@property
def use_collections_template(self):
return self.get_pref('use_collections_template') and self.manage_collections
@property
def collections_template(self):
return self.get_pref('collections_template') if self.use_collections_template else ''
def get_collections_attributes(self):
collections_str = self.collections_columns
@ -3858,6 +3889,7 @@ class KOBOTOUCH(KOBO):
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
@ -3888,7 +3920,7 @@ class KOBOTOUCH(KOBO):
(
'\nDevice database version: %s.'
'\nDevice firmware version: %s'
) % (self.dbversion, self.fwversion),
) % (self.dbversion, self.display_fwversion),
UserFeedback.WARN
)

View File

@ -7,14 +7,15 @@ __docformat__ = 'restructuredtext en'
import textwrap
from qt.core import (QWidget, QLabel, QGridLayout, QLineEdit, QVBoxLayout,
QDialog, QDialogButtonBox, QCheckBox, QPushButton)
from qt.core import (QWidget, QLabel, QGridLayout, QLineEdit, QVBoxLayout, QDialog,
QDialogButtonBox, QCheckBox, QPushButton)
from calibre.gui2.device_drivers.tabbed_device_config import TabbedDeviceConfig, DeviceConfigTab, DeviceOptionsGroupBox
from calibre.devices.usbms.driver import debug_print
from calibre.gui2 import error_dialog
from calibre.gui2.widgets2 import ColorButton
from calibre.gui2.dialogs.template_dialog import TemplateDialog
from calibre.gui2.dialogs.template_line_editor import TemplateLineEditor
def wrap_msg(msg):
@ -98,7 +99,10 @@ class KOBOTOUCHConfig(TabbedDeviceConfig):
p['manage_collections'] = self.manage_collections
p['create_collections'] = self.create_collections
p['use_collections_columns'] = self.use_collections_columns
p['collections_columns'] = self.collections_columns
p['use_collections_template'] = self.use_collections_template
p['collections_template'] = self.collections_template
p['ignore_collections_names'] = self.ignore_collections_names
p['delete_empty_collections'] = self.delete_empty_collections
@ -244,13 +248,30 @@ class CollectionsGroupBox(DeviceOptionsGroupBox):
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.use_collections_columns_checkbox = create_checkbox(
_("Collections columns:"),
_('Use a columns to generate collections.'),
device.get_pref('use_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.use_collections_template_checkbox = create_checkbox(
_("Collections template:"),
_('Use a template to generate collections.'),
device.get_pref('use_collections_template')
)
self.collections_template_edit = TemplateConfig(
device.get_pref('collections_template'),
tooltip=_("Enter a template to generate collections. "
"The result of the template will be combined with the values from Collections column."
"The template should return a list of collection names separated by ':@:' (without quotes)."
)
)
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.'),
@ -269,21 +290,46 @@ class CollectionsGroupBox(DeviceOptionsGroupBox):
'will not be changed. Names are separated by commas.'))
self.ignore_collections_names_edit.setText(device.get_pref('ignore_collections_names'))
self.options_layout.addWidget(self.collections_columns_label, 1, 0, 1, 1)
self.options_layout.addWidget(self.use_collections_columns_checkbox, 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.addWidget(self.ignore_collections_names_label, 4, 0, 1, 1)
self.options_layout.addWidget(self.ignore_collections_names_edit, 4, 1, 1, 1)
self.options_layout.addWidget(self.use_collections_template_checkbox, 2, 0, 1, 1)
self.options_layout.addWidget(self.collections_template_edit, 2, 1, 1, 1)
self.options_layout.addWidget(self.create_collections_checkbox, 3, 0, 1, 2)
self.options_layout.addWidget(self.delete_empty_collections_checkbox, 4, 0, 1, 2)
self.options_layout.addWidget(self.ignore_collections_names_label, 5, 0, 1, 1)
self.options_layout.addWidget(self.ignore_collections_names_edit, 5, 1, 1, 1)
self.use_collections_columns_checkbox.clicked.connect(self.use_collections_columns_checkbox_clicked)
self.use_collections_template_checkbox.clicked.connect(self.use_collections_template_checkbox_clicked)
self.use_collections_columns_checkbox_clicked(device.get_pref('use_collections_columns'))
self.use_collections_template_checkbox_clicked(device.get_pref('use_collections_template'))
def use_collections_columns_checkbox_clicked(self, checked):
self.collections_columns_edit.setEnabled(checked)
def use_collections_template_checkbox_clicked(self, checked):
self.collections_template_edit.setEnabled(checked)
@property
def manage_collections(self):
return self.isChecked()
@property
def use_collections_columns(self):
return self.use_collections_columns_checkbox.isChecked()
@property
def collections_columns(self):
return self.collections_columns_edit.text().strip()
@property
def use_collections_template(self):
return self.use_collections_template_checkbox.isChecked()
@property
def collections_template(self):
return self.collections_template_edit.template
@property
def create_collections(self):
return self.create_collections_checkbox.isChecked()
@ -733,7 +779,7 @@ class TemplateConfig(QWidget): # {{{
b = self.b = QPushButton(_('&Template editor'))
l.addWidget(b, 0, col, 1, 1)
b.clicked.connect(self.edit_template)
self.setToolTip(tooltip)
self.setToolTip(wrap_msg(tooltip))
@property
def template(self):