mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Get Books: Allow easy searching by title and author in addition to any keyword, to prevent large number of spurious matches.
This commit is contained in:
commit
a755b87b2f
@ -16,8 +16,6 @@ from calibre.gui2 import NONE, FunctionDispatcher
|
|||||||
from calibre.gui2.store.search_result import SearchResult
|
from calibre.gui2.store.search_result import SearchResult
|
||||||
from calibre.gui2.store.search.download_thread import DetailsThreadPool, \
|
from calibre.gui2.store.search.download_thread import DetailsThreadPool, \
|
||||||
CoverThreadPool
|
CoverThreadPool
|
||||||
from calibre.library.caches import _match, CONTAINS_MATCH, EQUALS_MATCH, \
|
|
||||||
REGEXP_MATCH
|
|
||||||
from calibre.utils.icu import sort_key
|
from calibre.utils.icu import sort_key
|
||||||
from calibre.utils.search_query_parser import SearchQueryParser
|
from calibre.utils.search_query_parser import SearchQueryParser
|
||||||
|
|
||||||
@ -153,7 +151,7 @@ class Matches(QAbstractItemModel):
|
|||||||
mod_query = query
|
mod_query = query
|
||||||
# Remove filter identifiers
|
# Remove filter identifiers
|
||||||
# Remove the prefix.
|
# Remove the prefix.
|
||||||
for loc in ('all', 'author', 'authors', 'title'):
|
for loc in ('all', 'author', 'author2', 'authors', 'title'):
|
||||||
query = re.sub(r'%s:"(?P<a>[^\s"]+)"' % loc, '\g<a>', query)
|
query = re.sub(r'%s:"(?P<a>[^\s"]+)"' % loc, '\g<a>', query)
|
||||||
query = query.replace('%s:' % loc, '')
|
query = query.replace('%s:' % loc, '')
|
||||||
# Remove the prefix and search text.
|
# Remove the prefix and search text.
|
||||||
@ -301,11 +299,16 @@ class Matches(QAbstractItemModel):
|
|||||||
|
|
||||||
|
|
||||||
class SearchFilter(SearchQueryParser):
|
class SearchFilter(SearchQueryParser):
|
||||||
|
CONTAINS_MATCH = 0
|
||||||
|
EQUALS_MATCH = 1
|
||||||
|
REGEXP_MATCH = 2
|
||||||
|
IN_MATCH = 3
|
||||||
|
|
||||||
USABLE_LOCATIONS = [
|
USABLE_LOCATIONS = [
|
||||||
'all',
|
'all',
|
||||||
'affiliate',
|
'affiliate',
|
||||||
'author',
|
'author',
|
||||||
|
'author2',
|
||||||
'authors',
|
'authors',
|
||||||
'cover',
|
'cover',
|
||||||
'download',
|
'download',
|
||||||
@ -331,6 +334,26 @@ class SearchFilter(SearchQueryParser):
|
|||||||
def universal_set(self):
|
def universal_set(self):
|
||||||
return self.srs
|
return self.srs
|
||||||
|
|
||||||
|
def _match(self, query, value, matchkind):
|
||||||
|
for t in value:
|
||||||
|
try: ### ignore regexp exceptions, required because search-ahead tries before typing is finished
|
||||||
|
t = icu_lower(t)
|
||||||
|
if matchkind == self.EQUALS_MATCH:
|
||||||
|
if query == t:
|
||||||
|
return True
|
||||||
|
elif matchkind == self.REGEXP_MATCH:
|
||||||
|
if re.search(query, t, re.I|re.UNICODE):
|
||||||
|
return True
|
||||||
|
elif matchkind == self.CONTAINS_MATCH:
|
||||||
|
if query in t:
|
||||||
|
return True
|
||||||
|
elif matchkind == self.IN_MATCH:
|
||||||
|
if t in query:
|
||||||
|
return True
|
||||||
|
except re.error:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
def get_matches(self, location, query):
|
def get_matches(self, location, query):
|
||||||
query = query.strip()
|
query = query.strip()
|
||||||
location = location.lower().strip()
|
location = location.lower().strip()
|
||||||
@ -341,17 +364,17 @@ class SearchFilter(SearchQueryParser):
|
|||||||
elif location == 'formats':
|
elif location == 'formats':
|
||||||
location = 'format'
|
location = 'format'
|
||||||
|
|
||||||
matchkind = CONTAINS_MATCH
|
matchkind = self.CONTAINS_MATCH
|
||||||
if len(query) > 1:
|
if len(query) > 1:
|
||||||
if query.startswith('\\'):
|
if query.startswith('\\'):
|
||||||
query = query[1:]
|
query = query[1:]
|
||||||
elif query.startswith('='):
|
elif query.startswith('='):
|
||||||
matchkind = EQUALS_MATCH
|
matchkind = self.EQUALS_MATCH
|
||||||
query = query[1:]
|
query = query[1:]
|
||||||
elif query.startswith('~'):
|
elif query.startswith('~'):
|
||||||
matchkind = REGEXP_MATCH
|
matchkind = self.REGEXP_MATCH
|
||||||
query = query[1:]
|
query = query[1:]
|
||||||
if matchkind != REGEXP_MATCH: ### leave case in regexps because it can be significant e.g. \S \W \D
|
if matchkind != self.REGEXP_MATCH: ### leave case in regexps because it can be significant e.g. \S \W \D
|
||||||
query = query.lower()
|
query = query.lower()
|
||||||
|
|
||||||
if location not in self.USABLE_LOCATIONS:
|
if location not in self.USABLE_LOCATIONS:
|
||||||
@ -372,6 +395,7 @@ class SearchFilter(SearchQueryParser):
|
|||||||
}
|
}
|
||||||
for x in ('author', 'download', 'format'):
|
for x in ('author', 'download', 'format'):
|
||||||
q[x+'s'] = q[x]
|
q[x+'s'] = q[x]
|
||||||
|
q['author2'] = q['author']
|
||||||
|
|
||||||
# make the price in query the same format as result
|
# make the price in query the same format as result
|
||||||
if location == 'price':
|
if location == 'price':
|
||||||
@ -415,16 +439,19 @@ class SearchFilter(SearchQueryParser):
|
|||||||
### Can't separate authors because comma is used for name sep and author sep
|
### Can't separate authors because comma is used for name sep and author sep
|
||||||
### Exact match might not get what you want. For that reason, turn author
|
### Exact match might not get what you want. For that reason, turn author
|
||||||
### exactmatch searches into contains searches.
|
### exactmatch searches into contains searches.
|
||||||
if locvalue == 'author' and matchkind == EQUALS_MATCH:
|
if locvalue == 'author' and matchkind == self.EQUALS_MATCH:
|
||||||
m = CONTAINS_MATCH
|
m = self.CONTAINS_MATCH
|
||||||
else:
|
else:
|
||||||
m = matchkind
|
m = matchkind
|
||||||
|
|
||||||
if locvalue == 'format':
|
if locvalue == 'format':
|
||||||
vals = accessor(sr).split(',')
|
vals = accessor(sr).split(',')
|
||||||
|
elif locvalue == 'author2':
|
||||||
|
m = self.IN_MATCH
|
||||||
|
vals = re.sub(r'(^|\s)(and|not|or|a|the|is|of|,)(\s|$)', ' ', query).split(' ')
|
||||||
else:
|
else:
|
||||||
vals = [accessor(sr)]
|
vals = [accessor(sr)]
|
||||||
if _match(query, vals, m):
|
if self._match(query, vals, m):
|
||||||
matches.add(sr)
|
matches.add(sr)
|
||||||
break
|
break
|
||||||
except ValueError: # Unicode errors
|
except ValueError: # Unicode errors
|
||||||
|
@ -13,7 +13,7 @@ from PyQt4.Qt import (Qt, QDialog, QDialogButtonBox, QTimer, QCheckBox, QLabel,
|
|||||||
QVBoxLayout, QIcon, QWidget, QTabWidget, QGridLayout,
|
QVBoxLayout, QIcon, QWidget, QTabWidget, QGridLayout,
|
||||||
QComboBox)
|
QComboBox)
|
||||||
|
|
||||||
from calibre.gui2 import JSONConfig, info_dialog
|
from calibre.gui2 import JSONConfig, info_dialog, error_dialog
|
||||||
from calibre.gui2.dialogs.choose_format import ChooseFormatDialog
|
from calibre.gui2.dialogs.choose_format import ChooseFormatDialog
|
||||||
from calibre.gui2.progress_indicator import ProgressIndicator
|
from calibre.gui2.progress_indicator import ProgressIndicator
|
||||||
from calibre.gui2.store.config.chooser.chooser_widget import StoreChooserWidget
|
from calibre.gui2.store.config.chooser.chooser_widget import StoreChooserWidget
|
||||||
@ -31,6 +31,8 @@ class SearchDialog(QDialog, Ui_Dialog):
|
|||||||
self.setupUi(self)
|
self.setupUi(self)
|
||||||
|
|
||||||
self.config = JSONConfig('store/search')
|
self.config = JSONConfig('store/search')
|
||||||
|
self.search_title.initialize('store_search_search_title')
|
||||||
|
self.search_author.initialize('store_search_search_author')
|
||||||
self.search_edit.initialize('store_search_search')
|
self.search_edit.initialize('store_search_search')
|
||||||
|
|
||||||
# Loads variables that store various settings.
|
# Loads variables that store various settings.
|
||||||
@ -60,13 +62,24 @@ class SearchDialog(QDialog, Ui_Dialog):
|
|||||||
self.setup_store_checks()
|
self.setup_store_checks()
|
||||||
|
|
||||||
# Set the search query
|
# Set the search query
|
||||||
|
# Title
|
||||||
|
self.search_title.setText(query)
|
||||||
|
self.search_title.setSizeAdjustPolicy(QComboBox.AdjustToMinimumContentsLengthWithIcon)
|
||||||
|
self.search_title.setMinimumContentsLength(25)
|
||||||
|
# Author
|
||||||
|
self.search_author.setText(query)
|
||||||
|
self.search_author.setSizeAdjustPolicy(QComboBox.AdjustToMinimumContentsLengthWithIcon)
|
||||||
|
self.search_author.setMinimumContentsLength(25)
|
||||||
|
# Keyword
|
||||||
self.search_edit.setText(query)
|
self.search_edit.setText(query)
|
||||||
self.search_edit.setSizeAdjustPolicy(QComboBox.AdjustToMinimumContentsLengthWithIcon)
|
self.search_edit.setSizeAdjustPolicy(QComboBox.AdjustToMinimumContentsLengthWithIcon)
|
||||||
self.search_edit.setMinimumContentsLength(25)
|
self.search_edit.setMinimumContentsLength(25)
|
||||||
|
|
||||||
# Create and add the progress indicator
|
# Create and add the progress indicator
|
||||||
self.pi = ProgressIndicator(self, 24)
|
self.pi = ProgressIndicator(self, 24)
|
||||||
self.top_layout.addWidget(self.pi)
|
self.button_layout.takeAt(0)
|
||||||
|
self.button_layout.setAlignment(Qt.AlignCenter)
|
||||||
|
self.button_layout.insertWidget(0, self.pi, 0, Qt.AlignCenter)
|
||||||
|
|
||||||
self.adv_search_button.setIcon(QIcon(I('search.png')))
|
self.adv_search_button.setIcon(QIcon(I('search.png')))
|
||||||
self.configure.setIcon(QIcon(I('config.png')))
|
self.configure.setIcon(QIcon(I('config.png')))
|
||||||
@ -152,8 +165,19 @@ class SearchDialog(QDialog, Ui_Dialog):
|
|||||||
self.results_view.model().clear_results()
|
self.results_view.model().clear_results()
|
||||||
|
|
||||||
# Don't start a search if there is nothing to search for.
|
# Don't start a search if there is nothing to search for.
|
||||||
query = unicode(self.search_edit.text())
|
query = []
|
||||||
|
if self.search_title.text():
|
||||||
|
query.append(u'title:"~%s"' % unicode(self.search_title.text()).replace(" ", ".*"))
|
||||||
|
if self.search_author.text():
|
||||||
|
query.append(u'author2:"%s"' % unicode(self.search_author.text()))
|
||||||
|
#query.append(u'author:"~%s"' % unicode(self.search_author.text()).replace(" ", ".*"))
|
||||||
|
if self.search_edit.text():
|
||||||
|
query.append(unicode(self.search_edit.text()))
|
||||||
|
query = " ".join(query)
|
||||||
if not query.strip():
|
if not query.strip():
|
||||||
|
error_dialog(self, _('No query'),
|
||||||
|
_('You must enter a title, author or keyword to'
|
||||||
|
' search for.'), show=True)
|
||||||
return
|
return
|
||||||
# Give the query to the results model so it can do
|
# Give the query to the results model so it can do
|
||||||
# futher filtering.
|
# futher filtering.
|
||||||
@ -188,7 +212,7 @@ class SearchDialog(QDialog, Ui_Dialog):
|
|||||||
query = query.replace('>', '')
|
query = query.replace('>', '')
|
||||||
query = query.replace('<', '')
|
query = query.replace('<', '')
|
||||||
# Remove the prefix.
|
# Remove the prefix.
|
||||||
for loc in ('all', 'author', 'authors', 'title'):
|
for loc in ('all', 'author', 'author2', 'authors', 'title'):
|
||||||
query = re.sub(r'%s:"(?P<a>[^\s"]+)"' % loc, '\g<a>', query)
|
query = re.sub(r'%s:"(?P<a>[^\s"]+)"' % loc, '\g<a>', query)
|
||||||
query = query.replace('%s:' % loc, '')
|
query = query.replace('%s:' % loc, '')
|
||||||
# Remove the prefix and search text.
|
# Remove the prefix and search text.
|
||||||
|
@ -14,49 +14,74 @@
|
|||||||
<string>Get Books</string>
|
<string>Get Books</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="windowIcon">
|
<property name="windowIcon">
|
||||||
<iconset>
|
<iconset resource="../../../../../resources/images.qrc">
|
||||||
<normaloff>:/images/store.png</normaloff>:/images/store.png</iconset>
|
<normaloff>:/images/store.png</normaloff>:/images/store.png</iconset>
|
||||||
</property>
|
</property>
|
||||||
<property name="sizeGripEnabled">
|
<property name="sizeGripEnabled">
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QVBoxLayout" name="verticalLayout_5">
|
<layout class="QGridLayout" name="gridLayout">
|
||||||
<item>
|
<item row="0" column="2">
|
||||||
<layout class="QHBoxLayout" name="top_layout">
|
<widget class="HistoryLineEdit" name="search_title">
|
||||||
<item>
|
<property name="placeholderText">
|
||||||
<widget class="QLabel" name="label">
|
<string>Search by title</string>
|
||||||
<property name="text">
|
</property>
|
||||||
<string>Query:</string>
|
</widget>
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QToolButton" name="adv_search_button">
|
|
||||||
<property name="text">
|
|
||||||
<string>...</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="HistoryLineEdit" name="search_edit">
|
|
||||||
<property name="sizePolicy">
|
|
||||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
|
||||||
<horstretch>0</horstretch>
|
|
||||||
<verstretch>0</verstretch>
|
|
||||||
</sizepolicy>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QPushButton" name="search">
|
|
||||||
<property name="text">
|
|
||||||
<string>Search</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item row="1" column="0" colspan="2">
|
||||||
|
<widget class="QLabel" name="label_3">
|
||||||
|
<property name="text">
|
||||||
|
<string>&Author:</string>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||||
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>search_author</cstring>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="2">
|
||||||
|
<widget class="HistoryLineEdit" name="search_author">
|
||||||
|
<property name="placeholderText">
|
||||||
|
<string>Search by author</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="0">
|
||||||
|
<widget class="QToolButton" name="adv_search_button">
|
||||||
|
<property name="text">
|
||||||
|
<string>...</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="1">
|
||||||
|
<widget class="QLabel" name="label">
|
||||||
|
<property name="text">
|
||||||
|
<string>&Keyword:</string>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||||
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>search_edit</cstring>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="2">
|
||||||
|
<widget class="HistoryLineEdit" name="search_edit">
|
||||||
|
<property name="sizePolicy">
|
||||||
|
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||||
|
<horstretch>0</horstretch>
|
||||||
|
<verstretch>0</verstretch>
|
||||||
|
</sizepolicy>
|
||||||
|
</property>
|
||||||
|
<property name="placeholderText">
|
||||||
|
<string>Search by any keyword</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="3" column="0" colspan="4">
|
||||||
<widget class="QSplitter" name="store_splitter">
|
<widget class="QSplitter" name="store_splitter">
|
||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
<enum>Qt::Horizontal</enum>
|
<enum>Qt::Horizontal</enum>
|
||||||
@ -82,8 +107,8 @@
|
|||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>193</width>
|
<width>204</width>
|
||||||
<height>127</height>
|
<height>141</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
@ -202,7 +227,7 @@
|
|||||||
</widget>
|
</widget>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item row="4" column="0" colspan="3">
|
||||||
<layout class="QHBoxLayout" name="bottom_layout">
|
<layout class="QHBoxLayout" name="bottom_layout">
|
||||||
<item>
|
<item>
|
||||||
<widget class="QLabel" name="label_2">
|
<widget class="QLabel" name="label_2">
|
||||||
@ -234,7 +259,47 @@
|
|||||||
<item>
|
<item>
|
||||||
<widget class="QPushButton" name="close">
|
<widget class="QPushButton" name="close">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Close</string>
|
<string>&Close</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="1">
|
||||||
|
<widget class="QLabel" name="label_4">
|
||||||
|
<property name="text">
|
||||||
|
<string>&Title:</string>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||||
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>search_title</cstring>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="3" rowspan="3">
|
||||||
|
<layout class="QVBoxLayout" name="button_layout">
|
||||||
|
<item>
|
||||||
|
<widget class="QWidget" name="widget" native="true">
|
||||||
|
<property name="sizePolicy">
|
||||||
|
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||||
|
<horstretch>0</horstretch>
|
||||||
|
<verstretch>0</verstretch>
|
||||||
|
</sizepolicy>
|
||||||
|
</property>
|
||||||
|
<property name="baseSize">
|
||||||
|
<size>
|
||||||
|
<width>24</width>
|
||||||
|
<height>24</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="search">
|
||||||
|
<property name="text">
|
||||||
|
<string>&Search</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
@ -255,17 +320,19 @@
|
|||||||
</customwidget>
|
</customwidget>
|
||||||
</customwidgets>
|
</customwidgets>
|
||||||
<tabstops>
|
<tabstops>
|
||||||
|
<tabstop>search_title</tabstop>
|
||||||
|
<tabstop>search_author</tabstop>
|
||||||
|
<tabstop>adv_search_button</tabstop>
|
||||||
<tabstop>search_edit</tabstop>
|
<tabstop>search_edit</tabstop>
|
||||||
<tabstop>search</tabstop>
|
<tabstop>search</tabstop>
|
||||||
<tabstop>results_view</tabstop>
|
|
||||||
<tabstop>store_list</tabstop>
|
<tabstop>store_list</tabstop>
|
||||||
<tabstop>select_all_stores</tabstop>
|
<tabstop>select_all_stores</tabstop>
|
||||||
<tabstop>select_invert_stores</tabstop>
|
<tabstop>select_invert_stores</tabstop>
|
||||||
<tabstop>select_none_stores</tabstop>
|
<tabstop>select_none_stores</tabstop>
|
||||||
|
<tabstop>results_view</tabstop>
|
||||||
<tabstop>configure</tabstop>
|
<tabstop>configure</tabstop>
|
||||||
<tabstop>open_external</tabstop>
|
<tabstop>open_external</tabstop>
|
||||||
<tabstop>close</tabstop>
|
<tabstop>close</tabstop>
|
||||||
<tabstop>adv_search_button</tabstop>
|
|
||||||
</tabstops>
|
</tabstops>
|
||||||
<resources>
|
<resources>
|
||||||
<include location="../../../../../resources/images.qrc"/>
|
<include location="../../../../../resources/images.qrc"/>
|
||||||
|
@ -594,6 +594,9 @@ class HistoryLineEdit(QComboBox): # {{{
|
|||||||
self.setInsertPolicy(self.NoInsert)
|
self.setInsertPolicy(self.NoInsert)
|
||||||
self.setMaxCount(10)
|
self.setMaxCount(10)
|
||||||
|
|
||||||
|
def setPlaceholderText(self, txt):
|
||||||
|
return self.lineEdit().setPlaceholderText(txt)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def store_name(self):
|
def store_name(self):
|
||||||
return 'lineedit_history_'+self._name
|
return 'lineedit_history_'+self._name
|
||||||
|
Loading…
x
Reference in New Issue
Block a user