on b8de673 help sign in
19 files changed+137−405
since splitsnapshots
unifiedsplit
.github/workflows/data-update.yml+1−1 .github/workflows/integration.yml+3−32 Dockerfile+4−5 manage+4−4 requirements-dev.txt+10−11 requirements.txt+3−3 searx/autocomplete.py+6−6 searx/engines/gigablast.py+13−24 searx/engines/google.py+11−20 searx/engines/startpage.py+20−100 searx/engines/tineye.py−86 searx/network/__init__.py+24−23 searx/network/client.py+8−1 searx/network/network.py−8 searx/results.py+2−8 searx/settings.yml+8−19 searx/utils.py+6−12 searx/webapp.py+13−41 utils/searx.sh+1−1
.github/workflows/data-update.yml+1−1
@@ -29,7 +29,7 @@ jobs:
2929 - name: Set up Python
3030 uses: actions/setup-python@v2
3131 with:
32− python-version: '3.10'
32+ python-version: '3.9'
3333 architecture: 'x64'
3434
3535 - name: Install Python dependencies
.github/workflows/integration.yml+3−32
@@ -13,7 +13,7 @@ jobs:
1313 strategy:
1414 matrix:
1515 os: [ubuntu-20.04]
16− python-version: [3.7, 3.8, 3.9, "3.10"]
16+ python-version: [3.6, 3.7, 3.8, 3.9]
1717 steps:
1818 - name: Checkout
1919 uses: actions/checkout@v2
@@ -56,17 +56,6 @@ jobs:
5656 uses: actions/checkout@v2
5757 - name: Install Ubuntu packages
5858 run: sudo ./utils/searx.sh install packages
59− - name: Set up Python
60− uses: actions/setup-python@v2
61− with:
62− python-version: '3.9'
63− architecture: 'x64'
64− - name: Cache Python dependencies
65− id: cache-python
66− uses: actions/cache@v2
67− with:
68− path: ./local
69− key: python-ubuntu-20.04-3.9-${{ hashFiles('requirements*.txt', 'setup.py') }}
7059 - name: Install node dependencies
7160 run: make V=1 node.env
7261 - name: Build themes
@@ -79,21 +68,14 @@ jobs:
7968 - name: Checkout
8069 uses: actions/checkout@v2
8170 with:
82− fetch-depth: '0'
8371 persist-credentials: false
8472 - name: Install Ubuntu packages
8573 run: sudo ./utils/searx.sh install buildhost
8674 - name: Set up Python
8775 uses: actions/setup-python@v2
8876 with:
89− python-version: '3.10'
77+ python-version: '3.9'
9078 architecture: 'x64'
91− - name: Cache Python dependencies
92− id: cache-python
93− uses: actions/cache@v2
94− with:
95− path: ./local
96− key: python-ubuntu-20.04-3.9-${{ hashFiles('requirements*.txt', 'setup.py') }}
9779 - name: Build documentation
9880 run: |
9981 make V=1 docs.clean docs.html
@@ -115,7 +97,7 @@ jobs:
11597 - documentation
11698 env:
11799 DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
118− runs-on: ubuntu-20.04
100+ runs-on: ubuntu-18.04
119101 steps:
120102 - name: Checkout
121103 if: env.DOCKERHUB_USERNAME != null
@@ -123,17 +105,6 @@ jobs:
123105 with:
124106 # make sure "make docker.push" can get the git history
125107 fetch-depth: '0'
126− - name: Set up Python
127− uses: actions/setup-python@v2
128− with:
129− python-version: '3.9'
130− architecture: 'x64'
131− - name: Cache Python dependencies
132− id: cache-python
133− uses: actions/cache@v2
134− with:
135− path: ./local
136− key: python-ubuntu-20.04-3.9-${{ hashFiles('requirements*.txt', 'setup.py') }}
137108 - name: Set up QEMU
138109 if: env.DOCKERHUB_USERNAME != null
139110 uses: docker/setup-qemu-action@v1
Dockerfile+4−5
@@ -1,4 +1,4 @@
1−FROM alpine:3.15
1+FROM alpine:3.12
22ENTRYPOINT ["/sbin/tini","--","/usr/local/searx/dockerfiles/docker-entrypoint.sh"]
33EXPOSE 8080
44VOLUME /etc/searx
@@ -46,19 +46,18 @@ RUN apk upgrade --no-cache \
4646 uwsgi \
4747 uwsgi-python3 \
4848 brotli \
49− && pip3 install --upgrade pip wheel setuptools \
49+ && pip3 install --upgrade pip \
5050 && pip3 install --no-cache -r requirements.txt \
5151 && apk del build-dependencies \
5252 && rm -rf /root/.cache
5353
54−COPY searx ./searx
55−COPY dockerfiles ./dockerfiles
54+COPY --chown=searx:searx . .
5655
5756ARG TIMESTAMP_SETTINGS=0
5857ARG TIMESTAMP_UWSGI=0
5958ARG VERSION_GITCOMMIT=unknown
6059
61−RUN /usr/bin/python3 -m compileall -q searx; \
60+RUN su searx -c "/usr/bin/python3 -m compileall -q searx"; \
6261 touch -c --date=@${TIMESTAMP_SETTINGS} searx/settings.yml; \
6362 touch -c --date=@${TIMESTAMP_UWSGI} dockerfiles/uwsgi.ini; \
6463 if [ ! -z $VERSION_GITCOMMIT ]; then\
manage+4−4
@@ -13,7 +13,7 @@ source_dot_config
1313
1414PY_SETUP_EXTRAS='[test]'
1515NPM_PACKAGES="less@2.7 less-plugin-clean-css grunt-cli"
16−GECKODRIVER_VERSION="v0.30.0"
16+GECKODRIVER_VERSION="v0.28.0"
1717# SPHINXOPTS=
1818
1919# These py files are linted by test.pylint(), all other files are linted by
@@ -224,7 +224,7 @@ docker.build() {
224224
225225 # build Docker image
226226 build_msg DOCKER "Building image ${SEARX_IMAGE_NAME}:${SEARX_GIT_VERSION}"
227− docker build \
227+ sudo docker build \
228228 --build-arg GIT_URL="${GIT_URL}" \
229229 --build-arg SEARX_GIT_VERSION="${SEARX_GIT_VERSION}" \
230230 --build-arg VERSION_GITCOMMIT="${VERSION_GITCOMMIT}" \
@@ -236,8 +236,8 @@ docker.build() {
236236 -t "${SEARX_IMAGE_NAME}:latest" -t "${SEARX_IMAGE_NAME}:${SEARX_GIT_VERSION}" .
237237
238238 if [ "$1" = "push" ]; then
239− docker push "${SEARX_IMAGE_NAME}:latest"
240− docker push "${SEARX_IMAGE_NAME}:${SEARX_GIT_VERSION}"
239+ sudo docker push "${SEARX_IMAGE_NAME}:latest"
240+ sudo docker push "${SEARX_IMAGE_NAME}:${SEARX_GIT_VERSION}"
241241 fi
242242 )
243243 dump_return $?
requirements-dev.txt+10−11
@@ -1,20 +1,19 @@
11mock==4.0.3
22nose2[coverage_plugin]==0.10.0
33cov-core==1.15.0
4−pycodestyle==2.8.0
4+pycodestyle==2.7.0
55pylint==2.12.2
6−splinter==0.17.0
7−transifex-client==0.14.3; python_version < '3.10'
8−transifex-client==0.12.1; python_version == '3.10'
9−selenium==4.1.0
10−twine==3.7.1
11−Pallets-Sphinx-Themes==2.0.2
6+splinter==0.15.0
7+transifex-client==0.14.3
8+selenium==3.141.0
9+twine==3.4.2
10+Pallets-Sphinx-Themes==2.0.1
1211docutils==0.16
13−Sphinx==4.4.0
14−sphinx-issues==3.0.1
15−sphinx-jinja==1.4.0
12+Sphinx==4.2.0
13+sphinx-issues==1.2.0
14+sphinx-jinja==1.1.1
1615sphinx-tabs==3.2.0
1716sphinxcontrib-programoutput==0.17
1817sphinx-autobuild==2021.3.14
19−linuxdoc==20211220
18+linuxdoc==20210324
2019aiounittest==1.4.1
requirements.txt+3−3
@@ -1,9 +1,9 @@
11certifi==2021.10.8
22babel==2.9.1
33flask-babel==2.0.0
4−flask==2.0.2
5−jinja2==3.0.3
6−lxml==4.7.1
4+flask==1.1.2
5+jinja2==3.0.2
6+lxml==4.6.3
77pygments==2.8.0
88python-dateutil==2.8.2
99pyyaml==6.0
searx/autocomplete.py+6−6
@@ -24,7 +24,6 @@ from httpx import HTTPError
2424
2525
2626from searx import settings
27−from searx.data import ENGINES_LANGUAGES
2827from searx.network import get as http_get
2928from searx.exceptions import SearxEngineResponseException
3029
@@ -94,11 +93,12 @@ def google(query, lang):
9493
9594def startpage(query, lang):
9695 # startpage autocompleter
97− lui = ENGINES_LANGUAGES['startpage'].get(lang, 'english')
98− url = 'https://startpage.com/suggestions?{query}'
99− resp = get(url.format(query=urlencode({'q': query, 'segment': 'startpage.udog', 'lui': lui})))
100− data = resp.json()
101− return [e['text'] for e in data.get('suggestions', []) if 'text' in e]
96+ url = 'https://startpage.com/do/suggest?{query}'
97+
98+ resp = get(url.format(query=urlencode({'query': query}))).text.split('\n')
99+ if len(resp) > 1:
100+ return resp
101+ return []
102102
103103
104104def swisscows(query, lang):
searx/engines/gigablast.py+13−24
@@ -5,10 +5,10 @@
55# pylint: disable=missing-function-docstring, invalid-name
66
77import re
8−from json import loads, JSONDecodeError
8+from json import loads
99from urllib.parse import urlencode
10+# from searx import logger
1011from searx.network import get
11−from searx.exceptions import SearxEngineResponseException
1212
1313# about
1414about = {
@@ -22,9 +22,6 @@ about = {
2222
2323# engine dependent config
2424categories = ['general']
25−collections = 'main'
26−search_type = ''
27−fast = 0
2825# gigablast's pagination is totally damaged, don't use it
2926paging = False
3027safesearch = True
@@ -37,8 +34,6 @@ base_url = 'https://gigablast.com'
3734extra_param = ''
3835extra_param_path='/search?c=main&qlangcountry=en-us&q=south&s=10'
3936
40−_wait_for_results_msg = 'Loading results takes too long. Please enable fast option in gigablast engine.'
41−
4237def parse_extra_param(text):
4338
4439 # example:
@@ -59,6 +54,7 @@ def parse_extra_param(text):
5954 if re_var is not None and re_var.search(line):
6055 extra_param += re_var.search(line).group(1)
6156 break
57+ # logger.debug('gigablast extra_param="%s"', extra_param)
6258
6359def init(engine_settings=None): # pylint: disable=unused-argument
6460 parse_extra_param(get(base_url + extra_param_path).text)
@@ -69,17 +65,14 @@ def request(query, params): # pylint: disable=unused-argument
6965
7066 # see API http://www.gigablast.com/api.html#/search
7167 # Take into account, that the API has some quirks ..
72− query_args = {
73− 'c': collections,
74− 'format': 'json',
75− 'q': query,
76− 'dr': 1 ,
77− 'showgoodimages': 0,
78− 'fast': fast,
79− }
80−
81− if search_type != '':
82− query_args['searchtype'] = search_type
68+
69+ query_args = dict(
70+ c = 'main'
71+ , format = 'json'
72+ , q = query
73+ , dr = 1
74+ , showgoodimages = 0
75+ )
8376
8477 if params['language'] and params['language'] != 'all':
8578 query_args['qlangcountry'] = params['language']
@@ -97,13 +90,9 @@ def request(query, params): # pylint: disable=unused-argument
9790def response(resp):
9891 results = []
9992
100− try:
101− response_json = loads(resp.text)
102− except JSONDecodeError as e:
103− if 'Waiting for results' in resp.text:
104− raise SearxEngineResponseException(message=_wait_for_results_msg) # pylint: disable=raise-missing-from
105− raise e
93+ response_json = loads(resp.text)
10694
95+ # logger.debug('gigablast returns %s results', len(response_json['results']))
10796
10897 for result in response_json['results']:
10998 # see "Example JSON Output (&format=json)"
searx/engines/google.py+11−20
@@ -8,7 +8,7 @@ Definitions`_.
88 https://developers.google.com/custom-search/docs/xml_results#WebSearch_Query_Parameter_Definitions
99"""
1010
11−# pylint: disable=invalid-name, missing-function-docstring, too-many-branches
11+# pylint: disable=invalid-name, missing-function-docstring
1212
1313from urllib.parse import urlencode
1414from lxml import html
@@ -108,9 +108,8 @@ filter_mapping = {
108108# specific xpath variables
109109# ------------------------
110110
111−# google results are grouped into <div class="g ..." ../>
112−results_xpath = '//div[@id="search"]//div[contains(@class, "g ")]'
113−results_xpath_mobile_ui = '//div[contains(@class, "g ")]'
111+# google results are grouped into <div class="g" ../>
112+results_xpath = '//div[@class="g"]'
114113
115114# google *sections* are no usual *results*, we ignore them
116115g_section_with_header = './g-section-with-header'
@@ -122,8 +121,8 @@ title_xpath = './/h3[1]'
122121# href=...>
123122href_xpath = './/div[@class="yuRUbf"]//a/@href'
124123
125−# in the result group there is <div class="VwiC3b ..." ../> containing the *content*
126−content_xpath = './/div[contains(@class, "VwiC3b")]'
124+# in the result group there is <div class="IsZvec" ../> containing he *content*
125+content_xpath = './/div[@class="IsZvec"]'
127126
128127# Suggestions are links placed in a *card-section*, we extract only the text
129128# from the links not the links itself.
@@ -275,12 +274,7 @@ def response(resp):
275274 logger.error(e, exc_info=True)
276275
277276 # parse results
278−
279− _results_xpath = results_xpath
280− if use_mobile_ui:
281− _results_xpath = results_xpath_mobile_ui
282−
283− for result in eval_xpath_list(dom, _results_xpath):
277+ for result in eval_xpath_list(dom, results_xpath):
284278
285279 # google *sections*
286280 if extract_text(eval_xpath(result, g_section_with_header)):
@@ -291,27 +285,24 @@ def response(resp):
291285 title_tag = eval_xpath_getindex(result, title_xpath, 0, default=None)
292286 if title_tag is None:
293287 # this not one of the common google results *section*
294− logger.debug('ingoring item from the result_xpath list: missing title')
288+ logger.debug('ingoring <div class="g" ../> section: missing title')
295289 continue
296290 title = extract_text(title_tag)
297291 url = eval_xpath_getindex(result, href_xpath, 0, None)
298292 if url is None:
299293 continue
300294 content = extract_text(eval_xpath_getindex(result, content_xpath, 0, default=None), allow_none=True)
301− if content is None:
302− logger.debug('ingoring item from the result_xpath list: missing content of title "%s"', title)
303− continue
304−
305− logger.debug('add link to results: %s', title)
306−
307295 results.append({
308296 'url': url,
309297 'title': title,
310298 'content': content
311299 })
312−
313300 except Exception as e: # pylint: disable=broad-except
314301 logger.error(e, exc_info=True)
302+ # from lxml import etree
303+ # logger.debug(etree.tostring(result, pretty_print=True))
304+ # import pdb
305+ # pdb.set_trace()
315306 continue
316307
317308 # parse suggestion
searx/engines/startpage.py+20−100
@@ -1,30 +1,16 @@
11# SPDX-License-Identifier: AGPL-3.0-or-later
2−# lint: pylint
3−"""Startpage (Web)
4−
2+"""
3+ Startpage (Web)
54"""
65
6+from lxml import html
7+from dateutil import parser
8+from datetime import datetime, timedelta
79import re
8−from time import time
9−
10−from urllib.parse import urlencode
1110from unicodedata import normalize, combining
12−from datetime import datetime, timedelta
13−
14−from dateutil import parser
15−from lxml import html
1611from babel import Locale
1712from babel.localedata import locale_identifiers
18−
19−from searx import logger
20−from searx.network import get
2113from searx.utils import extract_text, eval_xpath, match_language
22−from searx.exceptions import (
23− SearxEngineResponseException,
24− SearxEngineCaptchaException,
25−)
26−
27−logger = logger.getChild('startpage')
2814
2915# about
3016about = {
@@ -47,7 +33,7 @@ supported_languages_url = 'https://www.startpage.com/do/settings'
4733
4834# search-url
4935base_url = 'https://startpage.com/'
50−search_url = base_url + 'sp/search?'
36+search_url = base_url + 'do/search'
5137
5238# specific xpath variables
5339# ads xpath //div[@id="results"]/div[@id="sponsored"]//div[@class="result"]
@@ -56,74 +42,18 @@ results_xpath = '//div[@class="w-gl__result__main"]'
5642link_xpath = './/a[@class="w-gl__result-title result-link"]'
5743content_xpath = './/p[@class="w-gl__description"]'
5844
59−# timestamp of the last fetch of 'sc' code
60−sc_code_ts = 0
61−sc_code = ''
62−
63−
64−def raise_captcha(resp):
65−
66− if str(resp.url).startswith('https://www.startpage.com/sp/captcha'):
67− # suspend CAPTCHA for 7 days
68− raise SearxEngineCaptchaException(suspended_time=7 * 24 * 3600)
69−
70−
71−def get_sc_code(headers):
72− """Get an actual `sc` argument from startpage's home page.
73−
74− Startpage puts a `sc` argument on every link. Without this argument
75− startpage considers the request is from a bot. We do not know what is
76− encoded in the value of the `sc` argument, but it seems to be a kind of a
77− *time-stamp*. This *time-stamp* is valid for a few hours.
78−
79− This function scrap a new *time-stamp* from startpage's home page every hour
80− (3000 sec).
81−
82− """
83−
84− global sc_code_ts, sc_code # pylint: disable=global-statement
85−
86− if time() > (sc_code_ts + 3000):
87− logger.debug("query new sc time-stamp ...")
88−
89− resp = get(base_url, headers=headers)
90− raise_captcha(resp)
91− dom = html.fromstring(resp.text)
92−
93− try:
94− # href --> '/?sc=adrKJMgF8xwp20'
95− href = eval_xpath(dom, '//a[@class="footer-home__logo"]')[0].get('href')
96− except IndexError as exc:
97− # suspend startpage API --> https://github.com/searxng/searxng/pull/695
98− raise SearxEngineResponseException(
99− suspended_time=7 * 24 * 3600, message="PR-695: query new sc time-stamp failed!"
100− ) from exc
101−
102− sc_code = href[5:]
103− sc_code_ts = time()
104− logger.debug("new value is: %s", sc_code)
105−
106− return sc_code
107−
10845
10946# do search-request
11047def request(query, params):
11148
112− # pylint: disable=line-too-long
113− # The format string from Startpage's FFox add-on [1]::
114− #
115− # https://www.startpage.com/do/dsearch?query={searchTerms}&cat=web&pl=ext-ff&language=__MSG_extensionUrlLanguage__&extVersion=1.3.0
116− #
117− # [1] https://addons.mozilla.org/en-US/firefox/addon/startpage-private-search/
118−
119− args = {
49+ params['url'] = search_url
50+ params['method'] = 'POST'
51+ params['data'] = {
12052 'query': query,
12153 'page': params['pageno'],
12254 'cat': 'web',
123− # 'pl': 'ext-ff',
124− # 'extVersion': '1.3.0',
125− # 'abp': "-1",
126− 'sc': get_sc_code(params['headers']),
55+ 'cmd': 'process_search',
56+ 'engine0': 'v1all',
12757 }
12858
12959 # set language if specified
@@ -131,10 +61,9 @@ def request(query, params):
13161 lang_code = match_language(params['language'], supported_languages, fallback=None)
13262 if lang_code:
13363 language_name = supported_languages[lang_code]['alias']
134− args['language'] = language_name
135− args['lui'] = language_name
64+ params['data']['language'] = language_name
65+ params['data']['lui'] = language_name
13666
137− params['url'] = search_url + urlencode(args)
13867 return params
13968
14069
@@ -210,11 +139,10 @@ def response(resp):
210139
211140# get supported languages from their site
212141def _fetch_supported_languages(resp):
213− # startpage's language selector is a mess each option has a displayed name
214− # and a value, either of which may represent the language name in the native
215− # script, the language name in English, an English transliteration of the
216− # native name, the English name of the writing script used by the language,
217− # or occasionally something else entirely.
142+ # startpage's language selector is a mess
143+ # each option has a displayed name and a value, either of which may represent the language name
144+ # in the native script, the language name in English, an English transliteration of the native name,
145+ # the English name of the writing script used by the language, or occasionally something else entirely.
218146
219147 # this cases are so special they need to be hardcoded, a couple of them are mispellings
220148 language_names = {
@@ -228,15 +156,7 @@ def _fetch_supported_languages(resp):
228156 }
229157
230158 # get the English name of every language known by babel
231− language_names.update(
232− {
233− # fmt: off
234− name.lower(): lang_code
235− # pylint: disable=protected-access
236− for lang_code, name in Locale('en')._data['languages'].items()
237− # fmt: on
238− }
239− )
159+ language_names.update({name.lower(): lang_code for lang_code, name in Locale('en')._data['languages'].items()})
240160
241161 # get the native name of every language known by babel
242162 for lang_code in filter(lambda lang_code: lang_code.find('_') == -1, locale_identifiers()):
@@ -261,8 +181,8 @@ def _fetch_supported_languages(resp):
261181 if isinstance(lang_code, str):
262182 supported_languages[lang_code] = {'alias': sp_option_value}
263183 elif isinstance(lang_code, list):
264− for _lc in lang_code:
265− supported_languages[_lc] = {'alias': sp_option_value}
184+ for lc in lang_code:
185+ supported_languages[lc] = {'alias': sp_option_value}
266186 else:
267187 print('Unknown language option in Startpage: {} ({})'.format(sp_option_value, sp_option_text))
268188
searx/engines/tineye.pydeleted−86
@@ -1,86 +0,0 @@
1−"""
2−Tineye - Reverse search images
3−"""
4−
5−from json import loads
6−from urllib.parse import urlencode
7−
8−from datetime import datetime
9−
10−about = {
11− "website": "https://tineye.com",
12− "wikidata_id": "Q2382535",
13− "use_official_api": False,
14− "require_api_key": False,
15− "results": "JSON",
16−}
17−
18−
19−categories = ['images']
20−paging = True
21−
22−safesearch = False
23−
24−
25−base_url = 'https://tineye.com'
26−search_string = '/result_json/?page={page}&{query}'
27−
28−
29−def request(query, params):
30− params['url'] = base_url +\
31− search_string.format(
32− query=urlencode({'url': query}),
33− page=params['pageno'])
34−
35− params['headers'].update({
36− 'Connection': 'keep-alive',
37− 'Accept-Encoding': 'gzip, defalte, br',
38− 'Host': 'tineye.com',
39− 'DNT': '1',
40− 'TE': 'trailers',
41− })
42−
43− return params
44−
45−
46−def response(resp):
47− results = []
48− # Define wanted results
49− json_data = loads(resp.text)
50− number_of_results = json_data['num_matches']
51−
52− for i in json_data['matches']:
53− for i in json_data['matches']:
54− image_format = i['format']
55− width = i['width']
56− height = i['height']
57− thumbnail_src = i['image_url']
58− backlink = i['domains'][0]['backlinks'][0]
59−
60− url = backlink['backlink']
61− source = backlink['url']
62− title = backlink['image_name']
63− img_src = backlink['url']
64−
65− # Get and convert published date
66− api_date = backlink['crawl_date'][:-3]
67− publishedDate = datetime.fromisoformat(api_date)
68−
69− # Append results
70− results.append({
71− 'template': 'images.html',
72− 'url': url,
73− 'thumbnail_src': thumbnail_src,
74− 'source': source,
75− 'title': title,
76− 'img_src': img_src,
77− 'format': image_format,
78− 'widht': width,
79− 'height': height,
80− 'publishedDate': publishedDate,
81− })
82−
83− # Append number of results
84− results.append({'number_of_results': number_of_results})
85−
86− return results
searx/network/__init__.py+24−23
@@ -4,8 +4,6 @@ import asyncio
44import threading
55import concurrent.futures
66from time import time
7−from queue import SimpleQueue
8−from types import MethodType
97
108import httpx
119import h2.exceptions
@@ -14,6 +12,29 @@ from .network import get_network, initialize, check_network_configuration
1412from .client import get_loop
1513from .raise_for_httperror import raise_for_httperror
1614
15+# queue.SimpleQueue: Support Python 3.6
16+try:
17+ from queue import SimpleQueue
18+except ImportError:
19+ from queue import Empty
20+ from collections import deque
21+
22+ class SimpleQueue:
23+ """Minimal backport of queue.SimpleQueue"""
24+
25+ def __init__(self):
26+ self._queue = deque()
27+ self._count = threading.Semaphore(0)
28+
29+ def put(self, item):
30+ self._queue.append(item)
31+ self._count.release()
32+
33+ def get(self):
34+ if not self._count.acquire(True):
35+ raise Empty
36+ return self._queue.popleft()
37+
1738
1839THREADLOCAL = threading.local()
1940
@@ -135,27 +156,15 @@ async def stream_chunk_to_queue(network, q, method, url, **kwargs):
135156 try:
136157 async with await network.stream(method, url, **kwargs) as response:
137158 q.put(response)
138− # aiter_raw: access the raw bytes on the response without applying any HTTP content decoding
139− # https://www.python-httpx.org/quickstart/#streaming-responses
140159 async for chunk in response.aiter_bytes(65536):
141160 if len(chunk) > 0:
142161 q.put(chunk)
143− except httpx.ResponseClosed as e:
144− # the response was closed
145− pass
146162 except (httpx.HTTPError, OSError, h2.exceptions.ProtocolError) as e:
147163 q.put(e)
148164 finally:
149165 q.put(None)
150166
151167
152−def _close_response_method(self):
153− asyncio.run_coroutine_threadsafe(
154− self.aclose(),
155− get_loop()
156− )
157−
158−
159168def stream(method, url, **kwargs):
160169 """Replace httpx.stream.
161170
@@ -171,18 +180,10 @@ def stream(method, url, **kwargs):
171180 q = SimpleQueue()
172181 future = asyncio.run_coroutine_threadsafe(stream_chunk_to_queue(get_network(), q, method, url, **kwargs),
173182 get_loop())
174− # yield response
175− response = q.get()
176− if isinstance(response, Exception):
177− raise response
178− response.close = MethodType(_close_response_method, response)
179− yield response
180−
181− # yield chunks
182183 chunk_or_exception = q.get()
183184 while chunk_or_exception is not None:
184185 if isinstance(chunk_or_exception, Exception):
185186 raise chunk_or_exception
186187 yield chunk_or_exception
187188 chunk_or_exception = q.get()
188− future.result()
189+ return future.result()
searx/network/client.py+8−1
@@ -3,7 +3,6 @@
33import asyncio
44import logging
55import threading
6−import uvloop
76
87import httpcore
98import httpx
@@ -18,6 +17,14 @@ import python_socks._errors
1817
1918from searx import logger
2019
20+# Optional uvloop (support Python 3.6)
21+try:
22+ import uvloop
23+except ImportError:
24+ pass
25+else:
26+ uvloop.install()
27+
2128
2229logger = logger.getChild('searx.http.client')
2330LOOP = None
searx/network/network.py−8
@@ -326,14 +326,6 @@ def initialize(settings_engines=None, settings_outgoing=None):
326326 if isinstance(network, str):
327327 NETWORKS[engine_name] = NETWORKS[network]
328328
329− # the /image_proxy endpoint has a dedicated network.
330− # same parameters than the default network, but HTTP/2 is disabled.
331− # It decreases the CPU load average, and the total time is more or less the same
332− if 'image_proxy' not in NETWORKS:
333− image_proxy_params = default_params.copy()
334− image_proxy_params['enable_http2'] = False
335− NETWORKS['image_proxy'] = new_network(image_proxy_params)
336−
337329
338330@atexit.register
339331def done():
searx/results.py+2−8
@@ -6,7 +6,6 @@ from urllib.parse import urlparse, unquote
66from searx import logger
77from searx.engines import engines
88from searx.metrology.error_recorder import record_error
9−from searx.utils import add_scheme_to_url
109from searx import settings
1110
1211
@@ -241,15 +240,10 @@ class ResultContainer:
241240 result['parsed_url'] = urlparse(result['url'])
242241
243242 # if the result has no scheme, use http as default
244− if not result['parsed_url'].scheme or result['parsed_url'].scheme == '':
245− result['parsed_url'] = result['parsed_url']._replace(scheme='http')
243+ if not result['parsed_url'].scheme:
244+ result['parsed_url'] = result['parsed_url']._replace(scheme="http")
246245 result['url'] = result['parsed_url'].geturl()
247246
248− if 'thumbnail_src' in result:
249− result['thumbnail_src'] = add_scheme_to_url(result['thumbnail_src'])
250− if 'img_src' in result:
251− result['img_src'] = add_scheme_to_url(result['img_src'])
252−
253247 result['engines'] = set([result['engine']])
254248
255249 # strip multiple spaces and cariage returns from content
searx/settings.yml+8−19
@@ -410,13 +410,6 @@ engines:
410410 timeout: 3.0
411411 disabled : True
412412
413− - name : tineye
414− engine : tineye
415− shortcut : tin
416− paging : True
417− timeout : 9.0
418− disabled : True
419−
420413 - name : etools
421414 engine : etools
422415 shortcut : eto
@@ -505,13 +498,6 @@ engines:
505498 engine : gigablast
506499 shortcut : gb
507500 timeout : 3.0
508− # Collection names to search in, separated by whitespaces.
509− # By default searx, queries the main collection.
510− #collections: main
511− # Search types, not required. possible values: images, news
512− #search_type: images
513− # Enable fast to get results quicker.
514− #fast : 1
515501 disabled: True
516502 additional_tests:
517503 rosebud: *test_rosebud
@@ -697,19 +683,22 @@ engines:
697683
698684 - name : hoogle
699685 engine : xpath
686+ paging : True
700687 search_url : https://hoogle.haskell.org/?hoogle={query}&start={pageno}
701688 results_xpath : '//div[@class="result"]'
702− title_xpath : './div[@class="ans"]'
703− url_xpath : './div[@class="ans"]//a/@href'
704− content_xpath : './div[contains(@class, "doc")]'
689+ title_xpath : './/div[@class="ans"]//a'
690+ url_xpath : './/div[@class="ans"]//a/@href'
691+ content_xpath : './/div[@class="from"]'
692+ page_size : 20
705693 categories : it
706694 shortcut : ho
707695 about:
708− website: https://hoogle.haskell.org/
696+ website: https://www.haskell.org/
709697 wikidata_id: Q34010
698+ official_api_documentation: https://hackage.haskell.org/api
710699 use_official_api: false
711700 require_api_key: false
712− results: HTML
701+ results: JSON
713702
714703 - name : imdb
715704 engine : imdb
searx/utils.py+6−12
@@ -7,7 +7,7 @@ from numbers import Number
77from os.path import splitext, join
88from random import choice
99from html.parser import HTMLParser
10−from urllib.parse import urljoin, urlparse, urlunparse
10+from urllib.parse import urljoin, urlparse
1111
1212from lxml import html
1313from lxml.etree import ElementBase, XPath, XPathError, XPathSyntaxError, _ElementStringResult, _ElementUnicodeResult
@@ -214,16 +214,6 @@ def normalize_url(url, base_url):
214214 return url
215215
216216
217−def add_scheme_to_url(url, scheme="https"):
218− """Add schema to URL: if scheme is missing from the URL, then add it."""
219−
220− parsed = urlparse(url)
221− if parsed.scheme == '':
222− parsed_with_scheme = parsed._replace(scheme=scheme)
223− return urlunparse(parsed_with_scheme)
224− return url
225−
226−
227217def extract_url(xpath_results, base_url):
228218 """Extract and normalize URL from lxml Element
229219
@@ -272,7 +262,11 @@ def dict_subset(d, properties):
272262 >>> >> dict_subset({'A': 'a', 'B': 'b', 'C': 'c'}, ['A', 'D'])
273263 {'A': 'a'}
274264 """
275− return {k: d[k] for k in properties if k in d}
265+ result = {}
266+ for k in properties:
267+ if k in d:
268+ result[k] = d[k]
269+ return result
276270
277271
278272def get_torrent_size(filesize, filesize_multiplier):
searx/webapp.py+13−41
@@ -94,7 +94,7 @@ from searx.plugins import plugins
9494from searx.plugins.oa_doi_rewrite import get_doi_resolver
9595from searx.preferences import Preferences, ValidationException, LANGUAGE_CODES
9696from searx.answerers import answerers
97−from searx.network import stream as http_stream, set_context_network_name
97+from searx.network import stream as http_stream
9898from searx.answerers import ask
9999from searx.metrology.error_recorder import errors_per_engines
100100from searx.settings_loader import get_default_settings_path
@@ -921,8 +921,6 @@ def _is_selected_language_supported(engine, preferences):
921921
922922@app.route('/image_proxy', methods=['GET'])
923923def image_proxy():
924− # pylint: disable=too-many-return-statements, too-many-branches
925−
926924 url = request.args.get('url')
927925
928926 if not url:
@@ -934,21 +932,14 @@ def image_proxy():
934932 return '', 400
935933
936934 maximum_size = 5 * 1024 * 1024
937− forward_resp = False
938− resp = None
935+
939936 try:
940− request_headers = {
941− 'User-Agent': gen_useragent(),
942− 'Accept': 'image/webp,*/*',
943− 'Accept-Encoding': 'gzip, deflate',
944− 'Sec-GPC': '1',
945− 'DNT': '1',
946− }
947− set_context_network_name('image_proxy')
937+ headers = dict_subset(request.headers, {'If-Modified-Since', 'If-None-Match'})
938+ headers['User-Agent'] = gen_useragent()
948939 stream = http_stream(
949940 method='GET',
950941 url=url,
951− headers=request_headers,
942+ headers=headers,
952943 timeout=settings['outgoing']['request_timeout'],
953944 follow_redirects=True,
954945 max_redirects=20)
@@ -958,37 +949,25 @@ def image_proxy():
958949 if content_length and content_length.isdigit() and int(content_length) > maximum_size:
959950 return 'Max size', 400
960951
952+ if resp.status_code == 304:
953+ return '', resp.status_code
954+
961955 if resp.status_code != 200:
962956 logger.debug('image-proxy: wrong response code: {0}'.format(resp.status_code))
963957 if resp.status_code >= 400:
964958 return '', resp.status_code
965959 return '', 400
966960
967− if not resp.headers.get('Content-Type', '').startswith('image/'):
968− logger.debug('image-proxy: wrong content-type: %s', resp.headers.get('Content-Type', ''))
961+ if not resp.headers.get('content-type', '').startswith('image/'):
962+ logger.debug('image-proxy: wrong content-type: {0}'.format(resp.headers.get('content-type')))
969963 return '', 400
970964
971− forward_resp = True
972− except httpx.HTTPError:
973− logger.exception('HTTP error')
974− return '', 400
975− finally:
976− if resp and not forward_resp:
977− # the code is about to return an HTTP 400 error to the browser
978− # we make sure to close the response between searxng and the HTTP server
979− try:
980− resp.close()
981− except httpx.HTTPError:
982− logger.exception('HTTP error on closing')
965+ headers = dict_subset(resp.headers, {'Content-Length', 'Length', 'Date', 'Last-Modified', 'Expires', 'Etag'})
983966
984− try:
985− headers = dict_subset(
986− resp.headers,
987− {'Content-Type', 'Content-Encoding', 'Content-Length', 'Length'}
988− )
967+ total_length = 0
989968
990969 def forward_chunk():
991− total_length = 0
970+ nonlocal total_length
992971 for chunk in stream:
993972 total_length += len(chunk)
994973 if total_length > maximum_size:
@@ -1169,13 +1148,6 @@ def run():
11691148 )
11701149
11711150
1172−def patch_application(app):
1173− # serve pages with HTTP/1.1
1174− WSGIRequestHandler.protocol_version = "HTTP/{}".format(settings['server']['http_protocol_version'])
1175− # patch app to handle non root url-s behind proxy & wsgi
1176− app.wsgi_app = ReverseProxyPathFix(ProxyFix(app.wsgi_app))
1177−
1178−
11791151class ReverseProxyPathFix:
11801152 '''Wrap the application in this middleware and configure the
11811153 front-end server to add these headers, to let you quietly bind
utils/searx.sh+1−1
@@ -418,7 +418,7 @@ install_settings() {
418418 err_msg "you have to install searx first"
419419 exit 42
420420 fi
421− mkdir -p "$(dirname "${SEARX_SETTINGS_PATH}")"
421+ mkdir -p "$(dirname ${SEARX_SETTINGS_PATH})"
422422
423423 if [[ ! -f ${SEARX_SETTINGS_PATH} ]]; then
424424 info_msg "install settings ${SEARX_SETTINGS_TEMPLATE}"