diff --git a/src/calibre/gui2/store/search.py b/src/calibre/gui2/store/search.py index 1d263959ef..ce74d52547 100644 --- a/src/calibre/gui2/store/search.py +++ b/src/calibre/gui2/store/search.py @@ -8,6 +8,7 @@ __docformat__ = 'restructuredtext en' import re import time +import traceback from contextlib import closing from random import shuffle from threading import Thread @@ -20,9 +21,12 @@ from calibre import browser from calibre.gui2 import NONE from calibre.gui2.progress_indicator import ProgressIndicator from calibre.gui2.store.search_ui import Ui_Dialog +from calibre.library.caches import _match, CONTAINS_MATCH, EQUALS_MATCH, \ + REGEXP_MATCH from calibre.utils.config import DynamicConfig from calibre.utils.icu import sort_key from calibre.utils.magick.draw import thumbnail +from calibre.utils.search_query_parser import SearchQueryParser HANG_TIME = 75000 # milliseconds seconds TIMEOUT = 75 # seconds @@ -290,11 +294,15 @@ class SearchThread(Thread): while self._run and not self.tasks.empty(): try: query, store_name, store_plugin, timeout = self.tasks.get() - for res in store_plugin.search(query, timeout=timeout): + squery = query + for loc in SearchFilter.USABLE_LOCATIONS: + squery = re.sub(r'%s:"?(?P[^\s"]+)"?' % loc, '\g', squery) + for res in store_plugin.search(squery, timeout=timeout): if not self._run: return res.store_name = store_name - self.results.put(res) + if SearchFilter(res).parse(query): + self.results.put(res) self.tasks.task_done() except: pass @@ -450,3 +458,82 @@ class Matches(QAbstractItemModel): if reset: self.reset() + +class SearchFilter(SearchQueryParser): + + USABLE_LOCATIONS = [ + 'all', + 'author', + 'authors', + 'cover', + 'price', + 'title', + 'store', + ] + + def __init__(self, search_result): + SearchQueryParser.__init__(self, locations=self.USABLE_LOCATIONS) + self.search_result = search_result + + def universal_set(self): + return set([self.search_result]) + + def get_matches(self, location, query): + location = location.lower().strip() + if location == 'authors': + location = 'author' + + matchkind = CONTAINS_MATCH + if len(query) > 1: + if query.startswith('\\'): + query = query[1:] + elif query.startswith('='): + matchkind = EQUALS_MATCH + query = query[1:] + elif query.startswith('~'): + matchkind = REGEXP_MATCH + query = query[1:] + if matchkind != REGEXP_MATCH: ### leave case in regexps because it can be significant e.g. \S \W \D + query = query.lower() + + if location not in self.USABLE_LOCATIONS: + return set([]) + matches = set([]) + all_locs = set(self.USABLE_LOCATIONS) - set(['all']) + locations = all_locs if location == 'all' else [location] + q = { + 'author': self.search_result.author.lower(), + 'cover': self.search_result.cover_url, + 'format': '', + 'price': self.search_result.price, + 'store': self.search_result.store_name.lower(), + 'title': self.search_result.title.lower(), + } + for x in ('author', 'format'): + q[x+'s'] = q[x] + for locvalue in locations: + ac_val = q[locvalue] + if query == 'true': + if ac_val is not None: + matches.add(self.search_result) + continue + if query == 'false': + if ac_val is None: + matches.add(self.search_result) + continue + try: + ### 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 + ### exactmatch searches into contains searches. + if locvalue == 'author' and matchkind == EQUALS_MATCH: + m = CONTAINS_MATCH + else: + m = matchkind + + vals = [ac_val] + if _match(query, vals, m): + matches.add(self.search_result) + break + except ValueError: # Unicode errors + traceback.print_exc() + return matches