mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Enhancement: allow using templares in search strings
This commit is contained in:
parent
13e4f17a50
commit
ae189e0eee
@ -441,6 +441,20 @@ Identifiers (e.g., ISBN, doi, lccn etc) also use an extended syntax. First, note
|
||||
|
||||
:guilabel:`Advanced search dialog`
|
||||
|
||||
You can search using a template in the :ref:`templatelangcalibre` instead of a metadata field. To do so you enter a template, a search type, and the value to search for. The syntax is::
|
||||
|
||||
``template:``(the template)``#@``(search type)``#:``(the value)
|
||||
|
||||
The ``template`` is any valid calibre template language template. The ``search type`` must be one of ``t`` (text search), ``d`` (date search), ``n`` (numeric search), or ``b`` (set/not set (boolean)). The ``value`` is whatever you want. It can use the special operators described above for the various search types. You must quote the entire search string if there are spaces anywhere in it.
|
||||
|
||||
Examples:
|
||||
|
||||
* ``template:"program: connected_device_name('main')#@t#:kindle"`` -- is true of the ``kindle`` device is connected
|
||||
* ``template:"program: select(formats_sizes(), 'EPUB')#@n#:>1000000"`` -- finds epubs larger than 1 MB
|
||||
* ``template:"program: select(formats_modtimes('iso'), 'EPUB')#@d#:>10daysago"`` -- finds epubs newer than 10 days ago.
|
||||
|
||||
You can build template search queries easily using the :guilabel:`Advanced search dialog` accessed by clicking the button |sbi|. You can test templates on specific books using the calibre ``template tester``. Easiest is to add the tester to the book list context menu or to assign the tester a keyboard shortcut.
|
||||
|
||||
.. _saved_searches:
|
||||
|
||||
Saving searches
|
||||
|
@ -11,14 +11,14 @@ from functools import partial
|
||||
from datetime import timedelta
|
||||
from collections import deque, OrderedDict
|
||||
|
||||
from calibre.constants import preferred_encoding
|
||||
from calibre.constants import preferred_encoding, DEBUG
|
||||
from calibre.db.utils import force_to_bool
|
||||
from calibre.utils.config_base import prefs
|
||||
from calibre.utils.date import parse_date, UNDEFINED_DATE, now, dt_as_local
|
||||
from calibre.utils.icu import primary_contains, sort_key
|
||||
from calibre.utils.localization import lang_map, canonicalize_lang
|
||||
from calibre.utils.search_query_parser import SearchQueryParser, ParseException
|
||||
from polyglot.builtins import iteritems, unicode_type, string_or_bytes
|
||||
from polyglot.builtins import iteritems, unicode_type, string_or_bytes, error_message
|
||||
|
||||
CONTAINS_MATCH = 0
|
||||
EQUALS_MATCH = 1
|
||||
@ -627,6 +627,44 @@ class Parser(SearchQueryParser): # {{{
|
||||
|
||||
# Everything else (and 'all' matches)
|
||||
case_sensitive = prefs['case_sensitive']
|
||||
|
||||
if location == 'template':
|
||||
try:
|
||||
template, sep, query = regex.split('#@([tdnb]?)#:', query, flags=regex.IGNORECASE)
|
||||
if sep:
|
||||
sep = sep.lower()
|
||||
else:
|
||||
sep = 't'
|
||||
except:
|
||||
if DEBUG:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
raise ParseException(_('template: missing or invalid separator (#@[tdnb]?#)'))
|
||||
matchkind, query = _matchkind(query, case_sensitive=case_sensitive)
|
||||
matches = set()
|
||||
error_string = '*@*TEMPLATE_ERROR*@*'
|
||||
for book_id in candidates:
|
||||
mi = self.dbcache.get_proxy_metadata(book_id)
|
||||
val = mi.formatter.safe_format(template, {}, error_string, mi)
|
||||
if val.startswith(error_string):
|
||||
raise ParseException(val[len(error_string):])
|
||||
if sep == 't':
|
||||
if _match(query, [val,], matchkind, use_primary_find_in_search=upf,
|
||||
case_sensitive=case_sensitive):
|
||||
matches.add(book_id)
|
||||
elif sep == 'n' and val:
|
||||
matches.update(self.num_search(
|
||||
icu_lower(query), {val:{book_id,}}.items, '', '',
|
||||
{book_id,}, is_many=False))
|
||||
elif sep == 'd' and val:
|
||||
matches.update(self.date_search(
|
||||
icu_lower(query), {val:{book_id,}}.items))
|
||||
elif sep == 'b':
|
||||
matches.update(self.bool_search(icu_lower(query),
|
||||
{'True' if val else 'False':{book_id,}}.items, False))
|
||||
|
||||
return matches
|
||||
|
||||
matchkind, query = _matchkind(query, case_sensitive=case_sensitive)
|
||||
all_locs = set()
|
||||
text_fields = set()
|
||||
|
@ -235,6 +235,46 @@ def create_date_tab(self, db):
|
||||
l.addStretch(10)
|
||||
|
||||
|
||||
def create_template_tab(self):
|
||||
self.simple_tab = w = QWidget(self.tab_widget)
|
||||
self.tab_widget.addTab(w, _("&Template search"))
|
||||
|
||||
w.l = l = QFormLayout(w)
|
||||
l.setFieldGrowthPolicy(l.AllNonFixedFieldsGrow)
|
||||
|
||||
self.template_value_box = le = QLineEdit(w)
|
||||
le.setObjectName('template_value_box')
|
||||
le.setPlaceholderText(_('The value to search for'))
|
||||
le.setToolTip('<p>' +
|
||||
_("You can use the search test specifications described "
|
||||
"in the calibre documentation. For example, with Number "
|
||||
"comparisons you can the relational operators like '>=' etc. "
|
||||
"With Text comparisons you can use exact, comtains "
|
||||
"or regular expression matches. With Date you can use "
|
||||
"today, yesterday, etc. Set/not set takes 'true' for set "
|
||||
"and 'false' for not set.")
|
||||
+ '</p>')
|
||||
l.addRow(_('Template &value:'), le)
|
||||
|
||||
self.template_test_type_box = le = QComboBox(w)
|
||||
le.setObjectName('template_test_type_box')
|
||||
for op, desc in [
|
||||
('t', _('Text')),
|
||||
('d', _('Date')),
|
||||
('n', _('Number')),
|
||||
('b', _('Set/Not Set'))]:
|
||||
le.addItem(desc, op)
|
||||
le.setToolTip(_('How the template result will be compared to the value'))
|
||||
l.addRow(_('C&omparison type:'), le)
|
||||
|
||||
from calibre.gui2.dialogs.template_line_editor import TemplateLineEditor
|
||||
self.template_program_box = le = TemplateLineEditor(self.tab_widget)
|
||||
le.setObjectName('template_program_box')
|
||||
le.setPlaceholderText(_('The template that generates the value'))
|
||||
le.setToolTip(_('Right click to open a template editor'))
|
||||
l.addRow(_('Tem&plate:'), le)
|
||||
|
||||
|
||||
def setup_ui(self, db):
|
||||
self.setWindowTitle(_("Advanced search"))
|
||||
self.setWindowIcon(QIcon(I('search.png')))
|
||||
@ -251,6 +291,7 @@ def setup_ui(self, db):
|
||||
create_adv_tab(self)
|
||||
create_simple_tab(self, db)
|
||||
create_date_tab(self, db)
|
||||
create_template_tab(self)
|
||||
# }}}
|
||||
|
||||
|
||||
@ -270,6 +311,13 @@ class SearchDialog(QDialog):
|
||||
w = getattr(self, focused_field, None)
|
||||
if w is not None:
|
||||
w.setFocus(Qt.OtherFocusReason)
|
||||
elif current_tab == 3:
|
||||
self.template_program_box.setText(
|
||||
gprefs.get('advanced_search_template_tab_program_field', ''))
|
||||
self.template_value_box.setText(
|
||||
gprefs.get('advanced_search_template_tab_value_field', ''))
|
||||
self.template_test_type_box.setCurrentIndex(
|
||||
int(gprefs.get('advanced_search_template_tab_test_field', '0')))
|
||||
self.resize(self.sizeHint())
|
||||
|
||||
def save_state(self):
|
||||
@ -279,6 +327,13 @@ class SearchDialog(QDialog):
|
||||
fw = self.tab_widget.focusWidget()
|
||||
if fw:
|
||||
gprefs.set('advanced_search_simple_tab_focused_field', fw.objectName())
|
||||
elif self.tab_widget.currentIndex() == 3:
|
||||
gprefs.set('advanced_search_template_tab_program_field',
|
||||
unicode_type(self.template_program_box.text()))
|
||||
gprefs.set('advanced_search_template_tab_value_field',
|
||||
unicode_type(self.template_value_box.text()))
|
||||
gprefs.set('advanced_search_template_tab_test_field',
|
||||
unicode_type(self.template_test_type_box.currentIndex()))
|
||||
|
||||
def accept(self):
|
||||
self.save_state()
|
||||
@ -290,9 +345,9 @@ class SearchDialog(QDialog):
|
||||
|
||||
def clear_button_pushed(self):
|
||||
w = self.tab_widget.currentWidget()
|
||||
for c in w.findChildren(QComboBox):
|
||||
c.setCurrentIndex(0)
|
||||
if w is self.date_tab:
|
||||
for c in w.findChildren(QComboBox):
|
||||
c.setCurrentIndex(0)
|
||||
for c in w.findChildren(QSpinBox):
|
||||
c.setValue(c.minimum())
|
||||
self.sel_date.setChecked(True)
|
||||
@ -312,7 +367,18 @@ class SearchDialog(QDialog):
|
||||
|
||||
def search_string(self):
|
||||
i = self.tab_widget.currentIndex()
|
||||
return (self.adv_search_string, self.box_search_string, self.date_search_string)[i]()
|
||||
return (self.adv_search_string, self.box_search_string,
|
||||
self.date_search_string, self.template_search_string)[i]()
|
||||
|
||||
def template_search_string(self):
|
||||
template = unicode_type(self.template_program_box.text())
|
||||
value = unicode_type(self.template_value_box.text()).replace('"', '\\"')
|
||||
if template and value:
|
||||
cb = self.template_test_type_box
|
||||
op = unicode_type(cb.itemData(cb.currentIndex()))
|
||||
l = '{0}#@{1}#:{2}'.format(template, op, value)
|
||||
return 'template:"' + l + '"'
|
||||
return ''
|
||||
|
||||
def date_search_string(self):
|
||||
field = unicode_type(self.date_field.itemData(self.date_field.currentIndex()) or '')
|
||||
|
@ -384,7 +384,7 @@ class FieldMetadata(object):
|
||||
'int', 'float', 'bool', 'series', 'composite', 'enumeration'])
|
||||
|
||||
# search labels that are not db columns
|
||||
search_items = ['all', 'search', 'vl']
|
||||
search_items = ['all', 'search', 'vl', 'template']
|
||||
__calibre_serializable__ = True
|
||||
|
||||
def __init__(self):
|
||||
|
Loading…
x
Reference in New Issue
Block a user