diff --git a/searx/query.py b/searx/query.py index 751308baa..49fa89a9c 100644 --- a/searx/query.py +++ b/searx/query.py @@ -150,7 +150,7 @@ class LanguageParser(QueryPartParser): class ExternalBangParser(QueryPartParser): @staticmethod def check(raw_value): - return raw_value.startswith('!!') + return raw_value.startswith('!!') and len(raw_value) > 2 def __call__(self, raw_value): value = raw_value[2:] @@ -177,7 +177,8 @@ class ExternalBangParser(QueryPartParser): class BangParser(QueryPartParser): @staticmethod def check(raw_value): - return raw_value[0] == '!' + # make sure it's not any bang with double '!!' + return raw_value[0] == '!' and (len(raw_value) < 2 or raw_value[1] != '!') def __call__(self, raw_value): value = raw_value[1:].replace('-', ' ').replace('_', ' ') @@ -235,14 +236,25 @@ class BangParser(QueryPartParser): self._add_autocomplete(first_char + engine_shortcut) +class FeelingLuckyParser(QueryPartParser): + @staticmethod + def check(raw_value): + return raw_value == '!!' + + def __call__(self, raw_value): + self.raw_text_query.redirect_to_first_result = True + return True + + class RawTextQuery: """parse raw text query (the value from the html input)""" PARSER_CLASSES = [ - TimeoutParser, # this force the timeout - LanguageParser, # this force a language + TimeoutParser, # force the timeout + LanguageParser, # force a language ExternalBangParser, # external bang (must be before BangParser) - BangParser, # this force a engine or category + BangParser, # force an engine or category + FeelingLuckyParser, # redirect to the first link in the results list ] def __init__(self, query, disabled_engines): @@ -261,6 +273,7 @@ class RawTextQuery: self.query_parts = [] # use self.getFullQuery() self.user_query_parts = [] # use self.getQuery() self.autocomplete_location = None + self.redirect_to_first_result = False self._parse_query() def _parse_query(self): @@ -330,5 +343,6 @@ class RawTextQuery: + f"enginerefs={self.enginerefs!r}\n " + f"autocomplete_list={self.autocomplete_list!r}\n " + f"query_parts={self.query_parts!r}\n " - + f"user_query_parts={self.user_query_parts!r} >" + + f"user_query_parts={self.user_query_parts!r} >\n" + + f"redirect_to_first_result={self.redirect_to_first_result!r}" ) diff --git a/searx/search/models.py b/searx/search/models.py index 91e5d5982..ec1188fbb 100644 --- a/searx/search/models.py +++ b/searx/search/models.py @@ -37,6 +37,7 @@ class SearchQuery: 'timeout_limit', 'external_bang', 'engine_data', + 'redirect_to_first_result', ) def __init__( @@ -50,6 +51,7 @@ class SearchQuery: timeout_limit: typing.Optional[float] = None, external_bang: typing.Optional[str] = None, engine_data: typing.Optional[typing.Dict[str, str]] = None, + redirect_to_first_result: typing.Optional[bool] = None, ): self.query = query self.engineref_list = engineref_list @@ -60,6 +62,7 @@ class SearchQuery: self.timeout_limit = timeout_limit self.external_bang = external_bang self.engine_data = engine_data or {} + self.redirect_to_first_result = redirect_to_first_result self.locale = None if self.lang: @@ -73,7 +76,7 @@ class SearchQuery: return list(set(map(lambda engineref: engineref.category, self.engineref_list))) def __repr__(self): - return "SearchQuery({!r}, {!r}, {!r}, {!r}, {!r}, {!r}, {!r}, {!r})".format( + return "SearchQuery({!r}, {!r}, {!r}, {!r}, {!r}, {!r}, {!r}, {!r}, {!r})".format( self.query, self.engineref_list, self.lang, @@ -82,6 +85,7 @@ class SearchQuery: self.time_range, self.timeout_limit, self.external_bang, + self.redirect_to_first_result, ) def __eq__(self, other): @@ -94,6 +98,7 @@ class SearchQuery: and self.time_range == other.time_range and self.timeout_limit == other.timeout_limit and self.external_bang == other.external_bang + and self.redirect_to_first_result == other.redirect_to_first_result ) def __hash__(self): @@ -107,6 +112,7 @@ class SearchQuery: self.time_range, self.timeout_limit, self.external_bang, + self.redirect_to_first_result, ) ) @@ -121,4 +127,5 @@ class SearchQuery: self.timeout_limit, self.external_bang, self.engine_data, + self.redirect_to_first_result, ) diff --git a/searx/webadapter.py b/searx/webadapter.py index 121319eeb..9fbb8ea3e 100644 --- a/searx/webadapter.py +++ b/searx/webadapter.py @@ -254,6 +254,7 @@ def get_search_query_from_webapp( query_time_range = parse_time_range(form) query_timeout = parse_timeout(form, raw_text_query) external_bang = raw_text_query.external_bang + redirect_to_first_result = raw_text_query.redirect_to_first_result engine_data = parse_engine_data(form) query_lang = parse_lang(preferences, form, raw_text_query) @@ -288,6 +289,7 @@ def get_search_query_from_webapp( query_timeout, external_bang=external_bang, engine_data=engine_data, + redirect_to_first_result=redirect_to_first_result, ), raw_text_query, query_engineref_list_unknown, diff --git a/searx/webapp.py b/searx/webapp.py index fe6dd6b74..b2a76ff92 100755 --- a/searx/webapp.py +++ b/searx/webapp.py @@ -697,6 +697,10 @@ def search(): previous_result = None results = result_container.get_ordered_results() + + if search_query.redirect_to_first_result and results: + return redirect(results[0]['url'], 302) + for result in results: if output_format == 'html': if 'content' in result and result['content']: diff --git a/tests/unit/test_query.py b/tests/unit/test_query.py index 7274a8da5..59bbd6574 100644 --- a/tests/unit/test_query.py +++ b/tests/unit/test_query.py @@ -225,18 +225,6 @@ class TestExternalBangParser(SearxTestCase): a = query.autocomplete_list[0] self.assertEqual(query.get_autocomplete_full_query(a), a + ' the query') - def test_external_bang_autocomplete_empty(self): - query_text = 'the query !!' - query = RawTextQuery(query_text, []) - - self.assertEqual(query.getFullQuery(), 'the query !!') - self.assertEqual(len(query.query_parts), 0) - self.assertFalse(query.specific) - self.assertGreater(len(query.autocomplete_list), 2) - - a = query.autocomplete_list[0] - self.assertEqual(query.get_autocomplete_full_query(a), 'the query ' + a) - class TestBang(SearxTestCase): diff --git a/tests/unit/test_search.py b/tests/unit/test_search.py index 11cb8d4e4..87f5f280b 100644 --- a/tests/unit/test_search.py +++ b/tests/unit/test_search.py @@ -27,7 +27,7 @@ class SearchQueryTestCase(SearxTestCase): def test_repr(self): s = SearchQuery('test', [EngineRef('bing', 'general')], 'all', 0, 1, '1', 5.0, 'g') self.assertEqual( - repr(s), "SearchQuery('test', [EngineRef('bing', 'general')], 'all', 0, 1, '1', 5.0, 'g')" + repr(s), "SearchQuery('test', [EngineRef('bing', 'general')], 'all', 0, 1, '1', 5.0, 'g', None)" ) # noqa def test_eq(self):