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`
|
: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:
|
.. _saved_searches:
|
||||||
|
|
||||||
Saving searches
|
Saving searches
|
||||||
|
@ -11,14 +11,14 @@ from functools import partial
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from collections import deque, OrderedDict
|
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.db.utils import force_to_bool
|
||||||
from calibre.utils.config_base import prefs
|
from calibre.utils.config_base import prefs
|
||||||
from calibre.utils.date import parse_date, UNDEFINED_DATE, now, dt_as_local
|
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.icu import primary_contains, sort_key
|
||||||
from calibre.utils.localization import lang_map, canonicalize_lang
|
from calibre.utils.localization import lang_map, canonicalize_lang
|
||||||
from calibre.utils.search_query_parser import SearchQueryParser, ParseException
|
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
|
CONTAINS_MATCH = 0
|
||||||
EQUALS_MATCH = 1
|
EQUALS_MATCH = 1
|
||||||
@ -627,6 +627,44 @@ class Parser(SearchQueryParser): # {{{
|
|||||||
|
|
||||||
# Everything else (and 'all' matches)
|
# Everything else (and 'all' matches)
|
||||||
case_sensitive = prefs['case_sensitive']
|
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)
|
matchkind, query = _matchkind(query, case_sensitive=case_sensitive)
|
||||||
all_locs = set()
|
all_locs = set()
|
||||||
text_fields = set()
|
text_fields = set()
|
||||||
|
@ -235,6 +235,46 @@ def create_date_tab(self, db):
|
|||||||
l.addStretch(10)
|
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):
|
def setup_ui(self, db):
|
||||||
self.setWindowTitle(_("Advanced search"))
|
self.setWindowTitle(_("Advanced search"))
|
||||||
self.setWindowIcon(QIcon(I('search.png')))
|
self.setWindowIcon(QIcon(I('search.png')))
|
||||||
@ -251,6 +291,7 @@ def setup_ui(self, db):
|
|||||||
create_adv_tab(self)
|
create_adv_tab(self)
|
||||||
create_simple_tab(self, db)
|
create_simple_tab(self, db)
|
||||||
create_date_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)
|
w = getattr(self, focused_field, None)
|
||||||
if w is not None:
|
if w is not None:
|
||||||
w.setFocus(Qt.OtherFocusReason)
|
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())
|
self.resize(self.sizeHint())
|
||||||
|
|
||||||
def save_state(self):
|
def save_state(self):
|
||||||
@ -279,6 +327,13 @@ class SearchDialog(QDialog):
|
|||||||
fw = self.tab_widget.focusWidget()
|
fw = self.tab_widget.focusWidget()
|
||||||
if fw:
|
if fw:
|
||||||
gprefs.set('advanced_search_simple_tab_focused_field', fw.objectName())
|
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):
|
def accept(self):
|
||||||
self.save_state()
|
self.save_state()
|
||||||
@ -290,9 +345,9 @@ class SearchDialog(QDialog):
|
|||||||
|
|
||||||
def clear_button_pushed(self):
|
def clear_button_pushed(self):
|
||||||
w = self.tab_widget.currentWidget()
|
w = self.tab_widget.currentWidget()
|
||||||
|
for c in w.findChildren(QComboBox):
|
||||||
|
c.setCurrentIndex(0)
|
||||||
if w is self.date_tab:
|
if w is self.date_tab:
|
||||||
for c in w.findChildren(QComboBox):
|
|
||||||
c.setCurrentIndex(0)
|
|
||||||
for c in w.findChildren(QSpinBox):
|
for c in w.findChildren(QSpinBox):
|
||||||
c.setValue(c.minimum())
|
c.setValue(c.minimum())
|
||||||
self.sel_date.setChecked(True)
|
self.sel_date.setChecked(True)
|
||||||
@ -312,7 +367,18 @@ class SearchDialog(QDialog):
|
|||||||
|
|
||||||
def search_string(self):
|
def search_string(self):
|
||||||
i = self.tab_widget.currentIndex()
|
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):
|
def date_search_string(self):
|
||||||
field = unicode_type(self.date_field.itemData(self.date_field.currentIndex()) or '')
|
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'])
|
'int', 'float', 'bool', 'series', 'composite', 'enumeration'])
|
||||||
|
|
||||||
# search labels that are not db columns
|
# search labels that are not db columns
|
||||||
search_items = ['all', 'search', 'vl']
|
search_items = ['all', 'search', 'vl', 'template']
|
||||||
__calibre_serializable__ = True
|
__calibre_serializable__ = True
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
Loading…
x
Reference in New Issue
Block a user