From 26842bd407c95e2d2c135be3442039e7081d7797 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 4 May 2022 00:31:55 +1000 Subject: [PATCH] Add support for templates for collections A template can be used as well as the existing columns to create collections on Kobo ereaders. The template can return a list of collection names but uses ":@:" as the separator between the names. --- src/calibre/devices/kobo/books.py | 63 +++++++++---------- src/calibre/devices/kobo/driver.py | 50 ++++++++++++--- src/calibre/devices/kobo/kobotouch_config.py | 64 +++++++++++++++++--- 3 files changed, 126 insertions(+), 51 deletions(-) diff --git a/src/calibre/devices/kobo/books.py b/src/calibre/devices/kobo/books.py index 9421a31bd2..73fde8d094 100644 --- a/src/calibre/devices/kobo/books.py +++ b/src/calibre/devices/kobo/books.py @@ -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 diff --git a/src/calibre/devices/kobo/driver.py b/src/calibre/devices/kobo/driver.py index a997905569..0f1b35fcf6 100644 --- a/src/calibre/devices/kobo/driver.py +++ b/src/calibre/devices/kobo/driver.py @@ -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 ) diff --git a/src/calibre/devices/kobo/kobotouch_config.py b/src/calibre/devices/kobo/kobotouch_config.py index b803092cf8..5365b22894 100644 --- a/src/calibre/devices/kobo/kobotouch_config.py +++ b/src/calibre/devices/kobo/kobotouch_config.py @@ -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):