Here is a custom action that addresses filtering problem, without the need for the user to use template language at all. It allows the following:
Code:
#!/usr/bin/env python
# ~*~ coding: utf-8 ~*~
__license__ = 'GPL v3'
__copyright__ = '2025, Ahmed Zaki <azaki00.dev@gmail.com>'
__docformat__ = 'restructuredtext en'
from qt.core import (QWidget, QVBoxLayout, QGroupBox, QRadioButton,
QLineEdit, QCheckBox, QLabel)
from calibre import prints
from calibre.constants import DEBUG
from calibre.utils.search_query_parser import ParseException
from calibre_plugins.action_chains.actions.base import ChainAction
from calibre_plugins.action_chains.database import is_search_valid
class ConfigWidget(QWidget):
def __init__(self, plugin_action):
QWidget.__init__(self)
self.plugin_action = plugin_action
self.gui = plugin_action.gui
self.db = self.gui.current_db
self._init_controls()
def _init_controls(self):
l = self.l = QVBoxLayout()
self.setLayout(l)
search_lbl = QLabel(_('Calibre search'))
search_lbl.setToolTip(_('Use a regular calibre search'))
l.addWidget(search_lbl)
searchbox = self.searchbox = QLineEdit(self)
searchbox.textChanged.connect(self._on_searchbox_text_change)
l.addWidget(searchbox)
selection_groupbox = QGroupBox(_('Selection options'))
selection_groupbox_layout = QVBoxLayout()
selection_groupbox.setLayout(selection_groupbox_layout)
selected_opt = self.selected_opt = QRadioButton(_('Restrict to currently selected books'))
selection_groupbox_layout.addWidget(selected_opt)
selected_opt.setChecked(True)
all_opt = self.all_opt = QRadioButton(_('Act on all books'))
selection_groupbox_layout.addWidget(all_opt)
scope_opt = self.scope_opt = QRadioButton(_('Restrict to books in current scope'))
selection_groupbox_layout.addWidget(scope_opt)
saved_selection_opt = self.saved_selection_opt = QRadioButton(_('Restrict to books saved in a variable'))
selection_groupbox_layout.addWidget(saved_selection_opt)
retrieve_variable_ledit = self.retrieve_variable_ledit = QLineEdit()
selection_groupbox_layout.addWidget(retrieve_variable_ledit)
self.l.addWidget(selection_groupbox)
save_selection_gb = self.save_selection_gb = QGroupBox(_('Save currently selected books into a variable'))
save_selection_l = QVBoxLayout()
save_selection_gb.setLayout(save_selection_l)
save_selection_gb.setCheckable(True)
save_selection_lbl = QLabel(_('Variable name'))
save_selection_ledit = self.save_selection_ledit = QLineEdit()
save_selection_l.addWidget(save_selection_lbl)
save_selection_l.addWidget(save_selection_ledit)
l.addWidget(save_selection_gb)
self.l.addStretch(1)
self.setMinimumSize(300,400)
def _on_searchbox_text_change(self):
text = self.searchbox.text()
if text:
ok = is_search_valid(self.db, text)
ss = 'QLineEdit { border: 2px solid %s; border-radius: 3px }' % (
'#50c878' if ok else '#FF2400')
self.searchbox.setStyleSheet(ss)
else:
self.searchbox.setStyleSheet('')
def load_settings(self, settings):
if settings:
search_text = settings.get('search_text', '')
self.searchbox.setText(search_text)
superset = settings.get('opt', 'selected_books')
if superset == 'selected_books':
self.selected_opt.setChecked(True)
elif superset == 'current_scope':
self.scope_opt.setChecked(True)
elif superset == 'all_books':
self.all_opt.setChecked(True)
elif superset == 'saved_selection':
self.saved_selection_opt.setChecked(True)
self.retrieve_variable_ledit.setText(settings['retrieve_variable'])
self.save_selection_gb.setChecked(settings['save_selection'])
self.save_selection_ledit.setText(settings.get('save_variable', ''))
def save_settings(self):
settings = {}
settings['search_text'] = self.searchbox.text()
if self.selected_opt.isChecked():
settings['opt'] = 'selected_books'
elif self.scope_opt.isChecked():
settings['opt'] = 'current_scope'
elif self.all_opt.isChecked():
settings['opt'] = 'all_books'
elif self.saved_selection_opt.isChecked():
settings['opt'] = 'saved_selection'
settings['retrieve_variable'] = self.retrieve_variable_ledit.text()
settings['save_selection'] = self.save_selection_gb.isChecked()
if settings['save_selection']:
settings['save_variable'] = self.save_selection_ledit.text().strip()
return settings
class SubsetSelector(ChainAction):
name = 'Subset Selector'
_is_builtin = False
support_scopes = True
def run(self, gui, settings, chain):
db = gui.current_db
cache = {}
# dont save them yet, cache and save later
if settings['save_selection']:
cache['current_ids'] = gui.current_view().get_selected_ids()
superset = settings.get('opt', 'selected_books')
search_text = settings['search_text'].strip()
if search_text:
if superset == 'selected_books':
if cache.get('current_ids'):
superset_ids = cache['current_ids']
else:
superset_ids = gui.current_view().get_selected_ids()
elif superset == 'current_scope':
superset_ids = chain.scope().get_book_ids()
elif superset == 'all_books':
superset_ids = gui.current_db.all_ids()
elif superset == 'saved_selection':
superset_ids = getattr(chain, 'python_vars', {}).get(settings['retrieve_variable'], None)
if superset_ids == None:
prints(f'Action Chains: Subset Selector: {settings["retrieve_variable"]} is empty')
superset_ids = []
try:
if not search_text:
book_ids = superset_ids
else:
search_ids = db.data.search_getting_ids(search_text, '', use_virtual_library=False)
if superset == 'all_books':
book_ids = search_ids
else:
book_ids = list(set(search_ids).intersection(superset_ids))
except ParseException:
book_ids = []
gui.current_view().select_rows(book_ids, change_current=True, scroll=False)
# Avoid saving selected books early in case the opt
# saved_selection is ticked
if settings['save_selection']:
save_variable = settings['save_variable']
if not hasattr(chain, 'python_vars'):
chain.python_vars = {}
chain.python_vars[save_variable] = cache['current_ids']
chain.chain_vars[save_variable] = ','.join([str(x) for x in chain.python_vars[save_variable]])
def validate(self, settings):
gui = self.plugin_action.gui
db = gui.current_db
if not settings:
return (_('Settings Error'), _('You must configure this superset before running it'))
search_text = settings['search_text']
if not is_search_valid(db, search_text):
return (_('Invalid search'), _(f'Search "{search_text}" is not valid'))
if settings['save_selection'] and not settings.get('save_variable', '').strip():
return (_('Empty variable'), _('You must choose a variable name to save books'))
if settings['opt'] == 'saved_selection' and not settings.get('retrieve_variable', '').strip():
return (_('Empty variable'), _('You must enter saved variable name'))
return True
def config_widget(self):
return ConfigWidget
- The user can use the action to save current selections to a named variable (e.g. chain_selected_ids, previously_selected_ids, third_action_ids .... etc)
- The user can restore the selections from a named variable.
- The user can apply a regular calibre search that will limit the selection to a subset of one of the following:
- Currently selected books.
- Book ids stored in a variable
- Books in current scope
Notes:
- Regular calibre search allows for using templates. You consult the calibre search documentation, looking for the section named: "Search using templates"
- The selection is going to be limited by the current library view including the search vls applied. To clear those, you can use the builtin "Selection Modifier". There is no need to duplicate the same functionality.
- The action is called "Subset Selector" for lack of better name. More descriptive suggestions are welcome.
- The action will not be part of the stock plugin until it fully matures and has a stable api.