on master help sign in
Merge branch 'master' of https://github.com/asciimoo/searx
commit132681b3aaf5b330d9d19624038b51fe2ebfd8d5 authorpw3t <romain@berthor.fr> dateThu 23 Jan 2014, 22:11 (13 years ago) parentd6b017e59eeeaa
52 files changed+886−409
.gitignore+11−7 Makefile+7−8 README.md−122 README.rst+159 babel.cfg+3 buildout.cfg−2 minimal.cfg−2 production.cfg−2 requirements.txt+1 searx/__init__.py+2−4 searx/engines/__init__.py+83−39 searx/engines/bing.py+3−2 searx/engines/currency_convert.py+20−15 searx/engines/dailymotion.py+9−4 searx/engines/deviantart.py+6−2 searx/engines/duckduckgo.py+7−5 searx/engines/duckduckgo_definitions.py+6−6 searx/engines/filecrop.py+17−8 searx/engines/flickr.py+8−2 searx/engines/github.py+5−2 searx/engines/google_images.py+8−2 searx/engines/json_engine.py+14−6 searx/engines/mediawiki.py+4−4 searx/engines/piratebay.py+18−9 searx/engines/soundcloud.py+5−2 searx/engines/stackoverflow.py+3−1 searx/engines/startpage.py+2−4 searx/engines/twitter.py+8−3 searx/engines/vimeo.py+15−12 searx/engines/xpath.py+17−12 searx/engines/yacy.py+4−2 searx/engines/youtube.py+7−7 settings.yml → searx/settings.yml+4 searx/settings_robot.py−16 searx/settings_robot.yml+107 searx/static/css/style.css+4−1 searx/templates/about.html+11−11 searx/templates/categories.html+1−1 searx/templates/engines.html+4−5 searx/templates/index.html+2−2 searx/templates/preferences.html+16−6 searx/templates/result_templates/default.html+2−4 searx/templates/result_templates/videos.html+2−4 searx/templates/results.html+4−4 searx/templates/stats.html+1−1 searx/testing.py+16−3 searx/translations/hu/LC_MESSAGES/messages.mo searx/translations/hu/LC_MESSAGES/messages.po+115 searx/utils.py+20−6 searx/webapp.py+104−46 setup.py+20−3 versions.cfg+1
.gitignore+11−7
@@ -1,20 +1,24 @@
1−env
2−engines.cfg
3−.installed.cfg
41.coverage
5−covearge/
2+.installed.cfg
3+engines.cfg
4+env
5+robot_log.html
6+robot_output.xml
7+robot_report.html
68setup.cfg
79
810*.pyc
911*/*.pyc
1012
1113bin/
12−include/
13−lib/
1414build/
15+covearge/
1516develop-eggs/
17+dist/
1618eggs/
19+include/
20+lib/
1721local/
18−searx.egg-info/
1922parts/
23+searx.egg-info/
2024var/
Makefile+7−8
@@ -21,11 +21,7 @@ $(python):
2121tests: .installed.cfg
2222 @bin/test
2323
24−enginescfg:
25− @test -f ./engines.cfg || echo "Copying engines.cfg ..."
26− @cp --no-clobber engines.cfg_sample engines.cfg
27−
28−robot: .installed.cfg enginescfg
24+robot: .installed.cfg
2925 @bin/robot
3026
3127flake8: .installed.cfg
@@ -37,18 +33,21 @@ coverage: .installed.cfg
3733 @bin/coverage report --show-missing
3834 @bin/coverage html --directory ./coverage
3935
40−production: bin/buildout production.cfg setup.py enginescfg
36+production: bin/buildout production.cfg setup.py
4137 bin/buildout -c production.cfg $(options)
4238 @echo "* Please modify `readlink --canonicalize-missing ./searx/settings.py`"
4339 @echo "* Hint 1: on production, disable debug mode and change secret_key"
4440 @echo "* Hint 2: searx will be executed at server startup by crontab"
4541 @echo "* Hint 3: to run immediatley, execute 'bin/supervisord'"
4642
47−minimal: bin/buildout minimal.cfg setup.py enginescfg
43+minimal: bin/buildout minimal.cfg setup.py
4844 bin/buildout -c minimal.cfg $(options)
4945
46+locales:
47+ @pybabel compile -d searx/translations
48+
5049clean:
5150 @rm -rf .installed.cfg .mr.developer.cfg bin parts develop-eggs \
5251 searx.egg-info lib include .coverage coverage
5352
54−.PHONY: all tests enginescfg robot flake8 coverage production minimal clean
53+.PHONY: all tests robot flake8 coverage production minimal locales clean
README.mddeleted−122
@@ -1,122 +0,0 @@
1−searx
2−=====
3−
4−A privacy-respecting, hackable [metasearch engine](https://en.wikipedia.org/wiki/Metasearch_engine).
5−
6−List of [running instances](https://github.com/asciimoo/searx/wiki/Searx-instances).
7−
8−[![Flattr searx](http://api.flattr.com/button/flattr-badge-large.png)](https://flattr.com/submit/auto?user_id=asciimoo&url=https://github.com/asciimoo/searx&title=searx&language=&tags=github&category=software)
9−
10−
11−### Features
12−
13−* Tracking free
14−* Modular (see [examples](https://github.com/asciimoo/searx/blob/master/examples))
15−* Parallel queries
16−* Supports multiple output formats
17− * json `curl https://searx.0x2a.tk/?format=json&q=[query]`
18− * csv `curl https://searx.0x2a.tk/?format=csv&q=[query]`
19− * opensearch/rss `curl https://searx.0x2a.tk/?format=rss&q=[query]`
20−* Opensearch support (you can set as default search engine)
21−* Configurable search engines/categories
22−
23−
24−### Installation
25−
26−* clone source: `git clone git@github.com:asciimoo/searx.git && cd searx`
27−* install dependencies: `pip install -r requirements.txt`
28−* edit your [settings.yml](https://github.com/asciimoo/searx/blob/master/settings.yml) (set your `secret_key`!)
29−* run `python searx/webapp.py` to start the application
30−
31−For all the details, follow this [step by step installation](https://github.com/asciimoo/searx/wiki/Installation)
32−
33−
34−### Alternative (Recommended) Installation
35−
36−* clone source: `git clone git@github.com:asciimoo/searx.git && cd searx`
37−* build in current folder: `make minimal`
38−* run `bin/searx-run` to start the application
39−
40−
41−### Development
42−
43−Just run `make`. Versions of dependencies are pinned down inside `versions.cfg` to produce most stable build. Also remember, NO make command should be run as root, not even `make production`
44−
45−
46−### Deployment
47−
48−* clone source: `git clone git@github.com:asciimoo/searx.git && cd searx`
49−* build in current folder: `make production`
50−* run `bin/supervisord` to start the application
51−
52−
53−### Upgrading
54−
55−* inside previously cloned searx directory run: `git stash` to temporarily save any changes you have made
56−* pull source: `git pull origin master`
57−* re-build in current folder: `make production`
58−* run `bin/supervisorctl stop searx` to stop searx, if it does not, then run `fuser -k 8888/tcp`
59−* run `bin/supervisorctl reload` to re-read supervisor config and start searx
60−
61−
62−### Command make
63−
64−##### `make`
65−
66−Builds development environment with testing support.
67−
68−##### `make tests`
69−
70−Runs tests. You can write tests [here](https://github.com/asciimoo/searx/tree/master/searx/tests) and remember 'untested code is broken code'.
71−
72−##### `make robot`
73−
74−Runs robot (Selenium) tests, you must have `firefox` installed because this functional tests actually run the browser and perform operations on it. Also searx is executed with [settings_robot](https://github.com/asciimoo/searx/blob/master/searx/settings_robot.py).
75−
76−##### `make flake8`
77−
78−'pep8 is a tool to check your Python code against some of the style conventions in [PEP 8](http://www.python.org/dev/peps/pep-0008/).'
79−
80−##### `make coverage`
81−
82−Checks coverage of tests, after running this, execute this: `firefox ./coverage/index.html`
83−
84−##### `make production`
85−
86−Used to make co-called production environment - without tests (you should ran tests before deploying searx on the server). This installs supervisord, so if searx crashes, it will try to pick itself up again. And crontab entry is added to start supervisord at server boot.
87−
88−##### `make minimal`
89−
90−Minimal build - without test frameworks, the quickest build option.
91−
92−##### `make clean`
93−
94−Deletes several folders and files (see `Makefile` for more), so that next time you run any other `make` command it will rebuild everithing.
95−
96−
97−### TODO
98−
99−* Moar engines
100−* Better ui
101−* Language support
102−* Documentation
103−* Pagination
104−* Fix `flake8` errors, `make flake8` will be merged into `make tests` when it does not fail anymore
105−* Tests
106−* When we have more tests, we can integrate Travis-CI
107−
108−
109−### Bugs
110−
111−Bugs or suggestions? Visit the [issue tracker](https://github.com/asciimoo/searx/issues).
112−
113−
114−### [License](https://github.com/asciimoo/searx/blob/master/LICENSE)
115−
116−
117−### More about searx
118−
119−* [ohloh](https://www.ohloh.net/p/searx/)
120−* [twitter](https://twitter.com/Searx_engine)
121−* IRC: #searx @ freenode
122−
README.rstnew file+159
@@ -0,0 +1,159 @@
1+searx
2+=====
3+
4+A privacy-respecting, hackable `metasearch
5+engine <https://en.wikipedia.org/wiki/Metasearch_engine>`__.
6+
7+List of `running
8+instances <https://github.com/asciimoo/searx/wiki/Searx-instances>`__.
9+
10+|Flattr searx|
11+
12+Features
13+~~~~~~~~
14+
15+- Tracking free
16+- Modular (see
17+ `examples <https://github.com/asciimoo/searx/blob/master/examples>`__)
18+- Parallel queries
19+- Supports multiple output formats
20+ - json ``curl https://searx.0x2a.tk/?format=json&q=[query]``
21+ - csv ``curl https://searx.0x2a.tk/?format=csv&q=[query]``
22+ - opensearch/rss ``curl https://searx.0x2a.tk/?format=rss&q=[query]``
23+- Opensearch support (you can set as default search engine)
24+- Configurable search engines/categories
25+
26+Installation
27+~~~~~~~~~~~~
28+
29+- clone source:
30+ ``git clone git@github.com:asciimoo/searx.git && cd searx``
31+- install dependencies: ``pip install -r requirements.txt``
32+- edit your
33+ `settings.yml <https://github.com/asciimoo/searx/blob/master/settings.yml>`__
34+ (set your ``secret_key``!)
35+- run ``python searx/webapp.py`` to start the application
36+
37+For all the details, follow this `step by step
38+installation <https://github.com/asciimoo/searx/wiki/Installation>`__
39+
40+Alternative (Recommended) Installation
41+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
42+
43+- clone source:
44+ ``git clone git@github.com:asciimoo/searx.git && cd searx``
45+- build in current folder: ``make minimal``
46+- run ``bin/searx-run`` to start the application
47+
48+Development
49+~~~~~~~~~~~
50+
51+Just run ``make``. Versions of dependencies are pinned down inside
52+``versions.cfg`` to produce most stable build. Also remember, NO make
53+command should be run as root, not even ``make production``
54+
55+Deployment
56+~~~~~~~~~~
57+
58+- clone source:
59+ ``git clone git@github.com:asciimoo/searx.git && cd searx``
60+- build in current folder: ``make production``
61+- run ``bin/supervisord`` to start the application
62+
63+Upgrading
64+~~~~~~~~~
65+
66+- inside previously cloned searx directory run: ``git stash`` to
67+ temporarily save any changes you have made
68+- pull source: ``git pull origin master``
69+- re-build in current folder: ``make production``
70+- run ``bin/supervisorctl stop searx`` to stop searx, if it does not,
71+ then run ``fuser -k 8888/tcp``
72+- run ``bin/supervisorctl reload`` to re-read supervisor config and
73+ start searx
74+
75+Command make
76+~~~~~~~~~~~~
77+
78+``make``
79+''''''''
80+
81+Builds development environment with testing support.
82+
83+``make tests``
84+''''''''''''''
85+
86+Runs tests. You can write tests
87+`here <https://github.com/asciimoo/searx/tree/master/searx/tests>`__ and
88+remember 'untested code is broken code'.
89+
90+``make robot``
91+''''''''''''''
92+
93+Runs robot (Selenium) tests, you must have ``firefox`` installed because
94+this functional tests actually run the browser and perform operations on
95+it. Also searx is executed with
96+`settings\_robot <https://github.com/asciimoo/searx/blob/master/searx/settings_robot.py>`__.
97+
98+``make flake8``
99+'''''''''''''''
100+
101+'pep8 is a tool to check your Python code against some of the style
102+conventions in `PEP 8 <http://www.python.org/dev/peps/pep-0008/>`__.'
103+
104+``make coverage``
105+'''''''''''''''''
106+
107+Checks coverage of tests, after running this, execute this:
108+``firefox ./coverage/index.html``
109+
110+``make production``
111+'''''''''''''''''''
112+
113+Used to make co-called production environment - without tests (you
114+should ran tests before deploying searx on the server). This installs
115+supervisord, so if searx crashes, it will try to pick itself up again.
116+And crontab entry is added to start supervisord at server boot.
117+
118+``make minimal``
119+''''''''''''''''
120+
121+Minimal build - without test frameworks, the quickest build option.
122+
123+``make clean``
124+''''''''''''''
125+
126+Deletes several folders and files (see ``Makefile`` for more), so that
127+next time you run any other ``make`` command it will rebuild everithing.
128+
129+TODO
130+~~~~
131+
132+- Moar engines
133+- Better ui
134+- Language support
135+- Documentation
136+- Pagination
137+- Fix ``flake8`` errors, ``make flake8`` will be merged into
138+ ``make tests`` when it does not fail anymore
139+- Tests
140+- When we have more tests, we can integrate Travis-CI
141+
142+Bugs
143+~~~~
144+
145+Bugs or suggestions? Visit the `issue
146+tracker <https://github.com/asciimoo/searx/issues>`__.
147+
148+`License <https://github.com/asciimoo/searx/blob/master/LICENSE>`__
149+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
150+
151+More about searx
152+~~~~~~~~~~~~~~~~
153+
154+- `ohloh <https://www.ohloh.net/p/searx/>`__
155+- `twitter <https://twitter.com/Searx_engine>`__
156+- IRC: #searx @ freenode
157+
158+.. |Flattr searx| image:: http://api.flattr.com/button/flattr-badge-large.png
159+ :target: https://flattr.com/submit/auto?user_id=asciimoo&url=https://github.com/asciimoo/searx&title=searx&language=&tags=github&category=software
babel.cfgnew file+3
@@ -0,0 +1,3 @@
1+[python: **.py]
2+[jinja2: **/templates/**.html]
3+extensions=jinja2.ext.autoescape,jinja2.ext.with_
buildout.cfg−2
@@ -16,8 +16,6 @@ recipe = zc.recipe.egg:script
1616eggs = ${buildout:eggs}
1717interpreter = py
1818dependent-scripts = true
19−entry-points =
20− searx-run=searx.webapp:run
2119
2220
2321[robot]
minimal.cfg−2
@@ -13,5 +13,3 @@ parts +=
1313recipe = zc.recipe.egg:script
1414eggs = ${buildout:eggs}
1515interpreter = py
16−entry-points =
17− searx-run=searx.webapp:run
production.cfg−2
@@ -15,8 +15,6 @@ parts +=
1515recipe = zc.recipe.egg:script
1616eggs = ${buildout:eggs}
1717interpreter = py
18−entry-points =
19− searx-run=searx.webapp:run
2018
2119
2220[supervisor]
requirements.txt+1
@@ -1,4 +1,5 @@
11flask
2+flask-babel
23grequests
34lxml
45pyyaml
searx/__init__.py+2−4
@@ -1,5 +1,5 @@
11from os import environ
2−from os.path import realpath, dirname, join
2+from os.path import realpath, dirname, join, abspath
33try:
44 from yaml import load
55except:
@@ -7,8 +7,7 @@ except:
77 stderr.write('[E] install pyyaml\n')
88 exit(2)
99
10−
11−searx_dir = realpath(dirname(realpath(__file__))+'/../')
10+searx_dir = abspath(dirname(__file__))
1211engine_dir = dirname(realpath(__file__))
1312
1413if 'SEARX_SETTINGS_PATH' in environ:
@@ -19,4 +18,3 @@ else:
1918
2019with open(settings_path) as settings_yaml:
2120 settings = load(settings_yaml)
22−
searx/engines/__init__.py+83−39
@@ -26,6 +26,7 @@ from searx import settings
2626from searx.utils import gen_useragent
2727import sys
2828from datetime import datetime
29+from flask.ext.babel import gettext
2930
3031engine_dir = dirname(realpath(__file__))
3132
@@ -35,6 +36,7 @@ engines = {}
3536
3637categories = {'general': []}
3738
39+
3840def load_module(filename):
3941 modname = splitext(filename)[0]
4042 if modname in sys.modules:
@@ -50,7 +52,7 @@ if not 'engines' in settings or not settings['engines']:
5052
5153for engine_data in settings['engines']:
5254 engine_name = engine_data['engine']
53− engine = load_module(engine_name+'.py')
55+ engine = load_module(engine_name + '.py')
5456 for param_name in engine_data:
5557 if param_name == 'engine':
5658 continue
@@ -58,38 +60,50 @@ for engine_data in settings['engines']:
5860 if engine_data['categories'] == 'none':
5961 engine.categories = []
6062 else:
61− engine.categories = map(str.strip, engine_data['categories'].split(','))
63+ engine.categories = map(
64+ str.strip, engine_data['categories'].split(','))
6265 continue
6366 setattr(engine, param_name, engine_data[param_name])
6467 for engine_attr in dir(engine):
6568 if engine_attr.startswith('_'):
6669 continue
67− if getattr(engine, engine_attr) == None:
68− print '[E] Engine config error: Missing attribute "{0}.{1}"'.format(engine.name, engine_attr)
70+ if getattr(engine, engine_attr) is None:
71+ print '[E] Engine config error: Missing attribute "{0}.{1}"'.format(engine.name, engine_attr) # noqa
6972 sys.exit(1)
7073 engines[engine.name] = engine
71− engine.stats = {'result_count': 0, 'search_count': 0, 'page_load_time': 0, 'score_count': 0, 'errors': 0}
74+ engine.stats = {
75+ 'result_count': 0,
76+ 'search_count': 0,
77+ 'page_load_time': 0,
78+ 'score_count': 0,
79+ 'errors': 0
80+ }
7281 if hasattr(engine, 'categories'):
7382 for category_name in engine.categories:
7483 categories.setdefault(category_name, []).append(engine)
7584 else:
7685 categories['general'].append(engine)
7786
87+
7888def default_request_params():
79− return {'method': 'GET', 'headers': {}, 'data': {}, 'url': '', 'cookies': {}}
89+ return {
90+ 'method': 'GET', 'headers': {}, 'data': {}, 'url': '', 'cookies': {}}
91+
8092
8193def make_callback(engine_name, results, suggestions, callback, params):
8294 # creating a callback wrapper for the search engine results
8395 def process_callback(response, **kwargs):
8496 cb_res = []
8597 response.search_params = params
86− engines[engine_name].stats['page_load_time'] += (datetime.now() - params['started']).total_seconds()
98+ engines[engine_name].stats['page_load_time'] += \
99+ (datetime.now() - params['started']).total_seconds()
87100 try:
88101 search_results = callback(response)
89102 except Exception, e:
90103 engines[engine_name].stats['errors'] += 1
91104 results[engine_name] = cb_res
92− print '[E] Error with engine "{0}":\n\t{1}'.format(engine_name, str(e))
105+ print '[E] Error with engine "{0}":\n\t{1}'.format(
106+ engine_name, str(e))
93107 return
94108 for result in search_results:
95109 result['engine'] = engine_name
@@ -101,23 +115,25 @@ def make_callback(engine_name, results, suggestions, callback, params):
101115 results[engine_name] = cb_res
102116 return process_callback
103117
118+
104119def score_results(results):
105− flat_res = filter(None, chain.from_iterable(izip_longest(*results.values())))
120+ flat_res = filter(
121+ None, chain.from_iterable(izip_longest(*results.values())))
106122 flat_len = len(flat_res)
107123 engines_len = len(results)
108124 results = []
109125 # deduplication + scoring
110− for i,res in enumerate(flat_res):
126+ for i, res in enumerate(flat_res):
111127 res['parsed_url'] = urlparse(res['url'])
112128 res['engines'] = [res['engine']]
113129 weight = 1.0
114130 if hasattr(engines[res['engine']], 'weight'):
115131 weight = float(engines[res['engine']].weight)
116− score = int((flat_len - i)/engines_len)*weight+1
132+ score = int((flat_len - i) / engines_len) * weight + 1
117133 duplicated = False
118134 for new_res in results:
119− p1 = res['parsed_url'].path[:-1] if res['parsed_url'].path.endswith('/') else res['parsed_url'].path
120− p2 = new_res['parsed_url'].path[:-1] if new_res['parsed_url'].path.endswith('/') else new_res['parsed_url'].path
135+ p1 = res['parsed_url'].path[:-1] if res['parsed_url'].path.endswith('/') else res['parsed_url'].path # noqa
136+ p2 = new_res['parsed_url'].path[:-1] if new_res['parsed_url'].path.endswith('/') else new_res['parsed_url'].path # noqa
121137 if res['parsed_url'].netloc == new_res['parsed_url'].netloc and\
122138 p1 == p2 and\
123139 res['parsed_url'].query == new_res['parsed_url'].query and\
@@ -125,7 +141,7 @@ def score_results(results):
125141 duplicated = new_res
126142 break
127143 if duplicated:
128− if len(res.get('content', '')) > len(duplicated.get('content', '')):
144+ if len(res.get('content', '')) > len(duplicated.get('content', '')): # noqa
129145 duplicated['content'] = res['content']
130146 duplicated['score'] += score
131147 duplicated['engines'].append(res['engine'])
@@ -139,6 +155,7 @@ def score_results(results):
139155 results.append(res)
140156 return sorted(results, key=itemgetter('score'), reverse=True)
141157
158+
142159def search(query, request, selected_engines):
143160 global engines, categories, number_of_searches
144161 requests = []
@@ -160,13 +177,20 @@ def search(query, request, selected_engines):
160177 request_params['started'] = datetime.now()
161178 request_params = engine.request(query, request_params)
162179
163− callback = make_callback(selected_engine['name'], results, suggestions, engine.response, request_params)
164−
165− request_args = dict(headers = request_params['headers']
166− ,hooks = dict(response=callback)
167− ,cookies = request_params['cookies']
168− ,timeout = settings['server']['request_timeout']
169− )
180+ callback = make_callback(
181+ selected_engine['name'],
182+ results,
183+ suggestions,
184+ engine.response,
185+ request_params
186+ )
187+
188+ request_args = dict(
189+ headers=request_params['headers'],
190+ hooks=dict(response=callback),
191+ cookies=request_params['cookies'],
192+ timeout=settings['server']['request_timeout']
193+ )
170194
171195 if request_params['method'] == 'GET':
172196 req = grequests.get
@@ -180,7 +204,7 @@ def search(query, request, selected_engines):
180204
181205 requests.append(req(request_params['url'], **request_args))
182206 grequests.map(requests)
183− for engine_name,engine_results in results.items():
207+ for engine_name, engine_results in results.items():
184208 engines[engine_name].stats['search_count'] += 1
185209 engines[engine_name].stats['result_count'] += len(engine_results)
186210
@@ -192,6 +216,7 @@ def search(query, request, selected_engines):
192216
193217 return results, suggestions
194218
219+
195220def get_engines_stats():
196221 # TODO refactor
197222 pageloads = []
@@ -200,14 +225,15 @@ def get_engines_stats():
200225 errors = []
201226 scores_per_result = []
202227
203− max_pageload = max_results = max_score = max_errors = max_score_per_result = 0
228+ max_pageload = max_results = max_score = max_errors = max_score_per_result = 0 # noqa
204229 for engine in engines.values():
205230 if engine.stats['search_count'] == 0:
206231 continue
207− results_num = engine.stats['result_count']/float(engine.stats['search_count'])
208− load_times = engine.stats['page_load_time']/float(engine.stats['search_count'])
232+ results_num = \
233+ engine.stats['result_count'] / float(engine.stats['search_count'])
234+ load_times = engine.stats['page_load_time'] / float(engine.stats['search_count']) # noqa
209235 if results_num:
210− score = engine.stats['score_count'] / float(engine.stats['search_count'])
236+ score = engine.stats['score_count'] / float(engine.stats['search_count']) # noqa
211237 score_per_result = score / results_num
212238 else:
213239 score = score_per_result = 0.0
@@ -220,30 +246,48 @@ def get_engines_stats():
220246 results.append({'avg': results_num, 'name': engine.name})
221247 scores.append({'avg': score, 'name': engine.name})
222248 errors.append({'avg': engine.stats['errors'], 'name': engine.name})
223− scores_per_result.append({'avg': score_per_result, 'name': engine.name})
249+ scores_per_result.append({
250+ 'avg': score_per_result,
251+ 'name': engine.name
252+ })
224253
225254 for engine in pageloads:
226− engine['percentage'] = int(engine['avg']/max_pageload*100)
255+ engine['percentage'] = int(engine['avg'] / max_pageload * 100)
227256
228257 for engine in results:
229− engine['percentage'] = int(engine['avg']/max_results*100)
258+ engine['percentage'] = int(engine['avg'] / max_results * 100)
230259
231260 for engine in scores:
232− engine['percentage'] = int(engine['avg']/max_score*100)
261+ engine['percentage'] = int(engine['avg'] / max_score * 100)
233262
234263 for engine in scores_per_result:
235− engine['percentage'] = int(engine['avg']/max_score_per_result*100)
264+ engine['percentage'] = int(engine['avg'] / max_score_per_result * 100)
236265
237266 for engine in errors:
238267 if max_errors:
239− engine['percentage'] = int(float(engine['avg'])/max_errors*100)
268+ engine['percentage'] = int(float(engine['avg']) / max_errors * 100)
240269 else:
241270 engine['percentage'] = 0
242271
243−
244− return [('Page loads (sec)', sorted(pageloads, key=itemgetter('avg')))
245− ,('Number of results', sorted(results, key=itemgetter('avg'), reverse=True))
246− ,('Scores', sorted(scores, key=itemgetter('avg'), reverse=True))
247− ,('Scores per result', sorted(scores_per_result, key=itemgetter('avg'), reverse=True))
248− ,('Errors', sorted(errors, key=itemgetter('avg'), reverse=True))
249− ]
272+ return [
273+ (
274+ gettext('Page loads (sec)'),
275+ sorted(pageloads, key=itemgetter('avg'))
276+ ),
277+ (
278+ gettext('Number of results'),
279+ sorted(results, key=itemgetter('avg'), reverse=True)
280+ ),
281+ (
282+ gettext('Scores'),
283+ sorted(scores, key=itemgetter('avg'), reverse=True)
284+ ),
285+ (
286+ gettext('Scores per result'),
287+ sorted(scores_per_result, key=itemgetter('avg'), reverse=True)
288+ ),
289+ (
290+ gettext('Errors'),
291+ sorted(errors, key=itemgetter('avg'), reverse=True)
292+ ),
293+ ]
searx/engines/bing.py+3−2
@@ -4,11 +4,12 @@ from cgi import escape
44
55base_url = 'http://www.bing.com/'
66search_string = 'search?{query}'
7−locale = 'en-US' # see http://msdn.microsoft.com/en-us/library/dd251064.aspx
7+locale = 'en-US' # see http://msdn.microsoft.com/en-us/library/dd251064.aspx
88
99
1010def request(query, params):
11− search_path = search_string.format(query=urlencode({'q': query, 'setmkt': locale}))
11+ search_path = search_string.format(
12+ query=urlencode({'q': query, 'setmkt': locale}))
1213 #if params['category'] == 'images':
1314 # params['url'] = base_url + 'images/' + search_path
1415 params['url'] = base_url + search_path
searx/engines/currency_convert.py+20−15
@@ -5,7 +5,8 @@ categories = []
55url = 'http://finance.yahoo.com/d/quotes.csv?e=.csv&f=sl1d1t1&s={query}=X'
66weight = 100
77
8−parser_re = re.compile(r'^\W*(\d+(?:\.\d+)?)\W*([a-z]{3})\W*(?:in)?\W*([a-z]{3})\W*$', re.I)
8+parser_re = re.compile(r'^\W*(\d+(?:\.\d+)?)\W*([a-z]{3})\W*(?:in)?\W*([a-z]{3})\W*$', re.I) # noqa
9+
910
1011def request(query, params):
1112 m = parser_re.match(query)
@@ -19,7 +20,7 @@ def request(query, params):
1920 # wrong params
2021 return params
2122
22− q = (from_currency+to_currency).upper()
23+ q = (from_currency + to_currency).upper()
2324
2425 params['url'] = url.format(query=q)
2526 params['ammount'] = ammount
@@ -33,25 +34,29 @@ def response(resp):
3334 global base_url
3435 results = []
3536 try:
36− _,conversion_rate,_ = resp.text.split(',', 2)
37+ _, conversion_rate, _ = resp.text.split(',', 2)
3738 conversion_rate = float(conversion_rate)
3839 except:
3940 return results
4041
41− title = '{0} {1} in {2} is {3}'.format(resp.search_params['ammount']
42− ,resp.search_params['from']
43− ,resp.search_params['to']
44− ,resp.search_params['ammount']*conversion_rate
45− )
42+ title = '{0} {1} in {2} is {3}'.format(
43+ resp.search_params['ammount'],
44+ resp.search_params['from'],
45+ resp.search_params['to'],
46+ resp.search_params['ammount'] * conversion_rate
47+ )
4648
47− content = '1 {0} is {1} {2}'.format(resp.search_params['from'], conversion_rate, resp.search_params['to'])
49+ content = '1 {0} is {1} {2}'.format(resp.search_params['from'],
50+ conversion_rate,
51+ resp.search_params['to'])
4852 now_date = datetime.now().strftime('%Y%m%d')
49− url = 'http://finance.yahoo.com/currency/converter-results/{0}/{1}-{2}-to-{3}.html'
50− url = url.format(now_date
51− ,resp.search_params['ammount']
52− ,resp.search_params['from'].lower()
53− ,resp.search_params['to'].lower()
54− )
53+ url = 'http://finance.yahoo.com/currency/converter-results/{0}/{1}-{2}-to-{3}.html' # noqa
54+ url = url.format(
55+ now_date,
56+ resp.search_params['ammount'],
57+ resp.search_params['from'].lower(),
58+ resp.search_params['to'].lower()
59+ )
5560 results.append({'title': title, 'content': content, 'url': url})
5661
5762 return results
searx/engines/dailymotion.py+9−4
@@ -1,17 +1,21 @@
11from urllib import urlencode
22from lxml import html
33from json import loads
4−from cgi import escape
54
65categories = ['videos']
76locale = 'en_US'
87
98# see http://www.dailymotion.com/doc/api/obj-video.html
10−search_url = 'https://api.dailymotion.com/videos?fields=title,description,duration,url,thumbnail_360_url&sort=relevance&limit=25&page=1&{query}'
9+search_url = 'https://api.dailymotion.com/videos?fields=title,description,duration,url,thumbnail_360_url&sort=relevance&limit=25&page=1&{query}' # noqa
10+
11+# TODO use video result template
12+content_tpl = '<a href="{0}" title="{0}" ><img src="{1}" /></a><br />'
13+
1114
1215def request(query, params):
1316 global search_url
14− params['url'] = search_url.format(query=urlencode({'search': query, 'localization': locale }))
17+ params['url'] = search_url.format(
18+ query=urlencode({'search': query, 'localization': locale}))
1519 return params
1620
1721
@@ -24,7 +28,7 @@ def response(resp):
2428 title = res['title']
2529 url = res['url']
2630 if res['thumbnail_360_url']:
27− content = '<a href="{0}" title="{0}" ><img src="{1}" /></a><br />'.format(url, res['thumbnail_360_url'])
31+ content = content_tpl.format(url, res['thumbnail_360_url'])
2832 else:
2933 content = ''
3034 if res['description']:
@@ -33,6 +37,7 @@ def response(resp):
3337 results.append({'url': url, 'title': title, 'content': content})
3438 return results
3539
40+
3641def text_content_from_html(html_string):
3742 desc_html = html.fragment_fromstring(html_string, create_parent=True)
3843 return desc_html.text_content()
searx/engines/deviantart.py+6−2
@@ -7,6 +7,7 @@ categories = ['images']
77base_url = 'https://www.deviantart.com/'
88search_url = base_url+'search?'
99
10+
1011def request(query, params):
1112 global search_url
1213 params['url'] = search_url + urlencode({'q': query})
@@ -22,8 +23,11 @@ def response(resp):
2223 for result in dom.xpath('//div[contains(@class, "tt-a tt-fh")]'):
2324 link = result.xpath('.//a[contains(@class, "thumb")]')[0]
2425 url = urljoin(base_url, link.attrib.get('href'))
25− title_links = result.xpath('.//span[@class="details"]//a[contains(@class, "t")]')
26+ title_links = result.xpath('.//span[@class="details"]//a[contains(@class, "t")]') # noqa
2627 title = ''.join(title_links[0].xpath('.//text()'))
2728 img_src = link.xpath('.//img')[0].attrib['src']
28− results.append({'url': url, 'title': title, 'img_src': img_src, 'template': 'images.html'})
29+ results.append({'url': url,
30+ 'title': title,
31+ 'img_src': img_src,
32+ 'template': 'images.html'})
2933 return results
searx/engines/duckduckgo.py+7−5
@@ -6,8 +6,11 @@ url = 'https://duckduckgo.com/'
66search_url = url + 'd.js?{query}&p=1&s=0'
77locale = 'us-en'
88
9+
910def request(query, params):
10− params['url'] = search_url.format(query=urlencode({'q': query, 'l': locale}))
11+ q = urlencode({'q': query,
12+ 'l': locale})
13+ params['url'] = search_url.format(query=q)
1114 return params
1215
1316
@@ -17,8 +20,7 @@ def response(resp):
1720 for r in search_res:
1821 if not r.get('t'):
1922 continue
20− results.append({'title': r['t']
21− ,'content': html_to_text(r['a'])
22− ,'url': r['u']
23− })
23+ results.append({'title': r['t'],
24+ 'content': html_to_text(r['a']),
25+ 'url': r['u']})
2426 return results
searx/engines/duckduckgo_definitions.py+6−6
@@ -3,8 +3,9 @@ from urllib import urlencode
33
44url = 'http://api.duckduckgo.com/?{query}&format=json&pretty=0&no_redirect=1'
55
6+
67def request(query, params):
7− params['url'] = url.format(query=urlencode({'q': query}))
8+ params['url'] = url.format(query=urlencode({'q': query}))
89 return params
910
1011
@@ -13,11 +14,10 @@ def response(resp):
1314 results = []
1415 if 'Definition' in search_res:
1516 if search_res.get('AbstractURL'):
16− res = {'title' : search_res.get('Heading', '')
17− ,'content' : search_res.get('Definition', '')
18− ,'url' : search_res.get('AbstractURL', '')
19− ,'class' : 'definition_result'
20− }
17+ res = {'title': search_res.get('Heading', ''),
18+ 'content': search_res.get('Definition', ''),
19+ 'url': search_res.get('AbstractURL', ''),
20+ 'class': 'definition_result'}
2121 results.append(res)
2222
2323 return results
searx/engines/filecrop.py+17−8
@@ -2,7 +2,8 @@ from urllib import urlencode
22from HTMLParser import HTMLParser
33
44url = 'http://www.filecrop.com/'
5−search_url = url + '/search.php?{query}&size_i=0&size_f=100000000&engine_r=1&engine_d=1&engine_e=1&engine_4=1&engine_m=1'
5+search_url = url + '/search.php?{query}&size_i=0&size_f=100000000&engine_r=1&engine_d=1&engine_e=1&engine_4=1&engine_m=1' # noqa
6+
67
78class FilecropResultParser(HTMLParser):
89 def __init__(self):
@@ -18,22 +19,28 @@ class FilecropResultParser(HTMLParser):
1819 def handle_starttag(self, tag, attrs):
1920
2021 if tag == 'tr':
21− if ('bgcolor', '#edeff5') in attrs or ('bgcolor', '#ffffff') in attrs:
22+ if ('bgcolor', '#edeff5') in attrs or\
23+ ('bgcolor', '#ffffff') in attrs:
2224 self.__start_processing = True
2325
2426 if not self.__start_processing:
2527 return
2628
2729 if tag == 'label':
28− self.result['title'] = [attr[1] for attr in attrs if attr[0] == 'title'][0]
29− elif tag == 'a' and ('rel', 'nofollow') in attrs and ('class', 'sourcelink') in attrs:
30+ self.result['title'] = [attr[1] for attr in attrs
31+ if attr[0] == 'title'][0]
32+ elif tag == 'a' and ('rel', 'nofollow') in attrs\
33+ and ('class', 'sourcelink') in attrs:
3034 if 'content' in self.result:
31− self.result['content'] += [attr[1] for attr in attrs if attr[0] == 'title'][0]
35+ self.result['content'] += [attr[1] for attr in attrs
36+ if attr[0] == 'title'][0]
3237 else:
33− self.result['content'] = [attr[1] for attr in attrs if attr[0] == 'title'][0]
38+ self.result['content'] = [attr[1] for attr in attrs
39+ if attr[0] == 'title'][0]
3440 self.result['content'] += ' '
3541 elif tag == 'a':
36− self.result['url'] = url + [attr[1] for attr in attrs if attr[0] == 'href'][0]
42+ self.result['url'] = url + [attr[1] for attr in attrs
43+ if attr[0] == 'href'][0]
3744
3845 def handle_endtag(self, tag):
3946 if self.__start_processing is False:
@@ -60,10 +67,12 @@ class FilecropResultParser(HTMLParser):
6067
6168 self.data_counter += 1
6269
70+
6371def request(query, params):
64− params['url'] = search_url.format(query=urlencode({'w' :query}))
72+ params['url'] = search_url.format(query=urlencode({'w': query}))
6573 return params
6674
75+
6776def response(resp):
6877 parser = FilecropResultParser()
6978 parser.feed(resp.text)
searx/engines/flickr.py+8−2
@@ -8,21 +8,27 @@ categories = ['images']
88
99url = 'https://secure.flickr.com/'
1010search_url = url+'search/?{query}'
11+results_xpath = '//div[@id="thumbnails"]//a[@class="rapidnofollow photo-click" and @data-track="photo-click"]' # noqa
12+
1113
1214def request(query, params):
1315 params['url'] = search_url.format(query=urlencode({'q': query}))
1416 return params
1517
18+
1619def response(resp):
1720 global base_url
1821 results = []
1922 dom = html.fromstring(resp.text)
20− for result in dom.xpath('//div[@id="thumbnails"]//a[@class="rapidnofollow photo-click" and @data-track="photo-click"]'):
23+ for result in dom.xpath(results_xpath):
2124 href = urljoin(url, result.attrib.get('href'))
2225 img = result.xpath('.//img')[0]
2326 title = img.attrib.get('alt', '')
2427 img_src = img.attrib.get('data-defer-src')
2528 if not img_src:
2629 continue
27− results.append({'url': href, 'title': title, 'img_src': img_src, 'template': 'images.html'})
30+ results.append({'url': href,
31+ 'title': title,
32+ 'img_src': img_src,
33+ 'template': 'images.html'})
2834 return results
searx/engines/github.py+5−2
@@ -4,12 +4,15 @@ from cgi import escape
44
55categories = ['it']
66
7−search_url = 'https://api.github.com/search/repositories?sort=stars&order=desc&{query}'
7+search_url = 'https://api.github.com/search/repositories?sort=stars&order=desc&{query}' # noqa
8+
9+accept_header = 'application/vnd.github.preview.text-match+json'
10+
811
912def request(query, params):
1013 global search_url
1114 params['url'] = search_url.format(query=urlencode({'q': query}))
12− params['headers']['Accept'] = 'application/vnd.github.preview.text-match+json'
15+ params['headers']['Accept'] = accept_header
1316 return params
1417
1518
searx/engines/google_images.py+8−2
@@ -6,12 +6,14 @@ from json import loads
66categories = ['images']
77
88url = 'https://ajax.googleapis.com/'
9−search_url = url + 'ajax/services/search/images?v=1.0&start=0&rsz=large&safe=off&filter=off&{query}'
9+search_url = url + 'ajax/services/search/images?v=1.0&start=0&rsz=large&safe=off&filter=off&{query}' # noqa
10+
1011
1112def request(query, params):
1213 params['url'] = search_url.format(query=urlencode({'q': query}))
1314 return params
1415
16+
1517def response(resp):
1618 results = []
1719 search_res = loads(resp.text)
@@ -24,5 +26,9 @@ def response(resp):
2426 title = result['title']
2527 if not result['url']:
2628 continue
27− results.append({'url': href, 'title': title, 'content': '', 'img_src': result['url'], 'template': 'images.html'})
29+ results.append({'url': href,
30+ 'title': title,
31+ 'content': '',
32+ 'img_src': result['url'],
33+ 'template': 'images.html'})
2834 return results
searx/engines/json_engine.py+14−6
@@ -2,12 +2,13 @@ from urllib import urlencode
22from json import loads
33from collections import Iterable
44
5−search_url = None
6−url_query = None
5+search_url = None
6+url_query = None
77content_query = None
8−title_query = None
8+title_query = None
99#suggestion_xpath = ''
1010
11+
1112def iterate(iterable):
1213 if type(iterable) == dict:
1314 it = iterable.iteritems()
@@ -17,11 +18,15 @@ def iterate(iterable):
1718 for index, value in it:
1819 yield str(index), value
1920
21+
2022def is_iterable(obj):
21− if type(obj) == str: return False
22− if type(obj) == unicode: return False
23+ if type(obj) == str:
24+ return False
25+ if type(obj) == unicode:
26+ return False
2327 return isinstance(obj, Iterable)
2428
29+
2530def parse(query):
2631 q = []
2732 for part in query.split('/'):
@@ -31,6 +36,7 @@ def parse(query):
3136 q.append(part)
3237 return q
3338
39+
3440def do_query(data, q):
3541 ret = []
3642 if not len(q):
@@ -38,7 +44,7 @@ def do_query(data, q):
3844
3945 qkey = q[0]
4046
41− for key,value in iterate(data):
47+ for key, value in iterate(data):
4248
4349 if len(q) == 1:
4450 if key == qkey:
@@ -54,11 +60,13 @@ def do_query(data, q):
5460 ret.extend(do_query(value, q))
5561 return ret
5662
63+
5764def query(data, query_string):
5865 q = parse(query_string)
5966
6067 return do_query(data, q)
6168
69+
6270def request(query, params):
6371 query = urlencode({'q': query})[2:]
6472 params['url'] = search_url.format(query=query)
searx/engines/mediawiki.py+4−4
@@ -3,10 +3,12 @@ from urllib import urlencode, quote
33
44url = 'https://en.wikipedia.org/'
55
6+search_url = url + 'w/api.php?action=query&list=search&{query}&srprop=timestamp&format=json' # noqa
7+
68number_of_results = 10
79
10+
811def request(query, params):
9− search_url = url + 'w/api.php?action=query&list=search&{query}&srprop=timestamp&format=json'
1012 params['url'] = search_url.format(query=urlencode({'srsearch': query}))
1113 return params
1214
@@ -14,7 +16,5 @@ def request(query, params):
1416def response(resp):
1517 search_results = loads(resp.text)
1618 res = search_results.get('query', {}).get('search', [])
17−
18− return [{'url': url + 'wiki/' + quote(result['title'].replace(' ', '_').encode('utf-8')),
19+ return [{'url': url + 'wiki/' + quote(result['title'].replace(' ', '_').encode('utf-8')), # noqa
1920 'title': result['title']} for result in res[:int(number_of_results)]]
20−
searx/engines/piratebay.py+18−9
@@ -7,13 +7,18 @@ categories = ['videos', 'music']
77
88url = 'https://thepiratebay.se/'
99search_url = url + 'search/{search_term}/0/99/{search_type}'
10−search_types = {'videos': '200'
11− ,'music' : '100'
12− ,'files' : '0'
13− }
10+search_types = {'videos': '200',
11+ 'music': '100',
12+ 'files': '0'}
13+
14+magnet_xpath = './/a[@title="Download this torrent using magnet"]'
15+content_xpath = './/font[@class="detDesc"]//text()'
16+
1417
1518def request(query, params):
16− params['url'] = search_url.format(search_term=quote(query), search_type=search_types.get(params['category']))
19+ search_type = search_types.get(params['category'])
20+ params['url'] = search_url.format(search_term=quote(query),
21+ search_type=search_type)
1722 return params
1823
1924
@@ -27,10 +32,14 @@ def response(resp):
2732 link = result.xpath('.//div[@class="detName"]//a')[0]
2833 href = urljoin(url, link.attrib.get('href'))
2934 title = ' '.join(link.xpath('.//text()'))
30− content = escape(' '.join(result.xpath('.//font[@class="detDesc"]//text()')))
35+ content = escape(' '.join(result.xpath(content_xpath)))
3136 seed, leech = result.xpath('.//td[@align="right"]/text()')[:2]
32− magnetlink = result.xpath('.//a[@title="Download this torrent using magnet"]')[0]
33− results.append({'url': href, 'title': title, 'content': content,
34− 'seed': seed, 'leech': leech, 'magnetlink': magnetlink.attrib['href'],
37+ magnetlink = result.xpath(magnet_xpath)[0]
38+ results.append({'url': href,
39+ 'title': title,
40+ 'content': content,
41+ 'seed': seed,
42+ 'leech': leech,
43+ 'magnetlink': magnetlink.attrib['href'],
3544 'template': 'torrent.html'})
3645 return results
searx/engines/soundcloud.py+5−2
@@ -5,7 +5,8 @@ categories = ['music']
55
66guest_client_id = 'b45b1aa10f1ac2941910a7f0d10f8e28'
77url = 'https://api.soundcloud.com/'
8−search_url = url + 'search?{query}&facet=model&limit=20&offset=0&linked_partitioning=1&client_id='+guest_client_id
8+search_url = url + 'search?{query}&facet=model&limit=20&offset=0&linked_partitioning=1&client_id='+guest_client_id # noqa
9+
910
1011def request(query, params):
1112 global search_url
@@ -21,5 +22,7 @@ def response(resp):
2122 if result['kind'] in ('track', 'playlist'):
2223 title = result['title']
2324 content = result['description']
24− results.append({'url': result['permalink_url'], 'title': title, 'content': content})
25+ results.append({'url': result['permalink_url'],
26+ 'title': title,
27+ 'content': content})
2528 return results
searx/engines/stackoverflow.py+3−1
@@ -7,6 +7,8 @@ categories = ['it']
77
88url = 'http://stackoverflow.com/'
99search_url = url+'search?'
10+result_xpath = './/div[@class="excerpt"]//text()'
11+
1012
1113def request(query, params):
1214 params['url'] = search_url + urlencode({'q': query})
@@ -20,6 +22,6 @@ def response(resp):
2022 link = result.xpath('.//div[@class="result-link"]//a')[0]
2123 href = urljoin(url, link.attrib.get('href'))
2224 title = escape(' '.join(link.xpath('.//text()')))
23− content = escape(' '.join(result.xpath('.//div[@class="excerpt"]//text()')))
25+ content = escape(' '.join(result.xpath(result_xpath)))
2426 results.append({'url': href, 'title': title, 'content': content})
2527 return results
searx/engines/startpage.py+2−4
@@ -1,11 +1,10 @@
11from urllib import urlencode
22from lxml import html
3−from urlparse import urlparse
4−from cgi import escape
53
64base_url = 'https://startpage.com/'
75search_url = base_url+'do/search'
86
7+
98def request(query, params):
109 global search_url
1110 query = urlencode({'q': query})[2:]
@@ -20,11 +19,10 @@ def response(resp):
2019 results = []
2120 dom = html.fromstring(resp.content)
2221 # ads xpath //div[@id="results"]/div[@id="sponsored"]//div[@class="result"]
23− # not ads : div[@class="result"] are the direct childs of div[@id="results"]
22+ # not ads: div[@class="result"] are the direct childs of div[@id="results"]
2423 for result in dom.xpath('//div[@id="results"]/div[@class="result"]'):
2524 link = result.xpath('.//h3/a')[0]
2625 url = link.attrib.get('href')
27− parsed_url = urlparse(url)
2826 title = link.text_content()
2927 content = result.xpath('./p[@class="desc"]')[0].text_content()
3028 results.append({'url': url, 'title': title, 'content': content})
searx/engines/twitter.py+8−3
@@ -7,6 +7,9 @@ categories = ['social media']
77
88base_url = 'https://twitter.com/'
99search_url = base_url+'search?'
10+title_xpath = './/span[@class="username js-action-profile-name"]//text()'
11+content_xpath = './/p[@class="js-tweet-text tweet-text"]//text()'
12+
1013
1114def request(query, params):
1215 global search_url
@@ -21,7 +24,9 @@ def response(resp):
2124 for tweet in dom.xpath('//li[@data-item-type="tweet"]'):
2225 link = tweet.xpath('.//small[@class="time"]//a')[0]
2326 url = urljoin(base_url, link.attrib.get('href'))
24− title = ''.join(tweet.xpath('.//span[@class="username js-action-profile-name"]//text()'))
25− content = escape(''.join(tweet.xpath('.//p[@class="js-tweet-text tweet-text"]//text()')))
26− results.append({'url': url, 'title': title, 'content': content})
27+ title = ''.join(tweet.xpath(title_xpath))
28+ content = escape(''.join(tweet.xpath(content_xpath)))
29+ results.append({'url': url,
30+ 'title': title,
31+ 'content': content})
2732 return results
searx/engines/vimeo.py+15−12
@@ -5,27 +5,31 @@ from lxml import html
55
66base_url = 'http://vimeo.com'
77search_url = base_url + '/search?{query}'
8−url_xpath = None
8+url_xpath = None
99content_xpath = None
10−title_xpath = None
10+title_xpath = None
1111results_xpath = ''
12+content_tpl = '<a href="{0}"> <img src="{2}"/> </a>'
1213
13−# the cookie set by vimeo contains all the following values, but only __utma seems to be requiered
14+# the cookie set by vimeo contains all the following values,
15+# but only __utma seems to be requiered
1416cookie = {
1517 #'vuid':'918282893.1027205400'
1618 # 'ab_bs':'%7B%223%22%3A279%7D'
17− '__utma':'00000000.000#0000000.0000000000.0000000000.0000000000.0'
19+ '__utma': '00000000.000#0000000.0000000000.0000000000.0000000000.0'
1820 # '__utmb':'18302654.1.10.1388942090'
1921 #, '__utmc':'18302654'
20− #, '__utmz':'18#302654.1388942090.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none)'
22+ #, '__utmz':'18#302654.1388942090.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none)' # noqa
2123 #, '__utml':'search'
2224}
2325
26+
2427def request(query, params):
25− params['url'] = search_url.format(query=urlencode({'q' :query}))
28+ params['url'] = search_url.format(query=urlencode({'q': query}))
2629 params['cookies'] = cookie
2730 return params
2831
32+
2933def response(resp):
3034 results = []
3135 dom = html.fromstring(resp.text)
@@ -36,10 +40,9 @@ def response(resp):
3640 url = base_url + result.xpath(url_xpath)[0]
3741 title = p.unescape(extract_text(result.xpath(title_xpath)))
3842 thumbnail = extract_text(result.xpath(content_xpath)[0])
39− content = '<a href="{0}"> <img src="{2}"/> </a>'.format(url, title, thumbnail)
40− results.append({'url': url
41− , 'title': title
42− , 'content': content
43− , 'template':'videos.html'
44− , 'thumbnail': thumbnail})
43+ results.append({'url': url,
44+ 'title': title,
45+ 'content': content_tpl.format(url, title, thumbnail),
46+ 'template': 'videos.html',
47+ 'thumbnail': thumbnail})
4548 return results
searx/engines/xpath.py+17−12
@@ -1,21 +1,25 @@
11from lxml import html
22from urllib import urlencode, unquote
33from urlparse import urlparse, urljoin
4−from cgi import escape
54from lxml.etree import _ElementStringResult
5+from searx.utils import html_to_text
66
7−search_url = None
8−url_xpath = None
7+search_url = None
8+url_xpath = None
99content_xpath = None
10−title_xpath = None
10+title_xpath = None
1111suggestion_xpath = ''
1212results_xpath = ''
1313
14+
1415'''
1516if xpath_results is list, extract the text from each result and concat the list
16−if xpath_results is a xml element, extract all the text node from it ( text_content() method from lxml )
17+if xpath_results is a xml element, extract all the text node from it
18+ ( text_content() method from lxml )
1719if xpath_results is a string element, then it's already done
1820'''
21+
22+
1923def extract_text(xpath_results):
2024 if type(xpath_results) == list:
2125 # it's list of result : concat everything using recursive call
@@ -30,7 +34,7 @@ def extract_text(xpath_results):
3034 return ''.join(xpath_results)
3135 else:
3236 # it's a element
33− return xpath_results.text_content()
37+ return html_to_text(xpath_results.text_content())
3438
3539
3640def extract_url(xpath_results):
@@ -60,7 +64,8 @@ def normalize_url(url):
6064 url += '/'
6165
6266 # FIXME : hack for yahoo
63− if parsed_url.hostname == 'search.yahoo.com' and parsed_url.path.startswith('/r'):
67+ if parsed_url.hostname == 'search.yahoo.com'\
68+ and parsed_url.path.startswith('/r'):
6469 p = parsed_url.path
6570 mark = p.find('/**')
6671 if mark != -1:
@@ -82,15 +87,15 @@ def response(resp):
8287 if results_xpath:
8388 for result in dom.xpath(results_xpath):
8489 url = extract_url(result.xpath(url_xpath))
85− title = extract_text(result.xpath(title_xpath)[0 ])
90+ title = extract_text(result.xpath(title_xpath)[0])
8691 content = extract_text(result.xpath(content_xpath)[0])
8792 results.append({'url': url, 'title': title, 'content': content})
8893 else:
8994 for url, title, content in zip(
90− map(extract_url, dom.xpath(url_xpath)), \
91− map(extract_text, dom.xpath(title_xpath)), \
92− map(extract_text, dom.xpath(content_xpath)), \
93− ):
95+ map(extract_url, dom.xpath(url_xpath)),
96+ map(extract_text, dom.xpath(title_xpath)),
97+ map(extract_text, dom.xpath(content_xpath))
98+ ):
9499 results.append({'url': url, 'title': title, 'content': content})
95100
96101 if not suggestion_xpath:
searx/engines/yacy.py+4−2
@@ -4,10 +4,12 @@ from urllib import urlencode
44url = 'http://localhost:8090'
55search_url = '/yacysearch.json?{query}&maximumRecords=10'
66
7+
78def request(query, params):
8− params['url'] = url + search_url.format(query=urlencode({'query':query}))
9+ params['url'] = url + search_url.format(query=urlencode({'query': query}))
910 return params
1011
12+
1113def response(resp):
1214 raw_search_results = loads(resp.text)
1315
@@ -25,7 +27,7 @@ def response(resp):
2527 tmp_result['content'] = ''
2628
2729 if len(result['description']):
28− tmp_result['content'] += result['description'] +"<br/>"
30+ tmp_result['content'] += result['description'] + "<br/>"
2931
3032 if len(result['pubDate']):
3133 tmp_result['content'] += result['pubDate'] + "<br/>"
searx/engines/youtube.py+7−7
@@ -5,6 +5,7 @@ categories = ['videos']
55
66search_url = 'https://gdata.youtube.com/feeds/api/videos?alt=json&{query}'
77
8+
89def request(query, params):
910 params['url'] = search_url.format(query=urlencode({'q': query}))
1011 return params
@@ -30,17 +31,16 @@ def response(resp):
3031 thumbnail = ''
3132 if len(result['media$group']['media$thumbnail']):
3233 thumbnail = result['media$group']['media$thumbnail'][0]['url']
33− content += '<a href="{0}" title="{0}" ><img src="{1}" /></a>'.format(url, thumbnail)
34+ content += '<a href="{0}" title="{0}" ><img src="{1}" /></a>'.format(url, thumbnail) # noqa
3435 if len(content):
3536 content += '<br />' + result['content']['$t']
3637 else:
3738 content = result['content']['$t']
3839
39− results.append({'url': url
40− , 'title': title
41− , 'content': content
42− , 'template':'videos.html'
43− , 'thumbnail':thumbnail})
40+ results.append({'url': url,
41+ 'title': title,
42+ 'content': content,
43+ 'template': 'videos.html',
44+ 'thumbnail': thumbnail})
4445
4546 return results
46−
settings.yml → searx/settings.ymlrenamed+4
@@ -105,3 +105,7 @@ engines:
105105 url_xpath : ./a/@href
106106 title_xpath : ./a/div[@class="data"]/p[@class="title"]/text()
107107 content_xpath : ./a/img/@src
108+
109+locales:
110+ en : English
111+ hu : Magyar
searx/settings_robot.pydeleted−16
@@ -1,16 +0,0 @@
1−
2−port = 11111
3−
4−secret_key = "ultrasecretkey" # change this!
5−
6−debug = False
7−
8−request_timeout = 5.0 # seconds
9−
10−weights = {} # 'search_engine_name': float(weight) | default is 1.0
11−
12−blacklist = [] # search engine blacklist
13−
14−categories = {} # custom search engine categories
15−
16−base_url = None # "https://your.domain.tld/" or None (to use request parameters)
searx/settings_robot.ymlnew file+107
@@ -0,0 +1,107 @@
1+server:
2+ port : 11111
3+ secret_key : "ultrasecretkey" # change this!
4+ debug : False
5+ request_timeout : 3.0 # seconds
6+ base_url: False
7+
8+engines:
9+ - name : wikipedia
10+ engine : mediawiki
11+ url : https://en.wikipedia.org/
12+ number_of_results : 1
13+
14+ - name : bing
15+ engine : bing
16+ locale : en-US
17+
18+ - name : currency
19+ engine : currency_convert
20+ categories : general
21+
22+ - name : deviantart
23+ engine : deviantart
24+ categories : images
25+
26+ - name : ddg definitions
27+ engine : duckduckgo_definitions
28+
29+ - name : duckduckgo
30+ engine : duckduckgo
31+ locale : en-us
32+
33+ - name : filecrop
34+ engine : filecrop
35+ categories : files
36+
37+ - name : flickr
38+ engine : flickr
39+ categories : images
40+
41+ - name : github
42+ engine : github
43+ categories : it
44+
45+ - name : google
46+ engine : json_engine
47+ search_url : https://ajax.googleapis.com/ajax/services/search/web?v=2.0&start=0&rsz=large&safe=off&filter=off&q={query}
48+ categories : general
49+ url_query : /responseData/results/unescapedUrl
50+ content_query : /responseData/results/content
51+ title_query : /responseData/results/titleNoFormatting
52+
53+ - name : google images
54+ engine : google_images
55+ categories : images
56+
57+ - name : piratebay
58+ engine : piratebay
59+ categories : videos, music, files
60+
61+ - name : soundcloud
62+ engine : soundcloud
63+ categories : music
64+
65+ - name : stackoverflow
66+ engine : stackoverflow
67+ categories : it
68+
69+ - name : startpage
70+ engine : startpage
71+
72+ - name : twitter
73+ engine : twitter
74+ categories : social media
75+
76+ - name : urbandictionary
77+ engine : xpath
78+ search_url : http://www.urbandictionary.com/define.php?term={query}
79+ url_xpath : //div[@class="word"]//a/@href
80+ title_xpath : //div[@class="word"]//a
81+ content_xpath : //div[@class="definition"]
82+
83+ - name : yahoo
84+ engine : xpath
85+ search_url : http://search.yahoo.com/search?p={query}
86+ results_xpath : //div[@class="res"]
87+ url_xpath : .//h3/a/@href
88+ title_xpath : .//h3/a
89+ content_xpath : .//div[@class="abstr"]
90+ suggestion_xpath : //div[@id="satat"]//a
91+
92+ - name : youtube
93+ engine : youtube
94+ categories : videos
95+
96+ - name : dailymotion
97+ engine : dailymotion
98+ locale : en_US
99+ categories : videos
100+
101+ - name : vimeo
102+ engine : vimeo
103+ categories : videos
104+ results_xpath : //div[@id="browse_content"]/ol/li
105+ url_xpath : ./a/@href
106+ title_xpath : ./a/div[@class="data"]/p[@class="title"]/text()
107+ content_xpath : ./a/img/@src
searx/static/css/style.css+4−1
@@ -49,6 +49,8 @@ input[type="submit"] { border: 1px solid #666666; color: #444444; padding: 4px;
4949
5050input[type="checkbox"] { visibility: hidden; }
5151
52+fieldset { margin: 8px; }
53+
5254#categories { margin: 0 10px; }
5355
5456.checkbox_container { display: inline-block; position: relative; margin: 0 3px; padding: 0px; }
@@ -79,7 +81,6 @@ a { text-decoration: none; color: #1a11be; }
7981a:visited { color: #7b11be; }
8082
8183.result { margin: 19px 0 18px 0; padding: 0; max-width: 55em; clear: both; }
82−.result:hover { background: #e8e7e6; }
8384.result_title { margin-bottom: 0; }
8485.result h3 { font-size: 1em; word-wrap:break-word; margin: 5px 0 1px 0; padding: 0 }
8586.result .content { font-size: 0.8em; margin: 0; padding: 0; max-width: 54em; word-wrap:break-word; line-height: 1.24; }
@@ -201,3 +202,5 @@ tr:hover td { background: #DDDDDD; }
201202
202203 .result img { max-width: 90%; width: auto; height: auto }
203204}
205+
206+.favicon { float: left; margin-right: 4px; }
searx/templates/about.html+11−11
@@ -8,25 +8,25 @@
88 </p>
99 <h2>Why use Searx?</h2>
1010 <ul>
11− <li>Maybe Searx won’t offer you as personalised results as Google, but it doesn't make a profile about you</li>
12− <li>Searx doesn't care about what you search, never shares anything with a third party, and it can't be used to compromise you</li>
13− <li>Searx is a free software, the code is 100% open and you can help to make it better. See more on <a href="https://gmail.com/asciimoo/searx">github</a></li>
11+ <li>Searx may not offer you as personalised results as Google, but it doesn't generate a profile about you</li>
12+ <li>Searx doesn't care about what you search for, never shares anything with a third party, and it can't be used to compromise you</li>
13+ <li>Searx is free software, the code is 100% open and you can help to make it better. See more on <a href="https://github.com/asciimoo/searx">github</a></li>
1414 </ul>
15− <p>If you do care about privacy, want to be a conscious user, moreover believe
15+ <p>If you do care about privacy, want to be a conscious user, or otherwise believe
1616 in digital freedom, make Searx your default search engine or run it on your own server</p>
1717
1818<h2>Technical details - How does it work?</h2>
1919
2020<p>Searx is a <a href="https://en.wikipedia.org/wiki/Metasearch_engine">metasearch engine</a>,
2121inspired by the <a href="http://seeks-project.info/">seeks project</a>.<br />
22−It provides basic privacy by mixing your queries with searches on other platforms without storing search data. Queries are made using a POST request on every browser (except chrome*). Therefore they don't show up in our logs, neither in your url history. In case of Chrome* users there is an exception, Searx uses the search bar to perform GET requests.<br />
23−Searx can be added to your browser's search bar, moreover it can be set as the default search engine.
22+It provides basic privacy by mixing your queries with searches on other platforms without storing search data. Queries are made using a POST request on every browser (except chrome*). Therefore they show up in neither our logs, nor your url history. In case of Chrome* users there is an exception, Searx uses the search bar to perform GET requests.<br />
23+Searx can be added to your browser's search bar; moreover, it can be set as the default search engine.
2424</p>
2525
26−<h2>How can I have my own?</h2>
26+<h2>How can I make it my own?</h2>
2727
28−<p>Searx appreciates your suspicion regarding logs, so take the <a href="https://github.com/asciimoo/searx">code</a> and run it yourself! <br />Add your Searx to this <a href="https://github.com/asciimoo/searx/wiki/Searx-instances">list</a> to help other people to have privacy and make the Internet freer!
29−<br />The more decentralized the Internet is the more freedom we have!</p>
28+<p>Searx appreciates your concern regarding logs, so take the <a href="https://github.com/asciimoo/searx">code</a> and run it yourself! <br />Add your Searx to this <a href="https://github.com/asciimoo/searx/wiki/Searx-instances">list</a> to help other people reclaim their privacy and make the Internet freer!
29+<br />The more decentralized the Internet, is the more freedom we have!</p>
3030
3131<hr />
3232
@@ -39,7 +39,7 @@ Searx can be added to your browser's search bar, moreover it can be set as the d
3939
4040<h3>New engines?</h3>
4141<ul>
42− <li>Edit your engines.cfg, see <a href="https://raw.github.com/asciimoo/searx/master/engines.cfg_sample">sample config</a></li>
42+ <li>Edit your <a href="https://raw.github.com/asciimoo/searx/master/searx/settings.yml">settings.yml</a></li>
4343 <li>Create your custom engine module, check the <a href="https://github.com/asciimoo/searx/blob/master/examples/basic_engine.py">example engine</a></li>
4444</ul>
4545<p>Don't forget to restart searx after config edit!</p>
@@ -48,7 +48,7 @@ Searx can be added to your browser's search bar, moreover it can be set as the d
4848<p>See the <a href="https://github.com/asciimoo/searx/wiki/Installation">installation and setup</a> wiki page</p>
4949
5050<h3>How to debug engines?</h3>
51−<p><a href="/stats">Stats page</a> contains some useful data about the used engines.</p>
51+<p><a href="/stats">Stats page</a> contains some useful data about the engines used.</p>
5252
5353</div>
5454{% endblock %}
searx/templates/categories.html+1−1
@@ -1,7 +1,7 @@
11<div id="categories">
22{% for category in categories %}
33 <div class="checkbox_container">
4− <input type="checkbox" id="checkbox_{{ category|replace(' ', '_') }}" name="category_{{ category }}" {% if category in selected_categories %}checked="checked"{% endif %} /><label for="checkbox_{{ category|replace(' ', '_') }}">{{ category }}</label>
4+ <input type="checkbox" id="checkbox_{{ category|replace(' ', '_') }}" name="category_{{ category }}" {% if category in selected_categories %}checked="checked"{% endif %} /><label for="checkbox_{{ category|replace(' ', '_') }}">{{ _(category) }}</label>
55 </div>
66{% endfor %}
77</div>
searx/templates/engines.html+4−5
@@ -1,12 +1,12 @@
11{% extends 'base.html' %}
22{% block content %}
33<div class="row">
4−<h2>Currently used search engines</h2>
4+ <h2>{{ _('Currently used search engines') }}</h2>
55
66 <table style="width: 80%;">
77 <tr>
8− <th>Engine name</th>
9− <th>Category</th>
8+ <th>{{ _('Engine name') }}</th>
9+ <th>{{ _('Category') }}</th>
1010 </tr>
1111 {% for (categ,search_engines) in categs %}
1212 {% for search_engine in search_engines %}
@@ -20,7 +20,6 @@
2020 {% endfor %}
2121 {% endfor %}
2222 </table>
23−<p>Please add more engines to this list, pull requests are welcome!</p>
24−<p class="right"><a href="/">back</a></p>
23+<p class="right"><a href="/">{{ _('back') }}</a></p>
2524</div>
2625{% endblock %}
searx/templates/index.html+2−2
@@ -4,8 +4,8 @@
44 <div class="title"><h1>searx</h1></div>
55 {% include 'search.html' %}
66 <p class="top_margin">
7− <a href="/about" class="hmarg">about</a>
8− <a href="/preferences" class="hmarg">preferences</a>
7+ <a href="/about" class="hmarg">{{ _('about') }}</a>
8+ <a href="/preferences" class="hmarg">{{ _('preferences') }}</a>
99 </p>
1010</div>
1111{% endblock %}
searx/templates/preferences.html+16−6
@@ -2,18 +2,28 @@
22{% block head %} {% endblock %}
33{% block content %}
44<div class="row">
5− <h2>Preferences</h2>
5+ <h2>{{ _('Preferences') }}</h2>
66
77
8+ <form method="post" action="/preferences" id="search_form">
89 <fieldset>
9− <legend>Default categories</legend>
10− <form method="post" action="/preferences" id="search_form">
10+ <legend>{{ _('Default categories') }}</legend>
1111 <p>
1212 {% include 'categories.html' %}
1313 </p>
14− <input type="submit" value="save" />
15− </form>
1614 </fieldset>
17− <div class="right"><a href="/">back</a></div>
15+ <fieldset>
16+ <legend>{{ _('Interface language') }}</legend>
17+ <p>
18+ <select name='locale'>
19+ {% for locale_id,locale_name in locales.items() %}
20+ <option value={{ locale_id }} {% if locale_id == current_locale %}selected="selected"{% endif %}>{{ locale_name}}</option>
21+ {% endfor %}
22+ </select>
23+ </p>
24+ </fieldset>
25+ <input type="submit" value="{{ _('save') }}" />
26+ </form>
27+ <div class="right"><a href="/">{{ _('back') }}</a></div>
1828</div>
1929{% endblock %}
searx/templates/result_templates/default.html+2−4
@@ -1,13 +1,11 @@
11<div class="result {{ result.class }}">
22
33 {% if result['favicon'] %}
4− <div style="float:left; margin:2px;">
5− <img width="18" height="18" src="static/img/icon_{{result['favicon']}}.ico" alt="{{result['favicon']}}.ico" title="{{result['favicon']}}.ico" />
6− </div>
4+ <img width="14" height="14" class="favicon" src="static/img/icon_{{result['favicon']}}.ico" />
75 {% endif %}
86
97 <div>
10− <h3 class="result_title"><a href="{{ result.url }}">{{ result.title|safe }}</a></h3></br>
8+ <h3 class="result_title"><a href="{{ result.url }}">{{ result.title|safe }}</a></h3>
119 <p class="content">{% if result.content %}{{ result.content|safe }}<br />{% endif %}</p>
1210 <p class="url">{{ result.pretty_url }}</p>
1311 </div>
searx/templates/result_templates/videos.html+2−4
@@ -1,13 +1,11 @@
11<div class="result">
22 {% if result['favicon'] %}
3− <div style="float:left; margin:2px;">
4− <img width="18" height="18" src="static/img/icon_{{result['favicon']}}.ico" alt="{{result['favicon']}}.ico" title="{{result['favicon']}}.ico" />
5− </div>
3+ <img width="14" height="14" class="favicon" src="static/img/icon_{{result['favicon']}}.ico" />
64 {% endif %}
75
86 <p>
97 <h3 class="result_title"><a href="{{ result.url }}">{{ result.title|safe }}</a></h3>
10− <a href="{{ result.url }}"><img width="300" height="170" src="{{ result.thumbnail }}" title={{ result.title }} alt=" {{ result.title }}"/></a>
8+ <a href="{{ result.url }}"><img width="400px" src="{{ result.thumbnail }}" title={{ result.title }} alt=" {{ result.title }}"/></a>
119 <p class="url">{{ result.url }}</p>
1210 </p>
1311</div>
searx/templates/results.html+4−4
@@ -7,12 +7,12 @@
77</div>
88<div id="results">
99 {% if suggestions %}
10− <div id="suggestions"><span>Suggestions: </span>{% for suggestion in suggestions %}<form method="post" action="/"><input type="hidden" name="q" value="{{suggestion}}"><input type="submit" value="{{ suggestion }}" /></form>{% endfor %}</div>
10+ <div id="suggestions"><span>{{ _('Suggestions') }}:</span>{% for suggestion in suggestions %}<form method="post" action="/"><input type="hidden" name="q" value="{{suggestion}}"><input type="submit" value="{{ suggestion }}" /></form>{% endfor %}</div>
1111 {% endif %}
12−
12+
1313
1414 <div id ="result_count">
15− Number of results: {{ number_of_results }}
15+ {{ _('Number of results') }}: {{ number_of_results }}
1616 </div>
1717
1818 {% for result in results %}
@@ -23,7 +23,7 @@
2323 {% endif %}
2424 {% endfor %}
2525 <div id="apis">
26− Download results
26+ {{ _('Download results') }}
2727 <form method="post" action="/">
2828 <div class="left">
2929 <input type="hidden" name="q" value="{{ q }}" />
searx/templates/stats.html+1−1
@@ -1,7 +1,7 @@
11{% extends "base.html" %}
22{% block head %} {% endblock %}
33{% block content %}
4−<h2>Engine stats</h2>
4+<h2>{{ _('Engine stats') }}</h2>
55
66{% for stat_name,stat_category in stats %}
77<div class="left">
searx/testing.py+16−3
@@ -7,10 +7,10 @@ from unittest2 import TestCase
77
88import os
99import subprocess
10−import sys
1110
1211
1312class SearxTestLayer:
13+ """Base layer for non-robot tests."""
1414
1515 __name__ = u'SearxTestLayer'
1616
@@ -36,24 +36,37 @@ class SearxRobotLayer(Layer):
3636
3737 def setUp(self):
3838 os.setpgrp() # create new process group, become its leader
39+
40+ # get program paths
3941 webapp = os.path.join(
4042 os.path.abspath(os.path.dirname(os.path.realpath(__file__))),
4143 'webapp.py'
4244 )
4345 exe = os.path.abspath(os.path.dirname(__file__) + '/../bin/py')
46+
47+ # set robot settings path
48+ os.environ['SEARX_SETTINGS_PATH'] = os.path.abspath(
49+ os.path.dirname(__file__) + '/settings_robot.yml')
50+
51+ # run the server
4452 self.server = subprocess.Popen(
45− [exe, webapp, 'settings_robot'],
53+ [exe, webapp],
4654 stdout=subprocess.PIPE,
4755 stderr=subprocess.STDOUT
4856 )
4957
5058 def tearDown(self):
51− # TERM all processes in my group
59+ # send TERM signal to all processes in my group, to stop subprocesses
5260 os.killpg(os.getpgid(self.server.pid), 15)
5361
62+ # remove previously set environment variable
63+ del os.environ['SEARX_SETTINGS_PATH']
64+
5465
5566SEARXROBOTLAYER = SearxRobotLayer()
5667
5768
5869class SearxTestCase(TestCase):
70+ """Base test case for non-robot tests."""
71+
5972 layer = SearxTestLayer
searx/translations/hu/LC_MESSAGES/messages.monew file

Binary file not shown.

searx/translations/hu/LC_MESSAGES/messages.ponew file+115
@@ -0,0 +1,115 @@
1+# Hungarian translations for PROJECT.
2+# Copyright (C) 2014 ORGANIZATION
3+# This file is distributed under the same license as the PROJECT project.
4+# FIRST AUTHOR <EMAIL@ADDRESS>, 2014.
5+#
6+msgid ""
7+msgstr ""
8+"Project-Id-Version: PROJECT VERSION\n"
9+"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
10+"POT-Creation-Date: 2014-01-22 00:55+0100\n"
11+"PO-Revision-Date: 2014-01-21 23:33+0100\n"
12+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
13+"Language-Team: hu <LL@li.org>\n"
14+"Plural-Forms: nplurals=1; plural=0\n"
15+"MIME-Version: 1.0\n"
16+"Content-Type: text/plain; charset=utf-8\n"
17+"Content-Transfer-Encoding: 8bit\n"
18+"Generated-By: Babel 1.3\n"
19+
20+#: searx/engines/__init__.py:274
21+msgid "Page loads (sec)"
22+msgstr "Válaszidők (sec)"
23+
24+#: searx/engines/__init__.py:278 searx/templates/results.html:15
25+msgid "Number of results"
26+msgstr "Találatok száma"
27+
28+#: searx/engines/__init__.py:282
29+msgid "Scores"
30+msgstr "Pontszámok"
31+
32+#: searx/engines/__init__.py:286
33+msgid "Scores per result"
34+msgstr "Pontszámok találatonként"
35+
36+#: searx/engines/__init__.py:290
37+msgid "Errors"
38+msgstr "Hibák"
39+
40+#: searx/templates/engines.html:4
41+msgid "Currently used search engines"
42+msgstr "Jelenleg használt keresők"
43+
44+#: searx/templates/engines.html:8
45+msgid "Engine name"
46+msgstr "Kereső neve"
47+
48+#: searx/templates/engines.html:9
49+msgid "Category"
50+msgstr "Kategória"
51+
52+#: searx/templates/engines.html:23 searx/templates/preferences.html:27
53+msgid "back"
54+msgstr "vissza"
55+
56+#: searx/templates/index.html:7
57+msgid "about"
58+msgstr "rólunk"
59+
60+#: searx/templates/index.html:8
61+msgid "preferences"
62+msgstr "beállítások"
63+
64+#: searx/templates/preferences.html:5
65+msgid "Preferences"
66+msgstr "Beállítások"
67+
68+#: searx/templates/preferences.html:10
69+msgid "Default categories"
70+msgstr "Alapértelmezett kategóriák"
71+
72+#: searx/templates/preferences.html:16
73+msgid "Interface language"
74+msgstr "Nyelv"
75+
76+#: searx/templates/preferences.html:25
77+msgid "save"
78+msgstr "mentés"
79+
80+#: searx/templates/results.html:10
81+msgid "Suggestions"
82+msgstr "Javaslatok"
83+
84+#: searx/templates/results.html:26
85+msgid "Download results"
86+msgstr "Találatok letöltése"
87+
88+#: searx/templates/stats.html:4
89+msgid "Engine stats"
90+msgstr "Kereső statisztikák"
91+
92+# categories - manually added
93+# TODO - automatically add
94+
95+msgid "files"
96+msgstr "fájlok"
97+
98+msgid "general"
99+msgstr "általános"
100+
101+msgid "music"
102+msgstr "zene"
103+
104+msgid "social media"
105+msgstr "közösségi média"
106+
107+msgid "images"
108+msgstr "képek"
109+
110+msgid "videos"
111+msgstr "videók"
112+
113+msgid "it"
114+msgstr "it"
115+
searx/utils.py+20−6
@@ -1,13 +1,16 @@
11from HTMLParser import HTMLParser
22#import htmlentitydefs
33import csv
4−import codecs
4+from codecs import getincrementalencoder
55import cStringIO
66import re
77
8+
89def gen_useragent():
910 # TODO
10− return "Mozilla/5.0 (X11; Linux x86_64; rv:26.0) Gecko/20100101 Firefox/26.0"
11+ ua = "Mozilla/5.0 (X11; Linux x86_64; rv:26.0) Gecko/20100101 Firefox/26.0"
12+ return ua
13+
1114
1215def highlight_content(content, query):
1316
@@ -34,16 +37,20 @@ def highlight_content(content, query):
3437
3538 return content
3639
40+
3741class HTMLTextExtractor(HTMLParser):
3842 def __init__(self):
3943 HTMLParser.__init__(self)
40− self.result = [ ]
44+ self.result = []
4145
4246 def handle_data(self, d):
4347 self.result.append(d)
4448
4549 def handle_charref(self, number):
46− codepoint = int(number[1:], 16) if number[0] in (u'x', u'X') else int(number)
50+ if number[0] in (u'x', u'X'):
51+ codepoint = int(number[1:], 16)
52+ else:
53+ codepoint = int(number)
4754 self.result.append(unichr(codepoint))
4855
4956 def handle_entityref(self, name):
@@ -54,6 +61,7 @@ class HTMLTextExtractor(HTMLParser):
5461 def get_text(self):
5562 return u''.join(self.result)
5663
64+
5765def html_to_text(html):
5866 s = HTMLTextExtractor()
5967 s.feed(html)
@@ -71,10 +79,16 @@ class UnicodeWriter:
7179 self.queue = cStringIO.StringIO()
7280 self.writer = csv.writer(self.queue, dialect=dialect, **kwds)
7381 self.stream = f
74− self.encoder = codecs.getincrementalencoder(encoding)()
82+ self.encoder = getincrementalencoder(encoding)()
7583
7684 def writerow(self, row):
77− self.writer.writerow([(s.encode("utf-8").strip() if type(s) == str or type(s) == unicode else str(s)) for s in row])
85+ unicode_row = []
86+ for col in row:
87+ if type(col) == str or type(col) == unicode:
88+ unicode_row.append(col.encode('utf-8').strip())
89+ else:
90+ unicode_row.append(col)
91+ self.writer.writerow(unicode_row)
7892 # Fetch UTF-8 output from the queue ...
7993 data = self.queue.getvalue()
8094 data = data.decode("utf-8")
searx/webapp.py+104−46
@@ -17,26 +17,36 @@ along with searx. If not, see < http://www.gnu.org/licenses/ >.
1717(C) 2013- by Adam Tauber, <asciimoo@gmail.com>
1818'''
1919
20+import json
21+import cStringIO
2022import os
21−import sys
22−if __name__ == "__main__":
23− sys.path.append(os.path.realpath(os.path.dirname(os.path.realpath(__file__))+'/../'))
2423
25−from searx import settings
24+from flask import Flask, request, render_template
25+from flask import url_for, Response, make_response, redirect
26+from flask import send_from_directory
2627
27−from flask import Flask, request, render_template, url_for, Response, make_response, redirect
28+from searx import settings
2829from searx.engines import search, categories, engines, get_engines_stats
29−import json
30−import cStringIO
3130from searx.utils import UnicodeWriter
32−from flask import send_from_directory
3331from searx.utils import highlight_content, html_to_text
3432
33+from flask.ext.babel import Babel
34+
3535
36+app = Flask(
37+ __name__,
38+ static_folder=os.path.join(os.path.dirname(__file__), 'static'),
39+ template_folder=os.path.join(os.path.dirname(__file__), 'templates')
40+)
3641
37−app = Flask(__name__)
3842app.secret_key = settings['server']['secret_key']
3943
44+babel = Babel(app)
45+
46+#TODO configurable via settings.yml
47+favicons = ['wikipedia', 'youtube', 'vimeo', 'soundcloud',
48+ 'twitter', 'stackoverflow', 'github']
49+
4050
4151opensearch_xml = '''<?xml version="1.0" encoding="utf-8"?>
4252<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
@@ -51,6 +61,24 @@ opensearch_xml = '''<?xml version="1.0" encoding="utf-8"?>
5161'''
5262
5363
64+@babel.localeselector
65+def get_locale():
66+ locale = request.accept_languages.best_match(settings['locales'].keys())
67+
68+ if request.cookies.get('locale', '') in settings['locales']:
69+ locale = request.cookies.get('locale', '')
70+
71+ if 'locale' in request.args\
72+ and request.args['locale'] in settings['locales']:
73+ locale = request.args['locale']
74+
75+ if 'locale' in request.form\
76+ and request.form['locale'] in settings['locales']:
77+ locale = request.form['locale']
78+
79+ return locale
80+
81+
5482def get_base_url():
5583 if settings['server']['base_url']:
5684 hostname = settings['server']['base_url']
@@ -65,7 +93,8 @@ def get_base_url():
6593def render(template_name, **kwargs):
6694 global categories
6795 kwargs['categories'] = ['general']
68− kwargs['categories'].extend(x for x in sorted(categories.keys()) if x != 'general')
96+ kwargs['categories'].extend(x for x in
97+ sorted(categories.keys()) if x != 'general')
6998 if not 'selected_categories' in kwargs:
7099 kwargs['selected_categories'] = []
71100 cookie_categories = request.cookies.get('categories', '').split(',')
@@ -76,6 +105,7 @@ def render(template_name, **kwargs):
76105 kwargs['selected_categories'] = ['general']
77106 return render_template(template_name, **kwargs)
78107
108+
79109def parse_query(query):
80110 query_engines = []
81111 query_parts = query.split()
@@ -89,7 +119,7 @@ def parse_query(query):
89119def index():
90120 global categories
91121
92− if request.method=='POST':
122+ if request.method == 'POST':
93123 request_data = request.form
94124 else:
95125 request_data = request.args
@@ -101,14 +131,15 @@ def index():
101131 query, selected_engines = parse_query(request_data['q'].encode('utf-8'))
102132
103133 if not len(selected_engines):
104− for pd_name,pd in request_data.items():
134+ for pd_name, pd in request_data.items():
105135 if pd_name.startswith('category_'):
106136 category = pd_name[9:]
107137 if not category in categories:
108138 continue
109139 selected_categories.append(category)
110140 if not len(selected_categories):
111− cookie_categories = request.cookies.get('categories', '').split(',')
141+ cookie_categories = request.cookies.get('categories', '')
142+ cookie_categories = cookie_categories.split(',')
112143 for ccateg in cookie_categories:
113144 if ccateg in categories:
114145 selected_categories.append(ccateg)
@@ -116,7 +147,9 @@ def index():
116147 selected_categories = ['general']
117148
118149 for categ in selected_categories:
119− selected_engines.extend({'category': categ, 'name': x.name} for x in categories[categ])
150+ selected_engines.extend({'category': categ,
151+ 'name': x.name}
152+ for x in categories[categ])
120153
121154 results, suggestions = search(query, request, selected_engines)
122155
@@ -131,16 +164,18 @@ def index():
131164 result['content'] = html_to_text(result['content']).strip()
132165 result['title'] = html_to_text(result['title']).strip()
133166 if len(result['url']) > 74:
134− result['pretty_url'] = result['url'][:35] + '[..]' + result['url'][-35:]
167+ url_parts = result['url'][:35], result['url'][-35:]
168+ result['pretty_url'] = '{0}[...]{1}'.format(*url_parts)
135169 else:
136170 result['pretty_url'] = result['url']
137171
138172 for engine in result['engines']:
139− if engine in ['wikipedia', 'youtube', 'vimeo', 'soundcloud', 'twitter', 'stackoverflow', 'github']:
173+ if engine in favicons:
140174 result['favicon'] = engine
141175
142176 if request_data.get('format') == 'json':
143− return Response(json.dumps({'query': query, 'results': results}), mimetype='application/json')
177+ return Response(json.dumps({'query': query, 'results': results}),
178+ mimetype='application/json')
144179 elif request_data.get('format') == 'csv':
145180 csv = UnicodeWriter(cStringIO.StringIO())
146181 keys = ('title', 'url', 'content', 'host', 'engine', 'score')
@@ -151,26 +186,28 @@ def index():
151186 csv.writerow([row.get(key, '') for key in keys])
152187 csv.stream.seek(0)
153188 response = Response(csv.stream.read(), mimetype='application/csv')
154− response.headers.add('Content-Disposition', 'attachment;Filename=searx_-_{0}.csv'.format('_'.join(query.split())))
189+ content_disp = 'attachment;Filename=searx_-_{0}.csv'.format(query)
190+ response.headers.add('Content-Disposition', content_disp)
155191 return response
156192 elif request_data.get('format') == 'rss':
157− response_rss = render('opensearch_response_rss.xml'
158− ,results=results
159− ,q=request_data['q']
160− ,number_of_results=len(results)
161− ,base_url=get_base_url()
162− )
193+ response_rss = render(
194+ 'opensearch_response_rss.xml',
195+ results=results,
196+ q=request_data['q'],
197+ number_of_results=len(results),
198+ base_url=get_base_url()
199+ )
163200 return Response(response_rss, mimetype='text/xml')
164201
165−
166− return render('results.html'
167− ,results=results
168− ,q=request_data['q']
169− ,selected_categories=selected_categories
170− ,number_of_results=len(results)+len(featured_results)
171− ,featured_results=featured_results
172− ,suggestions=suggestions
173− )
202+ return render(
203+ 'results.html',
204+ results=results,
205+ q=request_data['q'],
206+ selected_categories=selected_categories,
207+ number_of_results=len(results) + len(featured_results),
208+ featured_results=featured_results,
209+ suggestions=suggestions
210+ )
174211
175212
176213@app.route('/about', methods=['GET'])
@@ -187,20 +224,37 @@ def list_engines():
187224@app.route('/preferences', methods=['GET', 'POST'])
188225def preferences():
189226
190− if request.method=='POST':
227+ if request.method == 'POST':
191228 selected_categories = []
192− for pd_name,pd in request.form.items():
229+ locale = None
230+ for pd_name, pd in request.form.items():
193231 if pd_name.startswith('category_'):
194232 category = pd_name[9:]
195233 if not category in categories:
196234 continue
197235 selected_categories.append(category)
236+ elif pd_name == 'locale' and pd in settings['locales']:
237+ locale = pd
238+
239+ resp = make_response(redirect('/'))
240+
241+ if locale:
242+ # cookie max age: 4 weeks
243+ resp.set_cookie(
244+ 'locale', locale,
245+ max_age=60 * 60 * 24 * 7 * 4
246+ )
247+
198248 if selected_categories:
199− resp = make_response(redirect('/'))
200249 # cookie max age: 4 weeks
201− resp.set_cookie('categories', ','.join(selected_categories), max_age=60*60*24*7*4)
202− return resp
203− return render('preferences.html')
250+ resp.set_cookie(
251+ 'categories', ','.join(selected_categories),
252+ max_age=60 * 60 * 24 * 7 * 4
253+ )
254+ return resp
255+ return render('preferences.html',
256+ locales=settings['locales'],
257+ current_locale=get_locale())
204258
205259
206260@app.route('/stats', methods=['GET'])
@@ -216,6 +270,7 @@ def robots():
216270Allow: /
217271Allow: /about
218272Disallow: /stats
273+Disallow: /engines
219274""", mimetype='text/plain')
220275
221276
@@ -229,24 +284,27 @@ def opensearch():
229284 base_url = get_base_url()
230285 ret = opensearch_xml.format(method=method, host=base_url)
231286 resp = Response(response=ret,
232− status=200,
233− mimetype="application/xml")
287+ status=200,
288+ mimetype="application/xml")
234289 return resp
235290
291+
236292@app.route('/favicon.ico')
237293def favicon():
238294 return send_from_directory(os.path.join(app.root_path, 'static/img'),
239− 'favicon.png', mimetype='image/vnd.microsoft.icon')
295+ 'favicon.png',
296+ mimetype='image/vnd.microsoft.icon')
240297
241298
242299def run():
243300 from gevent import monkey
244301 monkey.patch_all()
245302
246− app.run(debug = settings['server']['debug']
247− ,use_debugger = settings['server']['debug']
248− ,port = settings['server']['port']
249− )
303+ app.run(
304+ debug=settings['server']['debug'],
305+ use_debugger=settings['server']['debug'],
306+ port=settings['server']['port']
307+ )
250308
251309
252310if __name__ == "__main__":
setup.py+20−3
@@ -11,12 +11,12 @@ def read(*rnames):
1111 return open(os.path.join(os.path.dirname(__file__), *rnames)).read()
1212
1313
14−long_description = read('README.md')
14+long_description = read('README.rst')
1515
1616setup(
1717 name='searx',
18− version="0.1",
19− description="",
18+ version="0.1.2",
19+ description="A privacy-respecting, hackable metasearch engine",
2020 long_description=long_description,
2121 classifiers=[
2222 "Programming Language :: Python",
@@ -30,6 +30,7 @@ setup(
3030 zip_safe=False,
3131 install_requires=[
3232 'flask',
33+ 'flask-babel',
3334 'grequests',
3435 'lxml',
3536 'pyyaml',
@@ -49,4 +50,20 @@ setup(
4950 'zope.testrunner',
5051 ]
5152 },
53+ entry_points={
54+ 'console_scripts': [
55+ 'searx-run = searx.webapp:run'
56+ ]
57+ },
58+ package_data={
59+ 'searx': [
60+ 'settings.yml',
61+ '../README.rst',
62+ 'static/*/*',
63+ 'translations/*/*',
64+ 'templates/*.html',
65+ 'templates/result_templates/*.html',
66+ ],
67+ },
68+
5269)
versions.cfg+1
@@ -1,5 +1,6 @@
11[versions]
22Flask = 0.10.1
3+Flask-Babel = 0.9
34Jinja2 = 2.7.2
45MarkupSafe = 0.18
56WebOb = 1.3.1